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);
关键是:
- 简单的build筑和devise模式
- 实现基本的OOP封装。
- 易于实施,阅读,理解和维护
如果我要在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:
-
封装和数据隐藏(可以使用结构体/不透明指针来实现)
-
对多态的inheritance和支持(使用结构可以实现单一inheritance – 确保抽象基础不可实例化)
-
构造函数和析构函数(不易实现)
-
types检查(至less对于用户定义的types,因为C没有强制执行)
-
引用计数(或实施RAII )
-
有限的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); }