用C语言进行面向对象编程

*******************************************************************************
C中可移植的继承性和多态性2
Fr: [ESP-9712-code]
By: Miro Samek
Rd: Amine Chen[amine@263.net]
*******************************************************************************
快速参考:
=========
本文解释如下这些"设计模式"在C中的实现:
封装性,继承性,多态性.
类继承: 单继承; 接口继承: 多继承.

用C语言进行面向对象编程

*******************************************************************************
C中可移植的继承性和多态性2
Fr: [ESP-9712-code]
By: Miro Samek
Rd: Amine Chen[amine@263.net]
*******************************************************************************
快速参考:
=========
本文解释如下这些"设计模式"在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[amine@263.net]
*******************************************************************************
虽然面向对象设计几乎与语言无关,但大部分文献都采用[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从事核物理实验工作.
miroslaw.samek@med.ge.com

 

http://blog.csdn.net/dadalan/archive/2009/03/12/3983888.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习C语言的好书. OOPC是指OOP(Object-Oriented Programming)与C语言的结合,它是一个面向对象C语言编程框架。它是一套C语言的宏,定义了OOP概念的关键字,借助于这一套宏,实现面向对象的特性,如类、对象、继承、接口、多态、消息等。   C++对于大型软件架构的良好可控性,和对以后程序员维护代码时良好的可读性;然而就目前来说,在嵌入式领域广泛的使用C++显然是不现实的事情。一般的嵌入式系统开发中只用到了其中的一小部分功能,而不需要全部的机制,比如多重继承、运算符重载等。因此,许多嵌入式系统的开发者就舍弃了C++的庞大身躯而回归到精简的C环境中。 一般情况下,一个更容易扩展、维护的软件通常采用的是OOP的思想,添加一个原本不存在的相对无关单独的个体,总比在一个已经存在的过程内硬塞进去一个对象要简单;而且面向过程更容易导致混乱的维护。然而舍弃C++的同时也舍弃了珍贵的OOP能力,实在太可惜了。 C语言良好的可移植性,对内存等良好的操作性以及执行之速度均是一般嵌入式产品的不二首选。我们要应此放弃C++吗?当然不,幸好已经有很多优秀的设计师为我们指明了C语言OOP化的道路。 虽然OOPC语法不如C++那么简洁,但是OOPC也有亮丽的特色,就是编译后的程序所占的内存空间比C++小的多,执行效率高,适用于Embedded System。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值