什么是一组漂亮的预处理器黑客(ANSI C89/ISO C90兼容),它在C中实现某种丑陋(但可用)的面向对象?
我熟悉一些不同的面向对象语言,所以请不要回答"学习C++!"这样的答案.我读过" 面向对象的ANSI C编程 "(当心:PDF格式)和其他一些有趣的解决方案,但我最感兴趣的是你:-)!
另请参见您能用C编写面向对象的代码吗?
Ada*_*eld 181
我建议不要使用预处理器(ab)来尝试使C语法更像是另一种更面向对象的语言.在最基本的层面上,您只需使用普通结构作为对象并通过指针传递它们:
struct monkey
{
float age;
bool is_male;
int happiness;
};
void monkey_dance(struct monkey *monkey)
{
/* do a little dance */
}
Run Code Online (Sandbox Code Playgroud)
要获得继承和多态这样的东西,你必须更努力地工作.您可以通过让结构的第一个成员成为超类的实例来进行手动继承,然后您可以自由地转换指向基类和派生类的指针:
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
Run Code Online (Sandbox Code Playgroud)
要获得多态(即虚函数),可以使用函数指针,也可以使用函数指针表,也称为虚拟表或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;
}
Run Code Online (Sandbox Code Playgroud)
这就是你在C中做多态的方法.它并不漂亮,但它能完成这项任务.有一些棘手的问题涉及基类和派生类之间的指针转换,只要基类是派生类的第一个成员,它们是安全的.多重继承要困难得多 - 在这种情况下,为了除了第一个之外的基类之间的情况,你需要根据适当的偏移量手动调整指针,这非常棘手且容易出错.
您可以做的另一件(棘手的事)是在运行时更改对象的动态类型!你只需重新分配一个新的vtable指针.您甚至可以选择性地更改某些虚拟功能,同时保留其他功能,从而创建新的混合类型.只需要小心创建一个新的vtable而不是修改全局vtable,否则你会意外地影响给定类型的所有对象.
Kie*_*eli 31
我曾经使用过一个C库,它的实现方式令我感到非常优雅.他们用C语言编写了一种定义对象的方法,然后从它们继承,以便它们像C++对象一样可扩展.基本的想法是这样的:
继承很难描述,但基本上是这样的:
struct vehicle {
int power;
int weight;
}
Run Code Online (Sandbox Code Playgroud)
然后在另一个文件中:
struct van {
struct vehicle base;
int cubic_size;
}
Run Code Online (Sandbox Code Playgroud)
然后你可以在内存中创建一个面包车,并由只知道车辆的代码使用:
struct van my_van;
struct vehicle *something = &my_van;
vehicle_function( something );
Run Code Online (Sandbox Code Playgroud)
它运行得很漂亮,.h文件确切地定义了你应该能够对每个对象做什么.
如果你认为将对象调用的方法作为将隐式' this
' 传递给函数的静态方法,它可以使C中的OO更容易思考.
例如:
String s = "hi";
System.out.println(s.length());
Run Code Online (Sandbox Code Playgroud)
变为:
string s = "hi";
printf(length(s)); // pass in s, as an implicit this
Run Code Online (Sandbox Code Playgroud)
或类似的东西.
在我知道OOP是什么之前,我曾经在C中做过这种事情.
下面是一个示例,它实现了一个数据缓冲区,该缓冲区在给定最小大小,增量和最大大小的情况下按需增长.这个特定的实现是基于"元素"的,也就是说它被设计为允许任何C类型的类似列表的集合,而不仅仅是一个可变长度的字节缓冲区.
我们的想法是使用xxx_crt()实例化对象,并使用xxx_dlt()删除.每个"成员"方法都采用特定类型的指针进行操作.
我以这种方式实现了链表,循环缓冲区和许多其他东西.
我必须承认,我从未考虑过如何使用这种方法实现继承.我想Kieveli提供的一些混合可能是一条好路.
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));
}
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
PS:vint只是int的typedef - 我用它来提醒我,它的长度在平台之间是可变的(用于移植).
小智 5
我认为 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);
Run Code Online (Sandbox Code Playgroud)
您可以在接口.h
文件中看到您只定义了原型。然后,您可以将实现部分“.c
文件”编译为静态或动态库。这会创建封装,您也可以随意更改实现。对象的用户几乎不需要了解它的实现。这也将重点放在对象的整体设计上。
我个人认为 oop 是一种概念化代码结构和可重用性的方式,与添加到 C++ 中的其他内容(如重载或模板)无关。是的,这些是非常有用的功能,但它们并不能代表面向对象编程的真正含义。