#C_OOP
说明
本项目是用 C 实现面向对象的编程范式。
支持面向对象的三大特性:继承、多态、封装。
目前只能支持单继承,不支持接口。
背景
由于码主转行从事智能家居的开发,许多嵌入式平台是不支持C++的。码主从事过5年的C++软件设计,深受面向对象的设计思想陶冶。如何用C写出面向对象的代码引起了码主的深思。
码主在网上找寻了许多用C语言来实现多态、继承的方法。码主觉得都不够深入到骨髓,于是码主亲自尝试用C实现OOP的开发。
目录介绍
oop_base OOP公共核心的代码
classes 定义的示例类 Person, Student, Worker, Engineer,每个类都有3个文件,xxxx.h, xxxx_imp.c, xxxx_imp.h
test.c 测试DEMO
示例类继承关系
+--------+
| Person |
+--------+
A A
| |
+-----+ +-----+
| |
+---------+ +--------+
| Student | | Worker |
+---------+ +--------+
A
|
+----------+
| Engineer |
+----------+
特性
封装
由于面象对象是将数据与方法封装到一个类里。使用者无需关心类是怎么实现的。在 C_OOP 中贯彻了这一思想。
如示例中的 Person 类,见 classes/person.h:
ObjectPtr Person_New(const char* name, int age); //! new一个Person对象,并初始化姓名与年龄
void Person_Delete(ObjectPtr person); //! delete Person对象
void Person_WorkForYear(ObjectPtr person); //! Person的工作方法
void Person_Say(ObjectPtr person, const char *msg); //! Person的说方法
int Person_GetAge(ObjectPtr person); //! Person的获取年龄方法
const char* Person_GetName(ObjectPtr person); //! Person的获取姓名方法
person.h 申明了该类对象的访问接口。使用者压根就不知道Person里面是什么,怎么实现的。只要会用就好了。这样可以尽可能地减少使用者的负担。使用也非常简单:
ObjectPtr p = Person_New("Li Lei", 16);
Person_Say(p, "Hi");
Person_Delete(p);
这种封装的程度比C++的类定义还要彻底。
具体类的定义是在 persion_imp.h 文件中。这个文件不会对使用者公开的。
继承
在常见用C语言实现继承的机制中,多半是用结构体组合实现的。如下:
struct Base {
int a;
int b;
};
struct Derive {
struct Base b;
double c;
};
实际上,实现C++的对象模型也是这样的。
在C_OPP 里,码主也是这样实现的。只是考虑到多态的特性,C_OOP的对象模型是这样的:
Base对象
+-----------+----+ +--+--+--+--+
| tag |vptr| -----> | | | | |
+------+----+----+ +--+--+--+--+
| | int a |
| Base +---------+
| | int b |
+------+---------+
Derive对象
+-----------+----+ +--+--+--+--+
| tag |vptr| -----> | | | | |
+------+----+----+ +--+--+--+--+
| | int a |
| Base +---------+
| | int b |
+------+---------+
|Derive| double c|
+------+---------+
对象的最开都都有一个存放对象信息的结构体 class_info_t 用于保存类标识、虚函数表。
见 oop_base/oop_common.h 中 class_info_t 定义。
typedef struct {
uint32_t tag; //! 高16位为TAG,低16位为class_id
void* vfunc; //! 虚函数结构体
} class_info_t;
上面提到tag字段的高16位用在标识这是不是一个合法的Object对象,后16位用于存储类的id,即表示这倒底是什么类。
如Student的ClassID为2(见oop_typeid.h中定义)。该标识在 class_inhert_map 数组中形成了类的继承关系表。例如Person的类id为1,Student的类id为2。那么class_inhert_map中的值如下:
int class_inhert_map[] = {-1,0,1}
应需要知道Student继承什么类时,只要 class_inhert_map[ClassID_Student] 即可得到其父类id。
在多态函数执行的时候会经常用到上述手法来鉴定某对象是否为指定类的派生对象。
在 C_OOP 的示例中 Student 继承于 Person。
+----------+
| Person |
+----------+
A
|
+----------+
| Student |
+----------+
见 classes 目录下的 person_imp.h 与 student_imp.h 文件中,
//classes/person_imp.h
typedef struct {
char _name[NAME_SIZE];
int _age;
} Person_Data;
----------------------------
//classes/student_imp.h
typedef struct {
Person_Data s;
int _grade;
} Student_Data;
如此,派生类可以转换成基类使用。
多态
C_OOP中的一个核心就是多态。
如何实现多态?采用C++的机制:vptr虚函数表
前面提到每个对象中都有 class_info_t 域,该域又有vptr字段。这个字段就是用于存放对象的虚函数表指针的。
每个类的都有一个特定的虚函数表,其表地址都存放在该类的所有对象的 class_info_t::vptr 域。
执行某个对象的虚函数,莫非就是去从该对象的vptr表中找到对应的函数指针,执行它,并将对象指针作为参数带入到函数中执行。
如Student是继承于Person的。Person有 Person_WorkForYear() 方法。
ObjectPtr s = Student_New("Sid Lee", 18);
Person_WorkForYear(s);
其实现为:
void Person_WorkForYear(ObjectPtr obj) {
OOP_ASSERT(is_instance_of(obj, ClassID_Person));
class_info_t *info = (class_info_t*)obj;
Person_Func *func = info->vfunc;
OOP_ASSERT(func->_WorkForYear != NULL);
func->_WorkForYear(obj);
}
读者看不到上面的代码。是因为码主为了避免大量这样重复的代码,在 oop_base/oop_defines.h 中定义了宏: VIRTUAL_FUNC_DEFINE(class_name, func_name) 来代替。
而对象的 class_info_t::vfunc 是在 Student_New() 时就赋值了。
ObjectPtr Student_New(TYPE_PARAM_LIST) {
ObjectPtr obj = OOP_Alloc(sizeof(Student));
if (obj != NULL) {
obj->tag = MAKE_CLASS_TAG(ClassID_Student);
obj->vfunc = &_class_vfunc; //! <<<<< 这里
Student_Construct(obj PARAM_LIST);
}
return obj;
}
同上,读者是找不到该函的定义的,该函数同样用宏替代了。见 oop_base/oop_defines.h 中 BASE_FUNC_DEFINE 宏定义。
如果读者还有兴趣深究下去,可以看看 _class_vfunc 的定义与初始化过程,各类中的 _VFuncInit() 函数
如何创建一个新的类
改天再写...
等不了的小伙伴可以参考demo中 Person, Student, Worker, Engineer 类的实现,依葫芦画瓢便是。
总结
码主承认,创建一个类好繁琐。这个OOP的实现着实有点令人烦心,什么都得自己操心。C++就是自动替我们处理了这些事情。
码主对比试验过。同样的功能,用OOP来实现编译所生成的文件大小并不比C++小,而且代码量是C++的好几倍。
码主的意见是,如果能用C++,那就别折腾这个了,直接上C++多好?如果上不了,那可以试试这个吧。
问题反馈
Email: hevake_lcj@126.com
微信:hevake_lcj
QQ:527020730
欢迎大家试玩,并提出改进建议 :-)