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

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

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

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

  1. //模拟虚函数表
  2. typedef struct _Shape Shape;
  3. struct ShapeClass {
  4. void (*construct)(Shape* self);
  5. void (*destroy)(Shape *self);
  6. void (*draw)(Shape *self);
  7. };
  8. struct _Shape {
  9. ShapeClass *klass;  //定义class的指针
  10. int x, y, width, height;
  11. };

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

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

  1. 实现一个ShapeClass类
  2. 初始化Shape为正确的类

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

  1. void Shape_construct(Shape* self) {
  2. self->x = 0;
  3. self->y  = 0;
  4. self->width = 100;
  5. self->height = 100;
  6. }
  7. void Shape_destroy(Shape* self)
  8. {
  9. //TODO delete datas
  10. }
  11. void Shape_draw(Shape* self)
  12. {
  13. //TODO draw ....
  14. }
  15. ShapeClass _shape_class = {
  16. Shape_construct,
  17. Shape_destroy,
  18. Shape_draw,
  19. };
  20. Shape *newShape()
  21. {
  22. Shape *shape = (Shape*)malloc(sizeof(Shape);
  23. shape->klass = &_shape_class;
  24. shape->klass->construct(shape);
  25. return shape;
  26. }
  27. void deleteShape(Shape* shape)
  28. {
  29. shape->klass->destroy(shape);
  30. free(shape);
  31. }

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

  1. Shape *shape = newShape();
  2. ....
  3. shape->klass->draw(shape);
  4. ....
  5. deleteShape(shape);

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

  1. #define ClassType(className)    className##Class
  2. #define Class(className)        g_st##className##Cls
  3. typedef struct _mObjectClass mObjectClass;
  4. typedef struct _mObject mObject;
  5. typedef mObjectClass* (*PClassConstructor)(mObjectClass *);
  6. #define mObjectClassHeader(clss, superCls) \
  7. PClassConstructor classConstructor; \
  8. ClassType(superCls) * super; \
  9. const char* typeName; /* */ \
  10. unsigned int objSize; \
  11. /* class virtual function */ \
  12. void (*construct)(clss *self, DWORD addData); \
  13. void (*destroy)(clss *self); \
  14. DWORD (*hash)(clss *self); \
  15. const char* (*toString)(clss *self, char* str, int max);
  16. struct _mObjectClass {
  17. mObjectClassHeader(mObject, mObject)
  18. };
  19. extern mObjectClass g_stmObjectCls; //Class(mObject);
  20. #define mObjectHeader(clss) \
  21. ClassType(clss) * _class;
  22. struct _mObject {
  23. mObjectHeader(mObject)
  24. };

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函数的实现

  1. mObject * newObject(mObjectClass *_class)
  2. {
  3. mObject * obj;
  4. if(_class == NULL)
  5. return NULL;
  6. obj = (mObject*)calloc(1, _class->objSize);
  7. if(!obj)
  8. return NULL;
  9. obj->_class = _class;
  10. return obj;
  11. }
  12. void deleteObject(mObject *obj)
  13. {
  14. if(obj == NULL || obj->_class)
  15. return;
  16. _c(obj)->destroy(obj);
  17. free(obj);
  18. }
  19. ......
  20. static inline mObject * ncsNewObject(mObjectClass *_class,DWORD add_data){
  21. mObject * obj = newObject(_class);
  22. if(!obj)
  23. <span style="white-space:pre">      </span>return NULL;
  24. _class->construct(obj, add_data);
  25. return obj;
  26. }

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

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

  1. static void mObject_construct(mObject* self, DWORD addData)
  2. {
  3. //do nothing
  4. //to avoid NULL pointer
  5. }
  6. static void mObject_destroy(mObject* self)
  7. {
  8. }
  9. static DWORD mObject_hash(mObject *self)
  10. {
  11. return (DWORD)self;
  12. }
  13. static const char* mObject_toString(mObject *self, char* str, int max)
  14. {
  15. if(!str)
  16. return NULL;
  17. snprintf(str, max, "NCS %s[@%p]", TYPENAME(self),self);
  18. return str;
  19. }
  20. static mObjectClass* mObjectClassConstructor(mObjectClass* _class)
  21. {
  22. _class->super = NULL;
  23. _class->typeName = "mObject";
  24. _class->objSize = sizeof(mObject);
  25. CLASS_METHOD_MAP(mObject, construct)
  26. CLASS_METHOD_MAP(mObject, destroy)
  27. CLASS_METHOD_MAP(mObject, hash)
  28. CLASS_METHOD_MAP(mObject, toString)
  29. return _class;
  30. }
  31. mObjectClass Class(mObject) = {
  32. (PClassConstructor)mObjectClassConstructor
  33. };

CLASS_METHOD_MAP宏的定义是

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

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

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

  • 给出类的名字
  • 给出对象的大小
  • 将虚函数表填充完整

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

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

  1. typedef struct _mShape mShape;
  2. typedef struct _mShapeClass mShapeClass;
  3. struct _mShape {
  4. mObject base;
  5. int x, y, width, height;
  6. };
  7. struct _mShapeClass {
  8. mObjectClass base;
  9. void (*draw)(mShape* self);
  10. };

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调用,还必须记住继承的顺序和层次,这基本上是不可能的。

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

  1. #define mShapeHeader(Cls) \
  2. mObjectHeader(Cls) \
  3. int x, y, width, height;
  4. struct  _mShape {
  5. mShapeHeader(mShape)
  6. };
  7. #define mShapeClassHeader(Cls, Super) \
  8. mObjectClassHeader(Cls, Super) \
  9. void (*draw)(Cls* self)
  10. struct mShapeClass {
  11. mShapeClassHeader(mShape, mObject)
  12. };

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

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

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

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

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

  1. extern mShapeClass g_stmShapeCls;

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

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

  1. #define BEGIN_MINI_CLASS(clss, superCls) \
  2. 1 static ClassType(clss) * clss##ClassConstructor(ClassType(clss)* _class); \
  3. 2 ClassType(clss) Class(clss) = { (PClassConstructor)clss##ClassConstructor }; \
  4. 3 static const char* clss##_type_name = #clss; \
  5. 4 static ClassType(clss) * clss##ClassConstructor(ClassType(clss)* _class) { \
  6. 5   _class = (ClassType(clss)*)((PClassConstructor)(Class(superCls).classConstructor))((mObjectClass*)_class); \
  7. 6   _class->super = &Class(superCls); \
  8. 7   _class->typeName = clss##_type_name; \
  9. 8   _class->objSize = sizeof(clss);
  10. #define END_MINI_CLASS return _class; }
  11. #define CLASS_METHOD_MAP(clss, name) \
  12. _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:得到成员变量的大小
使用的时候,非常简单
  1. BEGIN_MINI_CLASS(mShape, mObject)
  2. CLASS_METHOD_MAP(mShape, draw)
  3. END_MINI_CLASS

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

 
 
当然,这种情况下,类还是不能直接使用的,要使用,必须调用一次g_stmShapeCls.classConstructor类,真正完成类的初始化。为了简便,提供一个宏来简化这个过程:
  1. #define MGNCS_WIDGET_REGISTER(className) \
  2. Class(className).classConstructor((mObjectClass*)(void*)(&(Class(className))))

在初始化时

  1. void init()
  2. {
  3. ...
  4. MGNCS_WIDGET_REGISTER(mShape);
  5. ...
  6. }
用C模拟类,还能够得到C++的RTTI的一些效果,例如,模拟java的instanceof关键字
  1. BOOL ncsInstanceOf(mObject *object, mObjectClass* clss)
  2. {
  3. mObjectClass* objClss;
  4. if(object == NULL || clss == NULL)
  5. return FALSE;
  6. objClss = _c(object);
  7. while(objClss && clss != objClss){
  8. objClss = objClss->super;
  9. }
  10. return objClss != NULL;
  11. }
  12. #define INSTANCEOF(obj, clss)  ncsInstanceOf((mObject*)(obj), (mObjectClass*)(void*)(&Class(clss)))

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

C的面向对象编程的更多相关文章

  1. angular2系列教程(六)两种pipe:函数式编程与面向对象编程

    今天,我们要讲的是angualr2的pipe这个知识点. 例子

  2. 带你一分钟理解闭包--js面向对象编程

    上一篇<简单粗暴地理解js原型链--js面向对象编程>没想到能攒到这么多赞,实属意外.分享是个好事情,尤其是分享自己的学习感悟.所以网上关于原型链.闭包.作用域等文章多如牛毛,很多文章写得 ...

  3. PHP 面向对象编程和设计模式 (1/5) - 抽象类、对象接口、instanceof 和契约式编程

    PHP高级程序设计 学习笔记 2014.06.09 什么是面向对象编程 面向对象编程(Object Oriented Programming,OOP)是一种计算机编程架构.OOP 的一条基本原则是计算 ...

  4. Delphi_09_Delphi_Object_Pascal_面向对象编程

    今天这里讨论一下Delphi中的面向对象编程,这里不做过多过细的讨论,主要做提纲挈领的描述,帮助自己抓做重点. 本随笔分为两部分: 一.面向对象编程 二.面向对象编程详细描述 ------------ ...

  5. python基础-面向对象编程

    一.三大编程范式 编程范式即编程的方法论,标识一种编程风格 三大编程范式: 1.面向过程编程 2.函数式编程 3.面向对象编程 二.编程进化论 1.编程最开始就是无组织无结构,从简单控制流中按步写指令 ...

  6. 面向对象编程(OOP)

    什么是面向对象编程,对于面向对象编程与面向过程编程的解释随处可见,个人认为对面向对象编程解释最好的一个定义是:依赖倒转原则是面向对象编程的标志,面向对象编程是一种思想,无论使用哪一种编程语言,如果在编 ...

  7. python 学习笔记7 面向对象编程

    一.概述 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发"更快更好更强..." ...

  8. 进击的Python【第七章】:Python的高级应用(四)面向对象编程进阶

    Python的高级应用(三)面向对象编程进阶 本章学习要点: 面向对象高级语法部分 静态方法.类方法.属性方法 类的特殊方法 反射 异常处理 Socket开发基础 一.面向对象高级语法部分 静态方法 ...

  9. 进击的Python【第六章】:Python的高级应用(三)面向对象编程

    Python的高级应用(三)面向对象编程 本章学习要点: 面向对象编程介绍 面向对象与面向过程编程的区别 为什么要用面向对象编程思想 面向对象的相关概念 一.面向对象编程介绍 面向对象程序设计(英语: ...

  10. 第二章 Matlab面向对象编程基础

    DeepLab是一款基于Matlab面向对象编程的深度学习工具箱,所以了解Matlab面向对象编程的特点是必要的.笔者在做Matlab面向对象编程的时候发现无论是互联网上还是书店里卖的各式Matlab ...

随机推荐

  1. 使用Fiddler抓包调试https下的页面

    众所周知https技术诞生以来,一个很重要的作用就是加密通信内容.所以在项目团队将业务站点实施完https改造以后,原先使用fiddler进行抓包的美好生活到头了.其实fiddler本身是支持对于ht ...

  2. ASP.NET MVC:通过FileResult向浏览器发送文件

    在 Controller 中我们可以使用 FileResult 向客户端发送文件. FileResult FileResult 是一个抽象类,继承自 ActionResult.在 System.Web ...

  3. java 在控制台上输入密码时,密码不显示在控制台上

    用下面的方法可以实现在控制台上输入密码时,密码不显示在控制台上:Console cons=System.console(); System.out.print(" 密码:"); c ...

  4. Python pycharm(windows版本)部署spark环境

    一 部署本地spark环境 1.1  安装好JDK       下载并安装好jdk1.7,配置完环境变量.   1.2 Spark环境变量配置       去http://spark.apache.o ...

  5. 腾讯云-搭建 .NET Core 开发环境

    搭建 .NET Core 开发环境 安装 .Net Core 执行代码 任务时间:时间未知 .NET Core 的官方文档很详细,本实验带你建立一个.NET Core 1.1的Web运行环境,更多内容 ...

  6. 微信小程序--搜索关键词高亮

    代码地址如下:http://www.demodashi.com/demo/14249.html 一.前期准备工作 软件环境:微信开发者工具 官方下载地址:https://mp.weixin.qq.co ...

  7. IBM InfoSphere DataStage 8.1 DataStage Job 开发具体解释

    简单介绍 DataStage 使用了 Client-Server 架构,server端存储全部的项目和元数据,client DataStage Designer 为整个 ETL 过程提供了一个图形化的 ...

  8. 获取JSON格式的字符串各个属性对应的值

    {"lastrdtime":1515998187379,"creditbalance":"$5.00","contactmode& ...

  9. HDUOJ------2398Savings Account

    Savings Account Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)T ...

  10. HDUOJ---1102Constructing Roads

    Constructing Roads Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Other ...