C语言中的面向对象


157

什么是一组能使C语言实现某种丑陋(但可用)的面向对象的漂亮的预处理程序黑客(与ANSI C89 / ISO C90兼容)?

我熟悉几种不同的面向对象语言,因此请不要回答“学习C ++!”之类的答案。我已经阅读了“ 使用ANSI C进行面向对象的编程 ”(当心:PDF格式)和其他一些有趣的解决方案,但是我对您的最感兴趣:-)!


另请参见您可以用C编写面向对象的代码吗?


1
我可以回应学习D并在真正需要C的地方使用c兼容的abi吗?digitalmars.com/d
Tim Matthews,2009年

2
@Dinah:谢谢您的“另请参阅”。那篇文章很有趣。

1
有趣的问题似乎是您为什么要在C上进行OOP的预处理程序破解?
Calyth

3
@Calyth:我发现OOP很有用,并且“我在某些实际上只有C编译器可用的嵌入式系统上工作”(上面)。此外,您是否不觉得有趣的预处理器骇客有趣?

Answers:


31

C对象系统(COS)听起来很有希望(它仍处于alpha版本)。为了简单和灵活起见,它尝试将可用概念保持在最低限度:统一的面向对象的编程,包括开放类,元类,属性元类,泛型,多方法,委托,所有权,异常,协定和闭包。有一份描述它的草稿(PDF)。

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

两者都是Laurent Deniau编写的,后者正在C中从事OOP方面的工作。


@vonbrand COS迁移到了去年夏天提交的github。成熟可以解释缺乏提交。
philant

185

我建议不要使用预处理器(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

要获得多态性(即虚拟函数),请使用函数指针,以及可选的函数指针表,也称为虚拟表或vtable:

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,否则您会意外地影响给定类型的所有对象。


6
亚当,更改类型的全局vtable的乐趣在于模拟C语言中的鸭子输入。:)
jmucchiello

现在我很遗憾C ++ ...当然,C ++语法更清晰,但是由于它不是一个琐碎的语法,因此我感到很轻松。我想知道是否可以实现C ++和C之间的某种混合,所以void *仍然是有效的可转换类型。用的部分struct derived {struct base super;};很明显可以猜测它是如何工作的,因为按字节顺序排列是正确的。
jokoon 2011年

2
+1编写精美的代码。这正是我想要的!
Homunculus Reticulli'5年

3
做得好。这正是我一直在做的事情,也是正确的方法。不必考虑指向结构体/对象的指针,而只需将指针传递给整数(地址)即可。这将允许您传入任何类型的对象以进行无限多态方法调用。另外,唯一缺少的是用于初始化结构(对象/类)的函数。这将包括一个malloc函数并返回一个指针。也许我会添加一块如何做C.消息传递(Objective-C的)

1
这是我不愿使用C ++的稻草,要多使用C(在我仅将C ++用于继承之前)谢谢
Anne Quinn

31

我曾经使用过一个C库,该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文件准确定义了您应该对每个对象执行的操作。


我非常喜欢这种解决方案,只是所有“对象”的内部都是公开的。
劳伦斯·多尔

6
@Software Monkey:C没有访问控制。隐藏实现细节的唯一方法是通过不透明的指针进行交互,这很痛苦,因为所有字段都需要通过可能无法内联的访问器方法进行访问。
亚当·罗森菲尔德

1
@Adam:支持链接时优化的编译器将对其进行内联处理……
Christoph

9
如果这样做,还应确保.c文件中所有未定义为public的函数都被定义为静态的,这样它们就不会最终成为目标文件中的命名函数。这样可以确保在链接阶段没有人可以找到他们的名字。
jmucchiello

2
@Marcel:之所以使用C,是因为代码已部署在运行各种自治系统处理器的低级板上。他们都支持从C编译到各自的本地二进制文件。一旦您意识到他们想要做什么,该方法就使代码非常易于阅读。
Kieveli 2013年

18

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

它包括预处理器黑客,可进行类层次结构中的类型转换等操作。这是我为GNOME编写的示例类(例如gchar等都是typedef):

类源

类头

在GObject结构内部,有一个GType整数,它用作GLib动态类型系统的魔术数字(您可以将整个结构转换为“ GType”以找到其类型)。


不幸的是,自述文件/教程文件(Wiki链接)无法正常工作,并且只有相应的参考手册(我说的是GObject,而不是GTK)。请提供相同的一些教程文件...
FL4SOF,2009年

链接已修复。
詹姆斯·开普

4
链接再次断开。
SeanRamey

6

在我不知道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);

PS:vint只是int的typedef-我用它来提醒我,它的长度因平台而异(用于移植)。


7
天哪,这可能会赢得混乱的C竞赛!我喜欢!:)
horseyguy

@horseyguy不行。已经出版了。他们还考虑包括针对iocccsize工具的头文件滥用。它也不是一个完整的程序。2009年没有比赛,因此无法比较iocccsize。CPP也被滥用了很多次,所以它已经相当老了。对不起 我不是要否定我是现实的。我有点理解了您的意思,这是一本不错的书,并且我已对其进行了投票。(是的,我参加了比赛,也是的我也赢了。)
Pryftan

6

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

保存在这里


我以前确实看过。我相信这是一项不错的工作。

@Anthony Cuozzo:Stan Lippman写了一本很棒的书,叫做《 C ++-对象模型内部》,他在写作和维护c-front方面分享了他的许多经验和设计决策。多年以来从C过渡到C ++时,它仍然是一本好书,对我提供了极大帮助
zebrabox

5

如果您将调用对象的方法视为将隐式' this' 传递给函数的静态方法,则可以使C语言中的OO更加容易。

例如:

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

变成:

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

或类似的东西。


6
@Artelius:当然可以,但是有时候显而易见,除非明确指出。为此+1。
劳伦斯·多尔

1
更好的是string->length(s);
OozeMeister 2014年

4

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


我没有在它(ffmpeg)中看到任何工厂功能,而是似乎没有使用多态性/继承性(上面建议的琐碎方法)。
FL4SOF,2009年

avcodec_open是一项工厂功能。它将函数指针填充到AVCodecContext结构中(例如draw_horiz_band)。如果您在avcodec.h中查看FF_COMMON_FRAME宏的用法,您会看到类似于数据成员继承的内容。恕我直言,ffmpeg的证明,我认为OOP最好用C ++实现,而不是C.
先生福兹

3

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

您也可以使用伪目标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];
}

__attribute__((constructor))做什么void __meta_Foo_init(void) __attribute__((constructor))
AE Drew

1
这是一个GCC扩展,可以确保在将二进制文件加载到内存时调用标记的函数。@AEDrew
Maxthon Chan

popen(3)还返回FILE *另一个示例。
Pryftan

3

我认为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 ++中添加的其他任何内容(例如重载或模板)无关。是的,这些都是非常有用的功能,但它们并不代表什么是面向对象的编程。


创建结构后,可以声明typedef struct Monkey {} Monkey; 它的类型定义有什么意义?
MarcusJ '16

1
@MarcusJ这struct _monkey只是一个原型。实际的类型定义在实现文件(.c文件)中定义。这会产生封装效果,并允许API开发人员将来重新定义猴子结构而无需修改API。API的用户只需要关心实际的方法。API设计人员负责实现,包括对象/结构的布局方式。因此,对象/结构的详细信息对用户隐藏(不透明类型)。

我在标题中定义了我的结构,这不是标准的吗?好吧,我这样做是因为我有时需要访问该库之外的结构成员。
MarcusJ

1
@MarcusJ您可以根据需要在标头中定义结构(没有标准)。但是,如果您想改变它的内部结构,则可能会破坏您的代码。封装只是一种编码样式,它使得在不破坏代码的情况下更轻松地更改实现。int getCount(ObjectType obj)如果您选择在实现文件中定义结构,则始终可以通过诸如etc之类的访问器方法访问成员。

2

我的建议:保持简单。我遇到的最大问题之一是维护较旧的软件(有时已使用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. 易于实施,阅读,理解和维护

1

如果我要用CI编写OOP,可能会使用伪Pimpl设计。最终没有传递指向结构的指针,而是传递了指向结构的指针。这使内容不透明,并促进了多态性和继承。

C中的OOP真正的问题是变量退出作用域时会发生什么。没有编译器生成的析构函数,并且可能导致问题。可能会有所帮助,但是要看它总是很丑陋的。


1
使用C进行编程时,我通过使用if语句并在最后释放它们来处理范围。例如if ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }

1

使用C以面向对象样式进行编程的另一种方法是使用代码生成器,该代码生成器将特定于域的语言转换为C。就像使用TypeScript和JavaScript将OOP引入js一样。


0
#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进行OO编程的一个例子。

这是真实的纯C语言,没有预处理程序宏。我们具有继承,多态和数据封装(包括类或对象专用的数据)。受保护的限定词等效项是没有机会的,也就是说,私有数据在继承链中也是私有的。但这并不是不便之处,因为我认为没有必要。

CPolygon 之所以没有实例化它,是因为我们仅使用它来操作继承链中具有相同方面但实现方式不同(多态性)的对象。


0

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

此外,我建议您阅读

1)pjsip

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

2)iOS执行阶段

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


0

对我来说,C语言中的面向对象应具有以下功能:

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

  2. 继承和对多态的支持(可以使用结构实现单个继承-确保抽象库不可实例化)

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

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

  5. 引用计数(或用于实现RAII的内容

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

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


对于数字(5)-不能使用没有析构函数的语言来实现RAII(这意味着RAII在C或Java中不是编译器支持的技术)。
汤姆

可以为基于c的对象编写构造函数和析构函数-我猜GObject可以做到。当然还有RAAI(这不是直截了当的,可能很丑陋,根本不需要务实)-我一直在寻找的是识别基于C的语义来达到上述目的。
FL4SOF,2009年

C不支持析构函数。您必须输入一些内容才能使其正常工作。这意味着他们不清理自己。GObject不会更改语言。
汤姆


0

我在这里参加聚会有点晚,但是我想避免两种极端的情况-太多或太多的混淆代码,但是几个明显的宏可以使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选项)是有用的,而不是造成混淆。关键是要提高程序员的工作效率吗?



0

我也在基于宏解决方案进行此工作。我想这仅是最勇敢的;-)但它已经非常不错了,我已经在上面进行了一些项目。它可以正常工作,以便您首先为每个类定义一个单独的头文件。像这样:

#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中引入的struct初始化。它运行良好,未列出的所有内容均设为零。

要调用方法,请使用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中,那么它可以节省大量工作。wobject是一个非常简单的基于正则表达式的扫描程序,可以轻松满足特定需求或从头开始重写。



0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.