您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
C的面向对象编程
 
作者:wangzz 来源:CSDN 发布于:2015-10-20
   次浏览      
 

C语言并不支持类这样的概念,但是C仍旧可以使用面向对象的概念。

C++中的类,关键在于它的虚函数表。因此,我们要模拟一个能够支持虚函数表的类。

使用C的struct结构,可以模拟类和虚函数。

比如,我们来模拟一个shape类

//模拟虚函数表  
typedef struct _Shape Shape;

struct ShapeClass {
void (*construct)(Shape* self);
void (*destroy)(Shape *self);
void (*draw)(Shape *self);
};

struct _Shape {
ShapeClass *klass; //定义class的指针
int x, y, width, height;
};

ShapeClass 定义了Shape类的虚函数表,其中construct和destroy分别模拟构造和析构函数,draw则是一个虚函数。Shape模拟数据成员。Shape中的ShapeClass将关联到具体的实现上。

Shape对象要能够使用,还必须做到以下几点

实现一个ShapeClass类

初始化Shape为正确的类

首先,我们要实现ShapeClass定义的各个成员函数指针

void Shape_construct(Shape* self) {  
self->x = 0;
self->y = 0;
self->width = 100;
self->height = 100;
}

void Shape_destroy(Shape* self)
{
//TODO delete datas
}

void Shape_draw(Shape* self)
{
//TODO draw ....
}

ShapeClass _shape_class = {
Shape_construct,
Shape_destroy,
Shape_draw,
};

Shape *newShape()
{
Shape *shape = (Shape*)malloc(sizeof(Shape);
shape->klass = &_shape_class;
shape->klass->construct(shape);
return shape;
}

void deleteShape(Shape* shape)
{
shape->klass->destroy(shape);
free(shape);
}

当我们调用shape的draw函数时,应该

Shape *shape = newShape();  
....
shape->klass->draw(shape);
....
deleteShape(shape);

上面的原理容易理解,但是,编写起代码来,着实繁琐且易错, 而且,construct, destory这类方法都是对象最基本的方法,因此,我们抽象出一个Object类来

#define ClassType(className)    className##Class  
#define Class(className) g_st##className##Cls

typedef struct _mObjectClass mObjectClass;
typedef struct _mObject mObject;

typedef mObjectClass* (*PClassConstructor)(mObjectClass *);

#define mObjectClassHeader(clss, superCls) \
PClassConstructor classConstructor; \
ClassType(superCls) * super; \
const char* typeName; /* */ \
unsigned int objSize; \
/* class virtual function */ \
void (*construct)(clss *self, DWORD addData); \
void (*destroy)(clss *self); \
DWORD (*hash)(clss *self); \
const char* (*toString)(clss *self, char* str, int max);

struct _mObjectClass {
mObjectClassHeader(mObject, mObject)
};

extern mObjectClass g_stmObjectCls; //Class(mObject);


#define mObjectHeader(clss) \
ClassType(clss) * _class;

struct _mObject {
mObjectHeader(mObject)
};

mObject和mObjectClass是所有类的基础类。

这里,我们使用了一个技巧,及通过定义mObjectClassHeader和mObjectHeader两个宏,让Object的继承类能够“继承”Object的定义。这一点在后文讲述。
mObject的定义很简单的,就定义了一个mObjectClass *_class类(mObjectHeader宏的展开)。

mObjectClass的定义,稍微复杂一些,每个成员描述如下:

classConstructor : 这是类本身的初始化。他的作用是,将类的虚函数表填充完整。之所以用一个函数来填充虚函数表,是为了能够让派生类和基类的类类型都能够得到正确的初始化。

super : 这是超类,是为继承做准备的

typeName: 存储类的名称

objSize: 定义了类本身的大小,这样在malloc的时候,不需要知道具体的类类型,就可以分配足够的空间

construct, destory: 构造和析构

hash: hash函数,用在hash表中

toString:调试时生成描述信息

我们通过extern声明了g_stmObjectCls变量。这个变量是mObjectClass的变量,包含的都是类的虚函数表和最基本的信息。当我们创建类的时候,就需要这个函数了。

下面看看new和delete函数的实现

mObject * newObject(mObjectClass *_class)  
{
mObject * obj;

if(_class == NULL)
return NULL;

obj = (mObject*)calloc(1, _class->objSize);

if(!obj)
return NULL;

obj->_class = _class;

return obj;
}

void deleteObject(mObject *obj)
{
if(obj == NULL || obj->_class)
return;

_c(obj)->destroy(obj);

free(obj);
}
......
static inline mObject * ncsNewObject(mObjectClass *_class,DWORD add_data){
mObject * obj = newObject(_class);
if(!obj)
<span style="white-space:pre"> </span>return NULL;


_class->construct(obj, add_data);
return obj;
}

newObject负责对对象做最基本的初始化: 调用calloc分配空间,然后将_class赋给对象。而ncsNewObject函数,则调用了construct函数,完成对象的初始化。

那么,g_stmObjectCls是如何声明和初始化的?请看代码

static void mObject_construct(mObject* self, DWORD addData)  
{
//do nothing
//to avoid NULL pointer
}

static void mObject_destroy(mObject* self)
{

}

static DWORD mObject_hash(mObject *self)
{
return (DWORD)self;
}

static const char* mObject_toString(mObject *self, char* str, int max)
{
if(!str)
return NULL;

snprintf(str, max, "NCS %s[@%p]", TYPENAME(self),self);
return str;
}

static mObjectClass* mObjectClassConstructor(mObjectClass* _class)
{
_class->super = NULL;
_class->typeName = "mObject";
_class->objSize = sizeof(mObject);

CLASS_METHOD_MAP(mObject, construct)
CLASS_METHOD_MAP(mObject, destroy)
CLASS_METHOD_MAP(mObject, hash)
CLASS_METHOD_MAP(mObject, toString)
return _class;
}

mObjectClass Class(mObject) = {
(PClassConstructor)mObjectClassConstructor
};

CLASS_METHOD_MAP宏的定义是

#define CLASS_METHOD_MAP(clss, name) \  
_class->name = (typeof(_class->name))(clss##_##name);

这里为了方便,要求统一的命名规范。

注意到mObjectClassConstructor,他就是mObjectClass中的classConstructor的实现。看所做的工作:

给出类的名字

给出对象的大小

将虚函数表填充完整

mObject类本身没有任何用处,他只是作为根类存在。我们必须定义其他类,才能起到作用。 那么,如果要实现继承,应该怎么办呢?

还以Shape为例,基本上应该是这样

typedef struct _mShape mShape;  
typedef struct _mShapeClass mShapeClass;
struct _mShape {
mObject base;
int x, y, width, height;
};
struct _mShapeClass {
mObjectClass base;
void (*draw)(mShape* self);
};

mShape和mShapeClass都将mObject和mObjectClass放在最上面,这样,C编译器就会保证mShape和mObject的内存结构,在前半部分都是一致的。因此,当我使用 mObject *obj = (mObject*)shape这样的代码时,不会发生任何意外。通过这个方法,就能实现C++的多态。

但,这里有两个问题:

如果我们想访问父类的方法,就必须通过 shape->base.XXX来访问,如果访问方法,就必须shape->base._class->construct

必须进行强制转换:

如果我们访问父类的虚函数,则必须把子类转换为父类,如 shape->base._class->toString((mObject*)shape);

如果我们要访问自己的虚函数,则必须把父类的虚函数表,转换为自己的,如 ((mShapeClass*)(shape->base._class))->draw(shape);

这不仅仅是写法上繁琐这么简单。当继承层次很多时,既要写一长串的base调用,还必须记住继承的顺序和层次,这基本上是不可能的。

这是,我们需要通过宏,来实现声明的"继承"

#define mShapeHeader(Cls) \  
mObjectHeader(Cls) \
int x, y, width, height;

struct _mShape {
mShapeHeader(mShape)
};

#define mShapeClassHeader(Cls, Super) \
mObjectClassHeader(Cls, Super) \
void (*draw)(Cls* self)

struct mShapeClass {
mShapeClassHeader(mShape, mObject)
};

<ClassName>Header和<ClassName>ClassHeader宏很好的解决了这个问题。mObject的所有声明都将在mShape和mShapeClass中在声明一遍,而且,Class的名字,也从mObject替换为了mShape了。这样一来,当我们使用mShape类型的变量时,所有的虚函数都可以被直接调用,不需要任何的转换。

mShape和mObject之间,仍旧保持了那种内存上的一致性。

当mShape作为基类时,他的派生类可以使用mShapeHeader和mShapeClassHeader来生成新的类。

下面,我们讨论下,mShapeClass的初始化问题。

虚函数表虽然定义了结构,却没有定义变量,需要定义:

extern mShapeClass g_stmShapeCls; 

然后,在再shape.c中,声明和填充g_stmShapeCls。

g_stmShapeCls的实现和g_stmShapeCls是一样的,也需要定义一个classConstructor函数,然后在这个函数中初始化类的名字、mShape的大小以及draw函数指针的初始化。但是,这样写非常繁琐,因此,我们通过一个宏来定义

 #define BEGIN_MINI_CLASS(clss, superCls) \  
1 static ClassType(clss) * clss##ClassConstructor(ClassType(clss)* _class); \
2 ClassType(clss) Class(clss) = { (PClassConstructor)clss##ClassConstructor }; \
3 static const char* clss##_type_name = #clss; \
4 static ClassType(clss) * clss##ClassConstructor(ClassType(clss)* _class) { \
5 _class = (ClassType(clss)*)((PClassConstructor)(Class(superCls).classConstructor))((mObjectClass*)_class); \
6 _class->super = &Class(superCls); \
7 _class->typeName = clss##_type_name; \
8 _class->objSize = sizeof(clss);

#define END_MINI_CLASS return _class; }

#define CLASS_METHOD_MAP(clss, name) \
_class->name = (typeof(_class->name))(clss##_##name);

我们把ClassConstructor函数的声明拆成了3部分:初始化定义、结束定义和方法填充。重点解释的是初始化定义:

BEGIN_MINI_CLASS :

行1: 前置声明ClassConstructor函数,使用类名以区分不同类的classConstructor函数

行2: 声明了g_stmShapeCls变量,并将ClassConstructor赋值给它。这是非常重要的,如果没有这一步骤,那么,虚函数表就无法被初始化;

行3:声明一个类的名字的字符串数组

行4:定义了ClassConstructor函数的实现部分

行5:首先调用超类的ClassConstructor,让超类先初始化一遍,这样如果子类不覆盖超类的函数,那么,我们将继续使用超类的函数,这是多态的“继承”特性

行6:设置超类指针

行7:设置类名

行8:得到成员变量的大小

使用的时候,非常简单

BEGIN_MINI_CLASS(mShape, mObject)  
CLASS_METHOD_MAP(mShape, draw)
END_MINI_CLASS

这样做不仅避免了大量字符输入,更重要的是:1)避免错误;2)避免开发者学习和记住这些通用性很强的内容。

当然,这种情况下,类还是不能直接使用的,要使用,必须调用一次g_stmShapeCls.classConstructor类,真正完成类的初始化。为了简便,提供一个宏来简化这个过程:

#define MGNCS_WIDGET_REGISTER(className) \  
Class(className).classConstructor((mObjectClass*)(void*)(&(Class(className))))

在初始化时

void init()  
{
...
MGNCS_WIDGET_REGISTER(mShape);
...
}

用C模拟类,还能够得到C++的RTTI的一些效果,例如,模拟java的instanceof关键字

BOOL ncsInstanceOf(mObject *object, mObjectClass* clss)  
{
mObjectClass* objClss;
if(object == NULL || clss == NULL)
return FALSE;

objClss = _c(object);

while(objClss && clss != objClss){
objClss = objClss->super;
}

return objClss != NULL;
}
#define INSTANCEOF(obj, clss) ncsInstanceOf((mObject*)(obj), (mObjectClass*)(void*)(&Class(clss)))

我们可以直接去判断,如 INSTANCEOF(rectange, mShape)。这个消耗是很少的,因为,继承层次超过5层的已经非常少了,基本上,继承层次在5层以内就能做出足够的抽象。

   
次浏览       
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]
Visual C++编程命名规则
任何时候都适用的20个C++技巧
C语言进阶
串口驱动分析
轻轻松松从C一路走到C++
C++编程思想
更多...   


C++并发处理+单元测试
C++程序开发
C++高级编程
C/C++开发
C++设计模式
C/C++单元测试


北京 嵌入式C高质量编程
中国航空 嵌入式C高质量编程
华为 C++高级编程
北京 C++高级编程
丹佛斯 C++高级编程
北大方正 C语言单元测试
罗克韦尔 C++单元测试
更多...