*******************************************************************************
C中可移植的继承性和多态性2
Fr: [ESP-9712-code]
By: Miro Samek
Rd: Amine Chen[[email protected]]
*******************************************************************************
快速参考:
=========
本文解释如下这些"设计模式"在C中的实现:
封装性,继承性,多态性.
类继承: 单继承; 接口继承: 多继承.
用C语言进行面向对象编程
*******************************************************************************
C中可移植的继承性和多态性2
Fr: [ESP-9712-code]
By: Miro Samek
Rd: Amine Chen[[email protected]]
*******************************************************************************
快速参考:
=========
本文解释如下这些"设计模式"在C中的实现:
封装性,继承性,多态性.
类继承: 单继承; 接口继承: 多继承.
抽象基类 'Object'
----------------
Object类在类层次的根部.该类封装了虚指针,并定义了虚析构函数.为了效率,将构造和
析构函数定义为'inline'宏.它们都被保护(只能由子类访问),因为该类只想用于继承.客户
不应创建'Object'对象.
该类还声明几个私有方法(某些是类方法,也就是没有'this'指针). 因为这些方法要求
类型转换(只在一定的上下文中合法),所以只通过宏使用它们.
声明类
------
1.使用宏CLASS开始类的声明;
2.宏IMPLEMENTS用于想实现的每个接口;
3.声明类属性(实例变量);
4.用宏VTABLE声明虚函数.宏参数必须于CLASS的一致;
5.宏EXTENDS用于IMPLEMENTS声明的每个接口;
6.声明所有虚方法的函数指针;
7.用宏声明类方法;
8.为类实现的所有public&protected方法声明原型.
9.用宏END_CLASS结束类声明.
#include "object.h"
CLASS(Shape, Object) /* class Shape extends Object */
IMPLEMENTS(Scaleable); /* and implements Scaleable interface */
struct String name; /* public name (object composition) */
VTABLE(Shape, Object) /* Shape's VTABLE extends Object */
EXTENDS(Scaleable); /* and also extends Scaleable */
double (*Area)(Shape); /* virtual method to compute area */
METHODS
/* protected constructor (single '_' in the name)
* means that Shape is an abstract class
*/
Shape Shape_Con(Shape this, char *name);
void Shape_Des(Shape this); /* protected destructor */
END_CLASS
声明接口
--------
接口表示为函数指针的结构(VTABLE).接口可扩展其他接口.
1.使用宏INTERFACE开始接口的声明;
2.宏EXTENDS用于想继承的每个接口(可选);
3.声明该接口引入的所有抽象方法指针.每个方法必须声明第一个参数为Object类型;
4.用END_INTERFACE宏结束声明;
#include "object.h"
INTERFACE(Scaleable)
void (*Scale)(Object, double);
END_INTERFACE
INTERFACE(Fooable)
EXTENDS(Scaleable); /* interface Fooable extends Scaleable */
int (*Foo)(Object, long);
END_INTERFACE
绑定虚函数
----------
虚函数在类描述器(VTABLE)中声明为函数指针.VTABLEs必须为每个类定义和初始化.
分配实现函数给相应的函数指针.
1.使用BEGIN_VTABLE为给定的类定义类描述器constructor;
2.用VMETHOD将抽象类方法绑定到它们的实现;
3.用IMETHOD将抽象接口方法绑定到它们的实现;
4.用END_VTABLE结束虚拟表声明.
Example:
#include "shape.h"
BEGIN_VTABLE(Shape, Object)
VMETHOD(Object, Des) = Shape_Des; /* virtual destructor*/
VMETHOD(Shape, Area) = Object_NoIm; /* purely virtual */
IMETHOD(Scaleable, Scale) = Object_NoIm; /* ditto[同上] */
END_VTABLE
定义构造函数
------------
每个类必须提供至少一个构造函数,负责属性结构的初始化.
构造函数应有如下的行为:
1.调用基类的构造函数;
2.用VHOOK来赋值虚指针;
3.为所有的接口调用IHOOK,你要重载(调用IMETHOD)这些接口在相应VTABLE中的实现;
4.按类中声明的顺序初始化类属性;
Shape Shape_Con(Shape this, char *name) {
Object_Con(&this->super); /* superclass constructor */
VHOOK(this, Shape); /* assign 'Shape' VPTR */
IHOOK(this, Shape, Scaleable); /* assign 'Scaleable' VPTR */
if (!StringCon(&this->name, name)) /* construct member */
return NULL; /* signal failure */
return this; /* signal success */
}
定义析构函数
------------
类可可选地定义destructor的实现.应在类destructor中执行如下的任务:
1.用与类中声明相反的顺序,删除属性;
2.调用父类destructor;
void Shape_Des(Shape this) {
StringDes(&this->name); /* member destructor */
Object_Des(&this->super); /* superclass destructor */
}
调用[Invoking]虚Class方法:
--------------------------
可通过VTABLE的虚指针间接调用虚方法.
VCALL/END_CALL用来帮助完成虚类方法的调用.
#include "shape.h"
void testShape(Shape s) {
assert(IS_RUNTIME_CLASS(s, Shape)); /* use RTTI in assertion */
printf("Shape.name=/"%s/", Shape.Area()=%.2f/n",
StringToChar(&s->name), /* static binding */
VCALL(s, Shape, Area)END_CALL); /* dynamic binding */
}
#include "shape.h"
Circle c;
...
testShape((Shape)c);
...
调用[Invoking]虚Interface方法:
------------------------------
接口需要解析对实现对象的引用(I_TO_OBJ).
ICALL/END_CALL帮助完成虚接口方法的调用.
你可以编写只知道'Scaleable'接口的代码.
通过对Scale()方法的虚函数调用,对象将被调节大小[be scaled].
#include "sclable.h"
void testScaleable(Scaleable s) {
/* is it an Object?*/
assert(IS_RUNTIME_CLASS(I_TO_OBJ(s), Object));
printf("Scaleable.Scale(), ");
ICALL(s, Scale) ,2.0 END_CALL; /* dynamic binding */
}
任何实现'Scaleable'的对象可被用来调用'testScaleable'函数.
Circle c;
...
testScaleable(&c->super.Scaleable);
...
处理堆上的对象:
---------------
1.用ALLOC/DELETE来分配和删除动态对象;
2.用ALLOC_ARR/DELETE_ARR分配和删除动态对象数组;
用ALLOC/ALLOC_ARR必须调用构造函数初始化.
Circ c = ALLOC(Circle);
...
DELETE(c);
Rect r = ALLOC_ARR(Rect, NRECT);
...
DELETE_ARR(r);
使用RTTI (Run-Time Type Information):
-------------------------------------
可使用IS_RUNTIME_CLASS测试对象的类型相容性.不要乱用RTTI,因为它会使多态性无效.
使用虚函数代替.
1.若对象的run-time类是或继承自给定的类,IS_RUNTIME_CLASS返回1;
2.使用I_TO_OBJ从接口引用获得对象引用.
Shape s;
...
assert(IS_RUNTIME_CLASS(s, Shape));
Scaleable s;
...
assert(IS_RUNTIME_CLASS(I_TO_OBJ(s), Object));
*******************************************************************************
C中可移植的继承性和多态性
Fr: [ESP-9712]
By: Miro Samek
Rd: Amine Chen[[email protected]]
*******************************************************************************
虽然面向对象设计几乎与语言无关,但大部分文献都采用[assume]C++,Smalltalk,Java
用于OO实现.本文从更低层次看,认为过程语言(如C)同样可用于OO实现,想应用OO的嵌入开发
人员不必转换到OO语言.
*是否可以用非OO语言(如C)编写OO程序?
*在没有C++编译器的小嵌入系统中,如何实现OO设计?
*如何改善C编码风格,以使代码可更好复用,更模块化[modular],更健壮?
*继承和多态实际是怎样工作的?
*使用C而不用OO语言实现OO设计,必须损失[compromise]多少便利和表现?
为了回答这些问题,本文提出了一个小型,可移植,高效的OO概念的C语言实现.
这些OO概念如下(本文解释如下这些"设计模式"在C中的实现):
*封装性--包装数据和函数为类,以及信息隐藏和模块化的技巧;
*继承性--基于已有类定义新类和行为[behavior]的能力,以获得代码重用&组织;
*多态性--同一消息送到不同对象,导致不同行为;
在实现中,采用Java语言继承和多态的方法. 类继承(或实现继承)作为单继承模型提供,
Object抽象类处于类层次的根部. 相反, 该实现允许多实现继承(提供多接口继承(类型继承)),
允许类实现多个Java风格的接口.
*支持[leveraging]OO技术;
*从过程到OO思考的平滑过渡;
1.封装性:
-------------------------------------------------------------------------------
通过使每个类属性(instance variable)为C结构的一个域,在C中你可以包装数据和函数.
实现类方法为C函数,其第一个参数为指向属性结构的指针(this指针).通过对方法名采用一致
的命名规则,可进一步加强属性和方法之间的联合.最常用的规则为合并结构名(类名)和操作
名.函数名的改变是名字修饰的一部分(也称作名字损坏[mangling]),大多数C++编译器隐含
执行函数名的改变.因为名称修饰消除了不同类之间的方法名冲突, 所以它有效地将平面的
[flat]C函数名字空间划分为独立的,嵌套在类中的名字空间.
编码规则可解决的另一问题是访问控制. 在C中,你只能显式说明属性和方法允许访问的
级别.通过名称可比声明时注释更好地表达该意图.这样可更容易地发现代码中对类成员的
非法访问.大部分OO设计区分如下的3级保护:
*pivate--只能从类内部访问;
*protected--只能由类及其子类访问;
*public--随处可用(C中缺省);
pivate属性使用双下划线(__foo).注意没有必要在类声明文件(.H文件)中暴露私有方法;
你应该将它们完成隐藏在实现文件中(在.C文件中声明它们为static).protected成员使用单
下划线(_foo, String_Foo).public成员不使用下划线(foo, StringFoo).这样,名称中出现
下划线时,应检查访问权限.
每个类需提供至少一个constructor方法,用于初始化它的属性结构. Constructor调用
应该是初始化的唯一方法.否则,必须暴露对象的内部结构,而损坏包装性.
类可选提供destructor方法,负责释放对象生命期间分配的资源.尽管有多种方法实例化
[instantating]类(接受不同参数的不同constructors), 却只有一种方法消灭对象.
因为constructors&destructors的特殊角色,建议使用一致的命名模式.使用基名"Con"
(FooCon1, FooCon2)和"Des"(FooDes). 建议constructor返回初始化属性结构的指针或
NULL.destructor只接受一个参数this,应返回void.
对象分配可静态,动态(在堆中),自动(在栈中).由于C语法市委限制, 你不能在定义点用
constructor call初始化对象.对于静态对象,根本不能调用constructor,因为in a static
initializer不允许函数调用.自动对象必须都在block的开始定义,而此时一般没有足够的
初始化信息.因此,不得不将对象分配和初始化分离.你应将对象当做一般的C变量,初始化后
使用.一般当初始化信息可用时,立即初始化对象.
某些对象可能需要析构, 当对象过时或超出作用域时,为每个对象都调用destructor是
种好的编程习惯.后面将看到一个对所有类都可用的virtual destructor.
2.继承性:
-------------------------------------------------------------------------------
继承是一种机制,通过它,可根据已有类定义更专用的新类.子类可以包括父类的
所有属性和方法的定义.子类通过添加自己的属性和方法来扩展父类.
通过将父类的属性结构嵌入作为子类结构的第一个成员,在C中可实现这种类关系.这种
结构方式带来一种属性对齐[alignment],使得子类的指针可安全地转换[upcast]为父类的
指针.特别地,该指针可传给任何期望父类指针的函数.(为了C中严格的正确,应该显式类型
转换[upcast]该指针.)这意味着父类的所有方法都自动适用于子类.换句话说,这些方法被
继承了.
这种简单方法只适用于单继承,因为子类不能同时与多个父类的对齐属性.
我命名继承成员为super,以使类之间的继承关系更明显,并更类似Java. super成员
提供了访问父类属性的句柄[handle].例如,孙类可访问祖类的protected属性_foo,如下:
this->super.super._foo.
继承为类的constructor和destructor增加了责任.因为每个子类对象包含嵌入的父类
对象,子类constructor必须照顾初始化父类控制的部分.为了避免潜在的依赖, 在初始化
属性前应先调用父类的constructor.对于析构,则恰好相反,应在最后一步消灭继承部分.
在实现中还采用Java的单抽象基类对象的概念.这意味着任何类都不能定义为独立体
[standalone],而需扩展一些其他的类,Object类在类层次的根部.在这种混合实现(过程
语言添加OO)中,这种设置特别方便, 因为每个对象最终都可被当做Object类的实例--这可
以将对象和所有其他类型清楚地区分开来.这与C++方法不同,C++中每个结构等同于一个类.
Object类增加重要的行为,随后被所有其他的类继承,因此实现了[enabling]多态性.
参见object.h和Listing1.
3.多态性
-------------------------------------------------------------------------------
通过提供继承方法的新实现,扩展类可重载父类的行为.例如,Object类定义了析构函数
Object_Des.String类扩展Object,用自己的析构函数StringDes重载了这一行为.假设需要
删除一个包含一般Object指针的异类包容器[heterogeneous container].因为String从
Object继承而来,一些指针实际指向String对象.若能激发正确实现StringDes来删除String
对象,代码将是多态的.依靠对象(String)的run-time类,而不是指针(Object)的类,多态行为
要求方法决定[resolution],这称为动态绑定.
通过在方法决定中引入间接的附加级别,可以在C中有效地实现动态绑定.不要直接调用
方法(C函数),而使用在类描述器[descriptor]定义的函数指针来调用函数,类描述器可被
每个对象引用.类描述器(有时叫做虚拟表或VTABLE)是对应虚拟函数(有意让子类重载的方法)
的函数指针的记录.
在前面的例子中,Object类的虚拟destructor的实现如下.Object类的类描述器声明了
函数指针Des:
struct ObjectClass {
...
void (*Des)(struct Object*);
};
类Object的每个实例都包含一个指向该了描述器的指针(叫做虚拟指针或VPTR):
struct Object {
struct ObjectClass *__vptr;
};
采用如下形式进行虚拟destructor的动态绑定:
(*obj->__vptr->Des)(obj);
其中obj指向Object结构.
注意指针obj在这儿被使用了两次:一次用于决定[resolving]方法,一次作为this参数.
动态绑定需要两次的内存访问,比直接函数调用多一次.延迟[late]绑定的内存代价: 在每个
对象中需存储虚拟指针(从Object继承而来),并且每个类需存储一个VTABLE.
类描述器自己可被当做VTABLE类(a class being represented as a VTABLE object)
的单独的实例.因此你可以使用嵌套VTABLEs的技巧来完成虚拟函数的继承.这被封装在宏
VTABLE中.所有类描述器都直接或间接从ObjectClass描述器继承而来,所以都继承了该虚拟
destructor.
继承使虚拟函数调用的语法有点复杂.一般,你需要upcast对象指针(在Object类),
downcast虚拟指针__vptr(在特定的类描述器).这些操作,以及双对象指针引用,都封装在
VCALL和END_CALL宏中.例如,采用如下的形式调用对象obj的虚拟destructor:
VCALL(obj, Object, Des)END_CALL;
若虚拟函数不只使用this参数,其余的参数应列在END_CALL宏之前.例如:
result = VCALL(obj, FooClass, Foo) ,5 ,i+j END_CALL;
其中obj指针指向FooClass或其子类,虚拟函数Foo在FooClass VTABLE中定义.
虚拟表需通过它们的constructor进行初始化. 由VTABLE constructor执行的初始化可
分为两步: 拷贝继承的VTABLE; 重载选定的虚函数实现.
第一步由宏BEGIN_VTABLE自动生成.拷贝继承的VATBLE保证:给子类添加新函数不会破坏
子类, 换句话说,没必要手动修改子类(只需重编译子类代码).除非明确选定一个类来重载
子类的行为, 继承的实现已足够了.当然,若类声明自己的虚函数,相应的函数指针在该步骤
不会被初始化.
第二步将虚函数绑定到其实现,提供VMETHOD和IMETHOD宏来帮助其实现.若你不能提供
给定方法的实现(你想要它是由子类实现的纯虚函数),你仍需用Object_NoIm空实现来初始
化函数指针.Object_NoIm中断执行(通过失败断言[failing assertion]),在运行时帮助
发现未实现的抽象方法.
如前面所讲,每个对象都保留一个指向类描述器的指针(虚指针),这从Object类继承而
来.在constructor中对象初始化时,需正确地设置该指针. 这必须在父类constructor调用
之后完成, 因为父类constructor将设置该指针指向父类VTABLE. 如果正初始化的对象的
VTABLE还未设置,应调用VTBALE constructor.激发VHOOK宏来完成这两步.注意当该父类
constructor作完父类VTABLE同样的事情的时候,整个类层次被正确地初始化.
4.接口
-------------------------------------------------------------------------------
有时你只需定义对象应支持的抽象方法,而没必要提交[commit to]特定的实现.只根据
接口操作对象, 极大地减少了子系统间的实现依赖性.可重用OO设计的主要原则之一是:
对接口编程,而不对实现编程.Java对接口的支持很好地满足[addresses]了这一设计需求.
C中Java风格接口的方法只是VTABLE概念的一般化.如果类只定义抽象方法,而不定义
任何属性怎么办? 这样的类只由它的VTABLE表示.从这样的类继承,只需要维护一个指向相
应VTABLE的指针,并实现所有的抽象方法. 对象可以很容易维护多个这样的指针, 所以从
这样的特殊类的多继承将非常简单.
这没有完成解决内存对齐的问题.在期望接口的地方,不能简单地使用对象指针,因为
没有找到相应的VTABLE.这是因为附加的虚指针不能与从Object继承的虚指针__vptr对齐.
在这种情况下,你仍可添加间接级别来解决问题.
接口和类有很大的不同, 接口不来自Object(因为它们不定义属性),并且它们定义不同
VTABLE(包含__offset域).注意接口定义的虚函数也必须使用该指针,但它应该是一般Object
类型的.你可以用接口编程,而不用管特殊的实现.
5.例子代码
-------------------------------------------------------------------------------
为了说明已讨论的概念,提供了一个简单类层次的实现.类Shape扩展Object并实现接口
Scalable.这是一个抽象类(只用于继承),所以保护它的constructor和destructor.Shape类
包含String对象作为成员,说明对象合成.Scalable接口只定义一个抽象方法Scale().具体
类Circle和Rect都扩展Shape,并重载Scale()和Area()方法.测试例子将Circle对象分配在
栈帧中,将Rect对象数组分配在堆中.Shape类和Scalable接口的测试函数分别示范类和接口
的动态绑定.
作为练习,你可以修改代码,给Shape类(Object基类)添加属性或虚函数.这些修改不需要
手动修改子类.
6.失去什么
-------------------------------------------------------------------------------
使用C而非OO语言,会失去什么? 使用本文所讲的技巧,你不必牺牲太多的方便性和表达
力[expressiveness],因为你可以非常容易地将最重要的OO概念映射到C中.也不必损失太多
的可维护性,因为能使许多任务自动化.本实现最重要的特性是: 不需要对子类手动修改,
就可给父类添加新属性和方法(包括虚拟函数和接口).
真正的问题是: 在对象初始化和清除时, C比OO语言要求更严的编程纪律,特别是垃圾
的回收.但C众所周知的缺陷,不容易修复.(例如,C++仍被这些问题困扰.)
在C语言级,封装,继承,和多态性只是种设计模式.和所有的设计模式一样,它们通过
引入特定的命名规则和惯用法[idioms],以带来更高层次的抽象.你可以使用自己的规则,
最重要是要保持一致性,这可极大地改善代码的可读性,并允许快速识别模式.
如果你开始使用这些模式,会完全改变你的C思考方式和编程风格.你的C代码会与Java
更类似.
7.作者
-------------------------------------------------------------------------------
Miro Samek是GE Medical Systems的软件工程师,现在正为诊断x射线装置开发实时嵌入软件.
在波兰Cracow的Jagiellonian大学获得物理博士学位.
曾在德国Darmstadt的GSI从事核物理实验工作.
[email protected]
--------------------------------------------------------------------------------
早年使用SingleStep 6.5对Mc68332开发程序,
没有激活编译器C++特性的密码(虽然现在看来这密码如同儿戏),
但又想使用OO编程, 就找了些资料,
希望在C语言中按照一定规范进行OO编程, 实现封装性, 抽象性;
清华影印版"OOSC"写得不错, 其中章节讲拉OO In C.
我后面程序确实照这个想法重写了一次, 觉得还行;
后来, 编译器支持C++拉, 就不用拉, 不过思想不错.
对于一些使用C编译器的, 如C51等, 以及其它过程语言的,
都可以利用这种思想实现OO编程, 对程序质量有帮助.
下面的这篇文章原文来自ESP, 我在学习时翻译了部分, 不全
希望对大家有所帮助!
amine