C中的面向对象

什么将是一个漂亮的预处理器黑客(ANSI C89 / ISO C90兼容),使一些丑陋的(但可用)在C面向对象?

我熟悉一些不同的面向对象的语言,所以请不要回答“Learn C ++!”这样的答案。 我已经阅读了“ 使用ANSI C编写面向对象的程序 ”(注意: PDF格式 )以及其他一些有趣的解决scheme,但是我主要对你感兴趣:-)!


另请参阅您可以在C编写面向对象的代码?

C对象系统(COS)听起来很有前途(它仍然是alpha版本)。 为了简单和灵活性,它试图保持最小的可用概念:统一的面向对象编程,包括公开类,元类,属性元类,generics,多方法,委托,所有权,exception,契约和闭包。 有一个描述它的草稿 (PDF)。

C中的例外是在其他OO语言中发现的TRY-CATCH-FINALLY的C89实现。 它带有一个testing套件和一些例子。

由劳伦特Deniau,这是在C的OOP工作很多。

我build议不要使用预处理器(ab)来尝试使C语法更像另一种更面向对象的语言。 在最基本的层面上,您只需使用普通结构作为对象,并通过指针传递它们:

struct monkey { float age; bool is_male; int happiness; }; void monkey_dance(struct monkey *monkey) { /* do a little dance */ } 

要获得inheritance和多态的东西,你必须努力一点。 您可以通过让结构的第一个成员是超类的实例来进行手动inheritance,然后您可以自由地指针指向基类和派生类:

 struct base { /* base class members */ }; struct derived { struct base super; /* derived class members */ }; struct derived d; struct base *base_ptr = (struct base *)&d; // upcast struct derived *derived_ptr = (struct derived *)base_ptr; // downcast 

要获得多态性(即虚拟函数),可以使用函数指针和函数指针表(也称为虚拟表或vtables):

 struct base; struct base_vtable { void (*dance)(struct base *); void (*jump)(struct base *, int how_high); }; struct base { struct base_vtable *vtable; /* base members */ }; void base_dance(struct base *b) { b->vtable->dance(b); } void base_jump(struct base *b, int how_high) { b->vtable->jump(b, how_high); } struct derived1 { struct base super; /* derived1 members */ }; void derived1_dance(struct derived1 *d) { /* implementation of derived1's dance function */ } void derived1_jump(struct derived1 *d, int how_high) { /* implementation of derived 1's jump function */ } /* global vtable for derived1 */ struct base_vtable derived1_vtable = { &derived1_dance, /* you might get a warning here about incompatible pointer types */ &derived1_jump /* you can ignore it, or perform a cast to get rid of it */ }; void derived1_init(struct derived1 *d) { d->super.vtable = &derived1_vtable; /* init base members d->super.foo */ /* init derived1 members d->foo */ } struct derived2 { struct base super; /* derived2 members */ }; void derived2_dance(struct derived2 *d) { /* implementation of derived2's dance function */ } void derived2_jump(struct derived2 *d, int how_high) { /* implementation of derived2's jump function */ } struct base_vtable derived2_vtable = { &derived2_dance, &derived2_jump }; void derived2_init(struct derived2 *d) { d->super.vtable = &derived2_vtable; /* init base members d->super.foo */ /* init derived1 members d->foo */ } int main(void) { /* OK! We're done with our declarations, now we can finally do some polymorphism in C */ struct derived1 d1; derived1_init(&d1); struct derived2 d2; derived2_init(&d2); struct base *b1_ptr = (struct base *)&d1; struct base *b2_ptr = (struct base *)&d2; base_dance(b1_ptr); /* calls derived1_dance */ base_dance(b2_ptr); /* calls derived2_dance */ base_jump(b1_ptr, 42); /* calls derived1_jump */ base_jump(b2_ptr, 42); /* calls derived2_jump */ return 0; } 

这就是你如何在C中做多态性。这不是很好,但是它做的工作。 有一些棘手的问题涉及基类和派生类之间的指针转换,只要基类是派生类的第一个成员,它们就是安全的。 多重inheritance比较困难 – 在这种情况下,为了在基类之外的情况下,你需要根据适当的偏移手动调整指针,这真的很棘手,容易出错。

另一个(棘手的)你可以做的事情是在运行时改变一个对象的dynamictypes! 你只是重新分配一个新的vtable指针。 你甚至可以有select地改变一些虚拟function,同时保留其他function,创build新的混合types。 只要小心创build一个新的vtable,而不是修改全局的vtable,否则你会不小心影响给定types的所有对象。

我曾经与一个C库一起工作,这种C库的实现方式让我觉得非常优雅。 他们用C写了一种定义对象的方法,然后inheritance它们,使它们像C ++对象一样可扩展。 基本的想法是这样的:

  • 每个对象都有自己的文件
  • 公用函数和variables在.h文件中为对象定义
  • 私有variables和函数只位于.c文件中
  • 为了“inheritance”一个新的结构,该结构的第一个成员是要inheritance的对象

inheritance很难描述,但基本上是这样的:

 struct vehicle { int power; int weight; } 

然后在另一个文件中:

 struct van { struct vehicle base; int cubic_size; } 

那么你可以在记忆中创造一辆面包车,并被代码使用,只知道车辆:

 struct van my_van; struct vehicle *something = &my_van; vehicle_function( something ); 

它工作得非常好,.h文件定义了你应该能够对每个对象做什么。

用于Linux的GNOME桌面是用面向对象的C语言编写的,它有一个名为“ GObject ”的对象模型,它支持属性,inheritance,多态以及其他一些好处,如引用,事件处理(称为“信号”),运行时打字,私人数据等

它包括预处理器的黑客来做类似于在类层次结构中的types转换等等。下面是我为GNOME写的一个例子类(像gchar是typedef):

class级来源

类标题

在GObject结构中,有一个GType整数,用作GLibdynamictypes系统的幻数(可以将整个结构转换为“GType”来查找它的types)。

如果将对象称为静态方法,将隐式“ this ”传递给函数,则可以使C中的OO更容易。

例如:

 String s = "hi"; System.out.println(s.length()); 

变为:

 string s = "hi"; printf(length(s)); // pass in s, as an implicit this 

或类似的东西。

在我知道OOP是什么之前,我曾经用C来做这种事情。

下面是一个例子,它实现了一个按需增长的数据缓冲区,给定最小的大小,增量和最大的大小。 这个特定的实现是基于“元素”的,也就是说它被devise为允许任何Ctypes的列表类集合,而不仅仅是可变长度的字节缓冲区。

这个想法是使用xxx_crt()实例化对象,并使用xxx_dlt()删除。 每个“成员”方法采取特定types的指针进行操作。

我以这种方式实现了一个链表,循环缓冲区和其他一些东西。

我必须承认,我从来没有想过如何用这种方法来实现inheritance。 我想像Kieveli提供的一些混合可能是一条好path。

dtb.c:

 #include <limits.h> #include <string.h> #include <stdlib.h> static void dtb_xlt(void *dst, const void *src, vint len, const byte *tbl); DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz) { DTABUF *dbp; if(!minsiz) { return NULL; } if(!incsiz) { incsiz=minsiz; } if(!maxsiz || maxsiz<minsiz) { maxsiz=minsiz; } if(minsiz+incsiz>maxsiz) { incsiz=maxsiz-minsiz; } if((dbp=(DTABUF*)malloc(sizeof(*dbp))) == NULL) { return NULL; } memset(dbp,0,sizeof(*dbp)); dbp->min=minsiz; dbp->inc=incsiz; dbp->max=maxsiz; dbp->siz=minsiz; dbp->cur=0; if((dbp->dta=(byte*)malloc((vuns)minsiz)) == NULL) { free(dbp); return NULL; } return dbp; } DTABUF *dtb_dlt(DTABUF *dbp) { if(dbp) { free(dbp->dta); free(dbp); } return NULL; } vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen) { if(!dbp) { errno=EINVAL; return -1; } if(dtalen==-1) { dtalen=(vint)strlen((byte*)dtaptr); } if((dbp->cur + dtalen) > dbp->siz) { void *newdta; vint newsiz; if((dbp->siz+dbp->inc)>=(dbp->cur+dtalen)) { newsiz=dbp->siz+dbp->inc; } else { newsiz=dbp->cur+dtalen; } if(newsiz>dbp->max) { errno=ETRUNC; return -1; } if((newdta=realloc(dbp->dta,(vuns)newsiz))==NULL) { return -1; } dbp->dta=newdta; dbp->siz=newsiz; } if(dtalen) { if(xlt256) { dtb_xlt(((byte*)dbp->dta+dbp->cur),dtaptr,dtalen,xlt256); } else { memcpy(((byte*)dbp->dta+dbp->cur),dtaptr,(vuns)dtalen); } dbp->cur+=dtalen; } return 0; } static void dtb_xlt(void *dst,const void *src,vint len,const byte *tbl) { byte *sp,*dp; for(sp=(byte*)src,dp=(byte*)dst; len; len--,sp++,dp++) { *dp=tbl[*sp]; } } vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...) { byte textÝ501¨; va_list ap; vint len; va_start(ap,format); len=sprintf_len(format,ap)-1; va_end(ap); if(len<0 || len>=sizeof(text)) { sprintf_safe(text,sizeof(text),"STRTOOLNG: %s",format); len=(int)strlen(text); } else { va_start(ap,format); vsprintf(text,format,ap); va_end(ap); } return dtb_adddta(dbp,xlt256,text,len); } vint dtb_rmvdta(DTABUF *dbp,vint len) { if(!dbp) { errno=EINVAL; return -1; } if(len > dbp->cur) { len=dbp->cur; } dbp->cur-=len; return 0; } vint dtb_reset(DTABUF *dbp) { if(!dbp) { errno=EINVAL; return -1; } dbp->cur=0; if(dbp->siz > dbp->min) { byte *newdta; if((newdta=(byte*)realloc(dbp->dta,(vuns)dbp->min))==NULL) { free(dbp->dta); dbp->dta=null; dbp->siz=0; return -1; } dbp->dta=newdta; dbp->siz=dbp->min; } return 0; } void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen) { if(!elmlen || (elmidx*elmlen)>=dbp->cur) { return NULL; } return ((byte*)dbp->dta+(elmidx*elmlen)); } 

dtb.h

 typedef _Packed struct { vint min; /* initial size */ vint inc; /* increment size */ vint max; /* maximum size */ vint siz; /* current size */ vint cur; /* current data length */ void *dta; /* data pointer */ } DTABUF; #define dtb_dtaptr(mDBP) (mDBP->dta) #define dtb_dtalen(mDBP) (mDBP->cur) DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz); DTABUF *dtb_dlt(DTABUF *dbp); vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen); vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...); vint dtb_rmvdta(DTABUF *dbp,vint len); vint dtb_reset(DTABUF *dbp); void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen); 

PS:vint简直是int的typedef – 我用它来提醒我,从平台到平台的长度是可变的(用于移植)。

ffmpeg (一个用于video处理的工具包)是用直C(和汇编语言)编写的,但是使用面向对象的风格。 它充满了带有函数指针的结构。 有一组工厂函数用适当的“方法”指针初始化结构。

稍微偏离主题,但最初的C ++编译器Cfront将C ++编译为C,然后编译为汇编器。

在这里保存。

如果你真的认为,即使标准的C库使用OOP – 考虑FILE *为例: fopen()初始化一个FILE *对象,并使用它的成员方法fscanf()fprintf()fread()fwrite()和其他,并最终使用fclose()

您也可以使用伪Objective-C的方式,这也不难:

 typedef void *Class; typedef struct __class_Foo { Class isa; int ivar; } Foo; typedef struct __meta_Foo { Foo *(*alloc)(void); Foo *(*init)(Foo *self); int (*ivar)(Foo *self); void (*setIvar)(Foo *self); } meta_Foo; meta_Foo *class_Foo; void __meta_Foo_init(void) __attribute__((constructor)); void __meta_Foo_init(void) { class_Foo = malloc(sizeof(meta_Foo)); if (class_Foo) { class_Foo = {__imp_Foo_alloc, __imp_Foo_init, __imp_Foo_ivar, __imp_Foo_setIvar}; } } Foo *__imp_Foo_alloc(void) { Foo *foo = malloc(sizeof(Foo)); if (foo) { memset(foo, 0, sizeof(Foo)); foo->isa = class_Foo; } return foo; } Foo *__imp_Foo_init(Foo *self) { if (self) { self->ivar = 42; } return self; } // ... 

使用:

 int main(void) { Foo *foo = (class_Foo->init)((class_Foo->alloc)()); printf("%d\n", (foo->isa->ivar)(foo)); // 42 foo->isa->setIvar(foo, 60); printf("%d\n", (foo->isa->ivar)(foo)); // 60 free(foo); } 

如果使用一个相当古老的Objective-C-to-C转换器,那么这可能是由于这样的一些Objective-C代码造成的:

 @interface Foo : NSObject { int ivar; } - (int)ivar; - (void)setIvar:(int)ivar; @end @implementation Foo - (id)init { if (self = [super init]) { ivar = 42; } return self; } @end int main(void) { Foo *foo = [[Foo alloc] init]; printf("%d\n", [foo ivar]); [foo setIvar:60]; printf("%d\n", [foo ivar]); [foo release]; } 

我认为Adam Rosenfield发表的是用C做OOP的正确方法。我想补充的是,他所展示的是对象的实现。 换句话说,实际的实现将放在.c文件中,而接口将放在头文件.h文件中。 例如,使用上面的猴子示例:

界面如下所示:

 //monkey.h struct _monkey; typedef struct _monkey monkey; //memory management monkey * monkey_new(); int monkey_delete(monkey *thisobj); //methods void monkey_dance(monkey *thisobj); 

你可以在界面.h文件中看到,你只是定义了原型。 然后可以将实现部分“ .c文件”编译为静态或dynamic库。 这将创build封装,也可以随意更改实现。 你的对象的用户需要几乎不知道它的实现。 这也把重点放在对象的整体devise上。

我个人认为,oop是一种概念化你的代码结构和可重用性的方法,并且与那些添加到c ++中的其他东西(如重载或模板)没有任何关系。 是的,这些是非常好的有用的function,但它们并不代表什么是面向对象的编程。

我的build议:保持简单。 我遇到的最大的问题之一是维护较旧的软件(有时超过10年)。 如果代码不简单,可能会很困难。 是的,可以用C中的多态性编写非常有用的OOP,但可能难以阅读。

我更喜欢简单的对象封装一些定义良好的function。 一个很好的例子是GLIB2 ,例如一个哈希表:

 GHastTable* my_hash = g_hash_table_new(g_str_hash, g_str_equal); int size = g_hash_table_size(my_hash); ... g_hash_table_remove(my_hash, some_key); 

关键是:

  1. 简单的build筑和devise模式
  2. 实现基本的OOP封装。
  3. 易于实施,阅读,理解和维护

如果我要在CI中编写面向对象的应用程序,可能会使用伪Pimpldevise。 不要将指针传递给结构体,而是最终将指针传递给结构体的指针。 这使内容不透明,并促进多态性和inheritance。

C中OOP的真正问题是variables退出范围时会发生什么。 没有编译器生成的析构函数,可能会导致问题。 macros可能会有所帮助,但总会看起来很难看。

 #include "triangle.h" #include "rectangle.h" #include "polygon.h" #include <stdio.h> int main() { Triangle tr1= CTriangle->new(); Rectangle rc1= CRectangle->new(); tr1->width= rc1->width= 3.2; tr1->height= rc1->height= 4.1; CPolygon->printArea((Polygon)tr1); printf("\n"); CPolygon->printArea((Polygon)rc1); } 

输出:

 6.56 13.12 

这是一个什么是用C编写的面向对象的程序

这是纯粹的C,没有预处理macros。 我们有inheritance,多态和数据封装(包括类或对象的私有数据)。 保护限定符的等价性是没有机会的,也就是私有数据在私有链中也是私有的。 但这不是一个不便,因为我不认为这是必要的。

CPolygon没有被实例化,因为我们只用它来操纵具有共同方面但不同实现它们(多态性)的inheritance性链的对象。

@Adam Rosenfield对于如何用C实现OOP有很好的解释

此外,我会build议你阅读

1) pjsip

一个非常好的C语言库。 您可以通过结构和函数指针表了解它是如何实现OOP的

2) iOS运行时

了解iOS运行时如何支持Objective C,通过isa指针,元类实现OOP

对于我来说,在C中的对象方向应该有这些function:

  1. 封装和数据隐藏(可以使用结构体/不透明指针来实现)

  2. 对多态的inheritance和支持(使用结构可以实现单一inheritance – 确保抽象基础不可实例化)

  3. 构造函数和析构函数(不易实现)

  4. types检查(至less对于用户定义的types,因为C没有强制执行)

  5. 引用计数(或实施RAII )

  6. 有限的exception处理支持(setjmp和longjmp)

在上面它应该依赖ANSI / ISO规范,不应该依赖编译器特定的function。

看看http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html 。 如果没有其他阅读文档是一个启发性的经验。

我在这里晚了一点,但是我喜欢避免两个macros观的极端 – 太多或太多的混淆代码,但是一些明显的macros可以使OOP代码更易于开发和阅读:

 /* * OOP in C * * gcc -o oop oop.c */ #include <stdio.h> #include <stdlib.h> #include <math.h> struct obj2d { float x; // object center x float y; // object center y float (* area)(void *); }; #define X(obj) (obj)->b1.x #define Y(obj) (obj)->b1.y #define AREA(obj) (obj)->b1.area(obj) void * _new_obj2d(int size, void * areafn) { struct obj2d * x = calloc(1, size); x->area = areafn; // obj2d constructor code ... return x; } // -------------------------------------------------------- struct rectangle { struct obj2d b1; // base class float width; float height; float rotation; }; #define WIDTH(obj) (obj)->width #define HEIGHT(obj) (obj)->height float rectangle_area(struct rectangle * self) { return self->width * self->height; } #define NEW_rectangle() _new_obj2d(sizeof(struct rectangle), rectangle_area) // -------------------------------------------------------- struct triangle { struct obj2d b1; // deliberately unfinished to test error messages }; #define NEW_triangle() _new_obj2d(sizeof(struct triangle), triangle_area) // -------------------------------------------------------- struct circle { struct obj2d b1; float radius; }; #define RADIUS(obj) (obj)->radius float circle_area(struct circle * self) { return M_PI * self->radius * self->radius; } #define NEW_circle() _new_obj2d(sizeof(struct circle), circle_area) // -------------------------------------------------------- #define NEW(objname) (struct objname *) NEW_##objname() int main(int ac, char * av[]) { struct rectangle * obj1 = NEW(rectangle); struct circle * obj2 = NEW(circle); X(obj1) = 1; Y(obj1) = 1; // your decision as to which of these is clearer, but note above that // macros also hide the fact that a member is in the base class WIDTH(obj1) = 2; obj1->height = 3; printf("obj1 position (%f,%f) area %f\n", X(obj1), Y(obj1), AREA(obj1)); X(obj2) = 10; Y(obj2) = 10; RADIUS(obj2) = 1.5; printf("obj2 position (%f,%f) area %f\n", X(obj2), Y(obj2), AREA(obj2)); // WIDTH(obj2) = 2; // error: struct circle has no member named width // struct triangle * obj3 = NEW(triangle); // error: triangle_area undefined } 

我认为这有一个很好的平衡,它产生的错误(至less在默认的gcc 6.3选项)的一些更可能的错误是有益的,而不是混淆。 整个问题的关键是提高程序员的生产力吗?

如果你需要编写一个小代码,请试试这个: https : //github.com/fulminati/class-framework

 #include "class-framework.h" CLASS (People) { int age; }; int main() { People *p = NEW (People); p->age = 10; printf("%d\n", p->age); }