C 语言中的面向对象

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

我熟悉几种不同的面向对象语言,所以请不要回答“学习 C + + !”.我读过“ 面向对象程序设计”(当心: PDF 格式)和其他一些有趣的解决方案,但我最感兴趣的是你的: —— !


参见 你能用 C 语言编写面向对象的代码吗

90832 次浏览

我曾经使用过一个 C 库,它的实现方式让我觉得非常优雅。他们用 C 语言编写了一种定义对象的方法,然后从这些对象继承,这样它们就像 C + + 对象一样具有可扩展性。基本思想是这样的:

  • 每个对象都有自己的文件
  • 公共函数和变量在对象的.h 文件中定义
  • 私有变量和函数仅位于.c 文件中
  • 为了“继承”一个新的结构体,创建该结构体的第一个成员是要继承的对象

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

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 文件确切地定义了您应该对每个对象执行的操作。

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

如果将对象调用的方法看作是静态方法,它将隐式的“ this”传递给函数,那么在 C 语言中思考面向对象就会变得更容易。

例如:

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

变成:

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

或者类似的东西。

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

它包括预处理器技巧,可以在类层次结构中进行类型转换等等。下面是我为 GNOME 编写的一个示例类(比如 gchar 是 typedefs) :

类别来源

班长

在 GObject 结构内部有一个 GType 整数,它被用作 GLib 动态类型系统的神奇数字(您可以将整个结构强制转换为“ GType”来查找它的类型)。

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

struct monkey
{
float age;
bool is_male;
int happiness;
};


void monkey_dance(struct monkey *monkey)
{
/* do a little dance */
}

要获得类似继承和多态性这样的东西,您必须更加努力地工作。你可以通过让结构的第一个成员是超类的一个实例来进行手动继承,然后你可以自由地转换指向基类和派生类的指针:

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 语言中多态性的实现方法。虽然不怎么样,但还是很管用。在基类和派生类之间存在一些涉及指针强制转换的棘手问题,只要基类是派生类的第一个成员,指针强制转换就是安全的。多重继承要困难得多——在这种情况下,为了在第一个类之外的基类之间进行大小写处理,需要根据适当的偏移量手动调整指针,这非常棘手,而且容易出错。

您可以做的另一件(棘手的)事情是在运行时更改对象的动态类型!您只需重新分配它一个新的 vtable 指针。您甚至可以有选择地更改一些虚函数,同时保留其他函数,从而创建新的混合类型。只要注意创建一个新的 vtable 而不是修改全局 vtable,否则您将意外地影响给定类型的所有对象。

在我知道 OOP 是什么之前,我曾经在 C 语言中做过类似的事情。

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

其思想是使用 xxx _ crt ()实例化对象,并使用 xxx _ dlt ()删除对象。每个“成员”方法都接受一个特定类型的指针进行操作。

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

我必须承认,我从未考虑过如何用这种方法实现继承。我想,基耶夫利提供的某种混合物可能是一条不错的道路。

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);

Vint 只是 int 的 typedef-我用它来提醒我,它的长度在不同的平台之间是可变的(用于移植)。

对我来说,C 语言中的面向对象应该具有以下特性:

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

  2. 继承和对多态性的支持(可以使用 structs 实现单一继承——确保抽象基不是可实例化的)

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

  4. 类型检查(至少对于用户定义的类型,因为 C 不强制执行任何类型)

  5. 引用计数(或实现 拉尔的东西)

  6. 对异常处理(setjmp 和 longjmp)的支持有限

最重要的是,它应该依赖于 ANSI/ISO 规范,而不应该依赖于特定于编译器的功能。

C 对象系统(COS) 听起来很有希望(仍然是 alpha 版本)。为了简单和灵活,它试图尽量减少可用的概念: 统一的面向对象编程,包括开放类、元类、属性元类、泛型、多方法、委托、所有权、异常、契约和闭包。有一个 草案文件(PDF)来描述它。

C 中的异常是在其他 OO 语言中发现的 TRY-CATCH-FINLY 的 C89实现。它附带了一个测试套件和一些示例。

都是 Laurent Deniau 写的,他在 C 区的 OOP上花了不少功夫。

如果我要用 C 编写面向对象程序设计,我可能会采用伪 皮普尔设计。不是将指针传递给结构,而是将指针传递给指向结构的指针。这使得内容不透明,并促进了多态性和继承。

C 语言中 OOP 的真正问题是当变量退出作用域时会发生什么。没有编译器生成的析构函数,这可能会导致问题。马克罗斯可能会有所帮助,但它看起来总是很丑陋。

稍微跑题了,但原来的 C + + 编译器,前面,编译 C + + 到 C 再到汇编器。

保存 给你

#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 语言,没有预处理宏,我们有继承, 多态性和数据封装(包括类或对象的数据私有)。 没有保护限定符等效的机会,也就是说, 私有数据在继承链中也是私有的。 但这并不麻烦,因为我认为没有这个必要。

CPolygon没有实例化,因为我们只用它来操作对象 继承链的下游,有共同的方面,但不同的 它们的实现(多态性)。

如果你真的仔细思考,即使是标准的 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];
}

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

我更喜欢封装一些定义良好的功能的简单对象。这方面的一个很好的例子是 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. 简单的体系结构和设计模式
  2. 实现基本的 OOP 封装。
  3. 易于实现、阅读、理解和维护

@ Adam Rosenfield 对于如何用 C 实现面向对象编程有很好的解释

另外,我建议你读一读

1) Pjsip

一个非常好的用于 VoIP 的 C 库,你可以通过 structs 和函数指针表来学习它是如何实现面向对象编程的

2) IOS 运行时间

了解 iOS 运行时如何支持 Objective C。它通过一个指针元类来实现 OOP

我认为 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文件”编译成静态或动态库。这将创建封装,而且您还可以随意更改实现。对象的用户几乎不需要了解对象的实现。这也把重点放在对象的整体设计上。

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

看看 http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html,如果没有其他的东西,通读文档是一种有启发性的体验。

这里我有点晚了,但是我想避免 都有宏的极端情况——太多或太多的混淆代码,但是一些明显的宏可以使 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
}

我认为这有一个很好的平衡,它产生的错误(至少与默认的 gcc 6.3选项)的一些更可能的错误是有益的,而不是混乱。重点是提高程序员的工作效率不是吗?

如果你需要编写一些代码 试试这个: < a href = “ https://github.com/fulminati/class-Framework”rel = “ nofollow norefrer”> 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);
}

我也正在基于一个宏解决方案进行这方面的工作。所以我想这只是为最勇敢的人准备的; ——但是它已经很不错了,而且我已经在它的基础上做了一些项目。 它的工作原理是首先为每个类定义一个单独的头文件,如下所示:

#define CLASS Point
#define BUILD_JSON


#define Point__define                            \
METHOD(Point,public,int,move_up,(int steps)) \
METHOD(Point,public,void,draw)               \
\
VAR(read,int,x,JSON(json_int))               \
VAR(read,int,y,JSON(json_int))               \

要实现这个类,需要为它创建一个头文件和一个 C 文件,在这个文件中实现方法:

METHOD(Point,public,void,draw)
{
printf("point at %d,%d\n", self->x, self->y);
}

在为该类创建的头中,包括所需的其他头并定义与该类相关的类型等。在类头文件和 C 文件中都包含类规范文件(参见第一个代码示例)和 X 宏。这些 X 宏(123等)将代码扩展到实际的类结构和其他声明。

要继承一个类,#define SUPER supername并将 supername__define \添加为类定义中的第一行。两个都必须在那里。还有 JSON 支持、信号、抽象类等。

要创建一个对象,只需使用 W_NEW(classname, .x=1, .y=2,...)。初始化基于 C11中引入的结构初始化。它工作得很好,没有列出的所有内容都设置为零。

若要调用方法,请使用 W_CALL(o,method)(1,2,3)。它看起来像一个高阶函数调用,但它只是一个宏。它扩展到 ((o)->klass->method(o,1,2,3)),这是一个非常好的黑客。

参见 文件自我编码

因为框架需要一些样板代码,所以我编写了一个 Perl 脚本(wobject)来完成这项工作。如果你用那个,你可以直接写

class Point
public int move_up(int steps)
public void draw()
read int x
read int y

它将创建类规范文件、类头文件和一个 C 文件,其中包括实现类的 Point_impl.c。它节省了相当多的工作,如果你有许多简单的类,但仍然一切都在 C。 我们反对是一个非常简单的基于正则表达式的扫描仪,很容易适应特定的需要,或从头开始重写。

开源的 Dynace 项目正是这样做的,它在 https://github.com/blakemcbride/Dynace

用 C 编写面向对象的程序的另一种方法是使用代码生成器将领域特定语言转换为 C。正如使用 TypeScript 和 JavaScript 将 OOP 带入 js 一样。

您可以尝试 COOP,这是一个用于 C 语言的面向对象编程的程序员友好的框架,它具有类、异常、多态性和内存管理(对于嵌入式代码很重要)。这是一种相对轻量级的语法,请参见这里的 维基教程