孟繁超,叶会英
(郑州大学 信息工程学院,河南 郑州 450001)
随着面向对象的编程思想(object oriented programming)的推广与普及,C 语言在数据封装上的缺点也日益凸显。为了解决这一问题,Bjarne Stroustrup于20世纪80年代发明了C++语言。C++语言在不断发展和改进的同时,由于引入了过多的特性,其依旧存在语法过于冗杂,标准不统一等问题,这也使得其在一些硬件受限设备中以及系统级编程中并不适用。
虽然C语言本身对面向对象的特性支持有限,但这并不影响面向对象的思想在C 语言中的应用。实际中,可借助代码生成工具或代码扩展工具配合一定的代码规范或设计模式进行模拟,从而提高C 程序的可重用性。在桌面Linux系统中广泛使用的GNOME 界面系统是C 语言应用面向对象思想的一个代表,其底层的GObject库提供了一套完善的面向对象的类型系统。而早在早在C++创立之初,Bjarne Stroustrup也曾尝试过用类似的方法对C语言进行改造[1-2]。
在C89标准[3]中增加了泛型指针以及C99标准[4]中增加可变宏参数、结构体初始化等特性的支持之后,C 语言的元编程能力得以进一步的加强。合理的利用现代编译器的这些特性,配合专门设计的对象模型,可以将部分C++在编译时完成的动作让C 语言在运行时来进行,进而可以在保证易用性的同时为C 语言增加对封装、继承和多态等特性的支持。实际中,利用托管于Google Code的OOCGCC项目[5]中的代码,可以完成对面向对象思想的模拟,且能够很好的适用于AVR,STM32等一些嵌入式平台。
无论何种编程语言,只要其具备面向对象的特性,都需要专门为其设计特定的对象模型。为了方便说明,这里以一个C++类为例引出几种常规的对象模型的设计。
在C++中,类的成员有两种内存属性,静态的(static)和非静态的(non-static)。而类的方法则有3种内存属性,静态的(static),非静态的(non-static)以及虚拟的(virtual)。
现假定有一用C++定义的类如图1所示。
图1 C++类示例
而图1例子中涵盖了这些不同属性的方法和成员,其内存分布[6]情况如图2所示。
图2 C++内存模型
C++的对象模型中有相当一部分内存空间是在编译时独立分配的,同时如果某个类有以virtual关键字声明的方法(图1中为虚析构方法)则该模型会具备独立的虚表结构,且在虚表结构中还拥有一个提供RTTI(RunTime type information)特性的类型信息区域。
借助于编译器,C++可以保证使用时代码的简洁性,但如果要使用C语言直接去实现这样的对象模型,因为要将编译时的工作转移至运行时来做,必然会引入大量额外的附加代码。为此,必须对该模型进行改良,以适应C 语言的某些特性。
用C语言去模拟面向对象的特性有很多方式,引言中提及的GObject库虽然模拟了很多面向对象的特性,但这种模拟因为引入了过多的特性而在实际使用时显得非常繁琐[7-8]。为了在功能和易用性之间寻求一种平衡,OOCGCC项目中设计并实现了一种新的对象模型,利用现代C编译器的特性同时结合元编程技巧,在模拟了面向对象的基本特性的同时,保证了易用性,简化了开发的复杂度。对应于图1 中的例子,这种新的模型的内存分布如图3所示。
和图2中C++对象模型一样的是,将虚表结构和实例结构分离,这样可以有效的区分成员和方法,有助于内存的节约。和C++模型的主要不同是将cnt成员放在虚表结构中并在虚表结构中保留了指向方法的指针。同时,在虚表结构中增加了一个实例计数用的成员。而对于类的方法,因为C语言无法直接去调用,故在虚表结构中保留了指向其真实函数地址的函数指针。
图3 OOC-GCC的内存模型
因为不能借助于编译器,OOC-GCC项目并没有实现完整的类型系统,故省略了C++模型中和类型信息相关的结构,但并不影响对面向对象中封装,继承,多态等特性的模拟。如果需要对类型系统的模拟,也可配合GObject中的类型系统使用。
现假设一个类B继承了图1中所表述的类A,那么按照本文中提出的内存模型,其具体内存分配如图4所示。
图4 OOC-GCC单根继承对象模型
图4中看似复杂的内存布局,可配合表1中所定义的一些宏一起使用,无需经过任何第三方工具即可实现类的设计与使用。
表1 OOC-GCC宏的定义
B的实例部分继承了A的实例部分的内容,而B的虚表结构继承了A的虚表结构的内容,但虚表结构头部的析构指针和实例计数器部分保持不变。在B的实例中保留有两个虚表指针,分别指向A的虚表结构以及B的虚表结构。这意味着,即使在没有分配A的实例的前提下,直接声明一个B的实例,会同时为之分配A和B的两个虚表结构,而B的虚表结构因为继承的原因,也包含了A的虚表结构的内容。
在设计一个类时,CLASS宏和STATIC 宏分别用来定义一个类的实例结构和虚表结构,而与之相应的CLASS_EX 宏以及STATIC_EX 宏则为实例结构和虚表结构的定义增加了对单根继承特性的支持。ASM和ASM_EX 则是用来将类的定义与实现的类的构造函数与析构函数进行绑定。
在对一个类进行操作时,表1中定义的NEW 宏和DELETE宏和C++中的new 与delete关键字十分类似,它们都会在堆内存上开辟或释放一块区域并进行对象的构造与析构。NEW0 宏和DELETE0 宏则对应NEW 宏和DELETE宏不接收任何参数的情形。ST 宏则用来通过实例结构获取指向虚表结构的指针。*Fn则是对一定形式的函数指针的类型重定义,如iFn表示返回值为int型的函数指针,而vFn表示无返回值(返回值为void型)的函数指针。
使用表1中的宏来描述图4中定义的类A和类B的代码如图5中所示。
OOC-GCC中的模块如图6所示。
OOC-GCC项目由多个模块构成,这些模块被定义在与模块名相对应的.h或.c文件中。其中,最为核心的部分定义在OOCore模块中。OOBase模块则对OOCore模块进行了扩展,使之支持虚表结构以及单根继承等特性。对于使用者而言,只需关注两个模块,一个是OOStd模块,该模块用于对底层较复杂的内容进行重定义,简化实际使用时的复杂度;另一个是OOCfg模块,该模块用于对整体进行配置。通过OOCfg模块中的宏开关,可以选择性的使用调试用的OODbg模块,也可以开启对匿名结构体等特性的支持。
为了使表1中和类的设计相关的宏能和图3中以及图4中所表述的对象模型相适应,当使用CLASS,STATIC,CLASS_EX,STATIC_EX 宏定义一个类的时候,除了类本身的成员外,还会隐式的引入一些和构造与析构相关的成员。
每当使用CLASS宏时,都会在一个结构体的头部放置一个名为OOC_STRUCT__classRootHead的结构体。相应的每当使用STATIC宏时,头部的结构体名为OOC_STRUCT__staticRootHead。具体的定义如图7所示。
图7 头部结构设计
每一个用CLASS定义的结构体的头部都有一个指向真实虚表结构的指针。每一个STATIC 宏的头部都有一个指向真实析构函数的函数指针,以及一个用于实例计数的成员变量。通过DELETE宏进行析构时,即调用图7中__ooc_classDelete指针指向的函数。而用于计数的变量保证了使用者无需关注虚表结构的构造与析构,当一个类的第一实例构造之前,该类的虚表结构已经自动构造完成了;而当一个类的最后一个实例销毁后,该类的虚表结构的析构函数也会被自动调用。
从图5可以看出,STATIC宏应在CLASS宏后的大括号内使用。其不仅引入了虚表结构的头部,还引入了实例结构的尾部结构。继承关系每加深一级,会在实例结构的尾部多分配一个双重泛型指针,用于指向当前类型的虚表结构。而实例结构头部中虚表指针指向的是最后一级类型的虚表结构。之所以这里采用双重泛型指针,是为了保证在构造与析构时可以直接通过实例的构造或析构函数去修改位于虚表结构中的用于实例计数的成员,同时这样的设计可以保证宏的灵活性,不会引入额外的全局变量。
和CLASS 宏以及STATIC 宏不同,支持继承的宏CLASS_EX 以及STATIC_EX 并不会再次引入实例结构与虚表结构的头部。
假定要顶一个名为A的类,在使用CLASS和STATIC来定义的时候,在底层定义了名为A和StA的结构体,以及用于分配内存的newA 函数以及newStA 函数,用于释放内存的delA 函数以及delStA 函数,用于构造的iniA 函数以及iniStA 函数,用于析构的finA 函数以及finStA 函数。在生成与销毁一个实例时,和虚表结构相关的newStA 函数,delStA 函数,iniStA 函数以及finStA 函数会被自动的调用。而ASM 宏则用来将用户自己定义的A_reload 函数,A_unload 函数,A_reloadSt函数以及A_unloadSt函数与上述的几个函数进行绑定。
以图5中实例B的构造与析构为例,先使用NEW0宏在堆内存声明一个类B的实例,最后又用DELETE0宏将其销毁并释放,期间并没有其它实例的分配与销毁。在B的实例声明和销毁时,其会按图8中所示的顺序调用一些构造和析构相关的函数。
构造时,第1,2步完成B的虚表结构的构造,第3步完成A的虚表结构的构造,第4,5步完成B的实例结构的构造。而析构时,第1,2步完成B的实例结构的析构,第3步完成A的虚表结构的析构,第4,5步完成B的虚表结构的析构。
图8 构造与析构的顺序
可以看出,构造和析构的顺序是完全对称的,并且子类的构造和析构会自动调用父类的构造和析构,同时,虚表结构的分配与销毁也是自动的。而这些都是由ASM 宏和ASM_EX 宏保证的。
当定义的类或声明的实例更加复杂时,为了减少记忆的负担,其也应具备对称性。ASM 宏和ASM_EX 宏保证了其构造与析构过程遵循如下算法。
构造时,其遵循图9 中的算法。而析构时,其遵循图10中的算法。
为了方便调试,OOC-GCC项目中增加了方便调试程序调试层,可配合GCC(GNU compiler collection)一起使用。在将调试层一同编译时,其会替换NEW 宏中封装的malloc函数以及DELETE宏中封装的free函数,进而可以对内存的分配和释放以记录并输出。如果输出至文件,还配合Graphviz等工具方便的生成函数调用图表。除此之外,OOC-GCC的调试层还对C++的异常捕获机制进行了简单的模拟并提供了运行时间监测等实用的功能。
OOC-GCC中关于类的设计和实现的宏在设计上严格的对称,并没有引入任何一个全局变量,符合C 语言模块化的设计原则。也因此,其对调试层没有任何依赖。如果某些编译器不支持调试层提供的一些功能或是需要发布程序时,只需移除OODbg模块,使其不参与编译即可。
为了方便和C++进行比较,须设计一个场景。
假定现有一个抽象的名为Animal的父类,其包含一个名为name的成员和一个名为talk的方法。另有两个名为Cat和Dog的子类继承了Animal这一父类,并各自重写了父类中定义的talk方法(或重写了父类中已经实现的talk方法)。
在实际测试的例子中,通过子类来分配内存,使用父类的形式来调用接口来调用以体现面向对象思想中的多态的特性。一开始,直接在堆内存上分配4000个子类的实例(Cat类和Dog类随机),然后分别调用其talk方法,最后再依次将其析构。
实际测试时的C代码以及C++代码分别如图11和图12所示。
通过对图11和图12的对比可知,实际的代码长度十分相近。仅仅是需要额外的声明一个名为StAnimal的实例,并通过ST 宏获取相应的虚表结构。
在ubuntu和windows xp系统上测试的关于生成程序体积的数据如表2所示(其中数据的单位为字节)。
表2 生成体积对比
可以看出,从生成代码体积方面来说,其和使用C++生成的代码会有些差异,但这些差异并不显著,并且在开启优化选项后,生成的体积有可能比原生的C++占用更小的空间。
表1中声明的宏被设计成跨平台的,不但支持开源的GCC编译器,也兼容微软的MSVC编译器。对于容量受限或是开发工具受限的嵌入式平台,即使无法很好的支持C++等面向对象的编程语言,也可使用该模型可以大幅提升程序的可读性和可维护性。实际测试中,AVR,STM32,MSP430 等芯片所使用的编译器都可以很好的支持。
另外,利用OOC-GCC 项目中设计的对象模型,还可较为方便的将现有的使用面向对象语言(如C++,Java,Python等)编写的代码移植至纯C 语言的平台,文献[9]中给出了一个用Java语言实现的简易解释器的例子,并且该解释器的实现符合设计模式中解释器模式[10]的规范。
假定要实现一个具备如下功能的解释器:可以根据指定的格式向某终端输出指定格式的字符串,并支持循环结构。
图13 解释器的示例与运行结果
图13中左侧为该解释器的示例代码,右侧为对应的正确输出。该例子中共有7个关键字。其中PROGRAM 表示程序的开始,与之对应的END 既可用于程序的终结,也可用于循环结构的终结,而循环结构用REPEAT 关键字来描述,其后紧跟一个数字,用来指示具体循环的次数。PRINT 用于输出指定的字符串,但由于涉及字符的转义,这里使用SPACE关键字来输出空格,并使用BREAK 关键字来输出换行,而LINEBREAK 关键字则在输出一行直线的同时额外的输出一个换行。
将上述描述,可得出该解释器定义的BNF范式,具体如图14所示。
图14 解释器的BNF
在以解释器模式进行设计时,BNF范式中的每一个节点都对应一个类,而每个类都应实现一个用于解析代码并生成需要的AST 树的接口以及一个对已生成的AST 树进行遍历并执行接口。如图14 所示,该范式由program,command list等5个关键节点构成,按照解释器模式的规定,需为这5个节点分别设计具备用来解析字符串和用来执行抽象语法树中定义规则的方法的类。
图15 Repeat节点C实现
图15中展示了使用OOC-GCC配合C 语言以实现用于循环控制的Repeat节点的方式,可以看到,配合OOCGCC,C语言在形式上已经有明显的面向对象的色彩。因为该例子较为复杂,完整的实现可从文献该例子较为复杂,完整的例子可参见文献[5]中获得。另外,在文献[9]中给出了该解释器分别以JAVA 语言以及Python语言实现的方法。通过对比可以看出,使用Java语言或Python等原生支持面向对象思想的编程语言所编写的代码在逻辑上可以和使用C语言编写的具备OO 风格的代码一一对应,但因为缺乏诸如GC(garbage collector)等特性,在代码量上,使用C语言编写的程序仍比使用Java等语言编写的程序要长一些。
利用现代C编译器的特性以及元编程技巧,配合适用于C语言的对象模型,可以很好的利用面向对象的思想,提升C程序的可重用性。OOC-GCC中的设计,通过宏的方式简化因引入面向对象思想而产生的附加代码,无需借助任何第三方代码生成工具,很好的将OOP 与C 语言像结合。同时,其灵活的设计,维持了和C++相似的编码风格,符合C语言模块化[11]的编码规范,不依赖于其它第三方库,在实际中可以很好的解决大规模C 代码难以复用,难以管理的问题。
相较于著名的GObject库,这是一种轻量级的实现,并没有提供复杂的对象系统,但更加灵活易用,且方便移植。如果需要对象系统的支持,也可配合GObject,Lua,Python[12]以及Ruby中的对象系统一起使用。
为了方便测试以及推广文中涉及的代码。OOC-GCC项目遵循LGPL 3.0开源协议托管于Google Code,文献[5]中给出了具体的链接,可供读者测试,验证和使用。
[1]Bjarne Stroustrup.Evolving a language in and for the real world:C++1991-2006[C]//HOPL III,Proceedings of the Third ACM SIGPLAN Conference on History of Programming Languages,2007:3-5.
[2]Bjarne Stroustrup.The Design and evolution of C++[M].QIU Zongyan,transl.China Machine Press,2002:7-37(in Chinese).[Bjarne Stroustrup.C++语言的设计和演化[M].裘宗燕,译.机械工业出版社,2002:3-37.]
[3]ANSI C89Standard,ANSI X3.159-1989[S].
[4]ISO C99Standard,ISO/IEC 9899:1990[S].
[5]MENG Fanchao.OOC-GCC project:An easy to use template to write OO program with pure C programming language[CP/OL].http://code.google.com/p/ooc-gcc/,2012(in Chinese).[孟繁超.OOC-GCC[CP/OL].http://code.google.com/p/oocgcc/,2012.]
[6]Stanley B Lippman.Inside The C++object model[M].HOU Jie,transl.Publishing House of Electronics Industry,2012:1-13(in Chinese).[Stanley B Lippman.深度探索C++对象模型[M].侯捷,译.电子工业出版社,2012:1-13.]
[7]Andrew Krause.Foundations of GTK+development[M].Apress?,2007:406-456.
[8]SONG Guowei.GTK +2.0 programming paradigm[M].Tsinghua University Press,2002:174-178(in Chinese).[宋国伟:GTK+2.0 编程范例[M].清华大学出版社,2002:174-178]
[9]LIN Xinliang.Notes about design interpreter pattern[J/OL].http://caterpillar.onlyfun.net/Gossip/DesignPattern/InterpreterPattern.htm,2010(in Chinese).[林信良.关于解释器模式的笔记[J/OL].http://caterpillar.onlyfun.net/Gossip/DesignPattern/InterpreterPattern.htm,2010.]
[10]Erich Gamma,Richard Helm,Ralph Johnson,et al.Design patterns:Elements of reusable object-oriented software[M].Addison-Wesley Professional,1995:274-288
[11]David R Hanson.C interfaces and implementations:Techniques for creating reusable software[M].GUO Xu,transl.Posts &Telecom Press,2011:1-21(in Chinese).[David R Hanson.C语言接口与实现:创建可重用软件的技术[M].郭旭,译.人民邮电出版社,2011:1-21]
[12]CHEN Ru.Inside python source code:dig into the core of the dynamic languages[M].Publishing House of Electronics Industry,2008:15-28(in Chinese).[陈儒.Python 源码剖析:深度探索动态语言核心技术[M].电子工业出版社,2008:15-28.]