天方夜谭VCL: 开门虫虫
前言
如果你爱他,让他学VCL,因为那是天堂。
如果你恨他,让他学VCL,因为那是地狱。
──《天方夜谭VCL》
传说很久很久以前,中国和印度之间有个岛。那里的国王每天娶一个女子,过夜后就杀,闹得鸡犬不宁,最后宰相的女儿自愿嫁入宫。第一晚,她讲了一个非常有意思的故事,国王听入了迷,第二天没有杀她。此后她每晚讲一个奇特的故事,一直讲到第一千零一夜,国王终于幡然悔悟。这就是著名的《一千零一夜》,也就是《天方夜谭》。印度和中国陆地接壤,那么相信传说中所指的岛,必然是在南中国海-马六甲海峡-印度洋某个地方。现在我也算是在这其间的一个海岛上,正值夜晚,也就借借“天方夜谭”的大名吧。
初中我最喜欢的编程环境是Turbo C 2.0,高一开始用Visual Basic。后来用了没多久就发现,如果想做一个稍微复杂的东西,就需要不停地查资料来调用API,得在最前面作一个长得可怕的API函数声明。于是我开始怀念简洁的C语言。有位喜欢用Delphi的师哥,知道我极为愤恨Pascal,把我引向C++ Builder。即使对于C++中的继承、多态这些简单概念都还是一知半解,我居然也开始用VCL编一些莫名其妙的小程序(VCL上手倒真容易),开始熟悉VCL的结构,同时也了解了MFC和SDK,补习C++的基础知识。后来我才觉得,VCL易学易用根本是个谎言。其实VCL相当难学,甚至比MFC更麻烦。
不知道为什么,C++ Builder的资料出奇地少,也许正是这个原因,C++ Builder论坛上的人情味也特别浓。不管是我初学VCL时常问些莫名其妙白痴问题的天极论坛,还是现在我经常驻足的CSDN,C++ Builder论坛给人的感觉总是很温馨。每次C++ Builder都比同等版本Delphi晚出,每次用C++还不得不看Object Pascal的脸色,我想这是很多人心里的感受。CLX已经出现在Delphi6中,C++ Builder6的发布似乎还遥遥无期。CLX会代替VCL吗?看来似乎不会,后面还会提到。我也看过不少要号召把VCL用C++改写的帖子,往往雷声大雨点小。看看别人老外,说干就干,一个FreeCLX项目就这么启动了。
用MFC的人比用VCL的运气好,他们有Microsoft的支持,有Inside Visual C++、Programming Windows 95 with MFC、MFC Internals这些天王巨星的英文名著和中文翻译,也有诸如侯捷先生的《深入浅出MFC》(即Dissecting MFC)这些出色的中文原创作品。使用Delphi的人也远比使用C++ Builder的命好,关于Delphi的精彩资料远远比C++ Builder多,很无奈,真的很无奈。
C++ View杂志的主编向我约稿,我很为难,因为时间和技术水平都成问题。借用侯捷先生一句话,要拒绝和你住在同一个大脑同一个躯壳的人日日夜夜旦旦夕夕的请求,是很困难的。于是我下决心,写一系列分析VCL内部原理的文章。所谓“天方夜谭”,当然对初学者不会有立杆见影的帮助,甚至于会让您觉得“无聊”。这些文章面向的朋友应该比较熟悉VCL,有一定C++的基础(当然会Object Pascal和汇编更好),比如希望知道VCL底层运作机制的朋友,和希望自己开发应用框架或者想用C++重写VCL的朋友。同时我更希望大家交流一下解剖应用框架的经验,让我们不局限于VCL或者MFC,能站在更高的角度看问题,共同提高自己的能力。
在深入探讨VCL之前,先得把VCL主要的性质说一下。
同SmallTalk和Java所带的框架一样,VCL是Object Pascal的一部分,也就是说语言和框架之间没有明确的界限。比如Java带有JDK,任写一个类都是java.lang.Object的子类。VCL和Object Pascal是同样的道理。当然,Object Pascal为了兼容以前的Pascal,依然允许某个类没有任何父类,但本系列文章将不再考虑这种情形。
同大多数框架一样,VCL采取的是单根结构。也就是说,VCL的结构是以一棵TObject为根的继承树,除TObject外的所有VCL类都是TObject直接或间接的子类。
由于Object Pascal的语言特性,整个结构中只使用单继承。
所以,VCL的本质是一个Object Pascal类库,提供了Object Pascal和C++两个接口。在剖析的过程中,请时刻牢记这一点。
文章的组织结构是就事论事,一次一个话题。由于VCL并不像MFC是一个独立的框架,它与Object Pascal、IDE、编译器结合非常紧密,所以在剖析过程中不免会提到汇编。当然不会汇编的朋友也不用怕,我会把汇编代码都解释清楚,并尽量用C++改写。
文中有很多图是表示类的内存结构,如图所示。其中方框表示一个变量,两端伸出表示还有若干个变量,椭圆标注是说明虚线圆圈中的整个对象(在后面虚线圆圈不会画出)。
图1图例
文中的程序,如非特别说明,均可以在Console Application模式下(如果使用了VCL类则需要复选“Use VCL”)编译通过。
开门
倒霉者如愚公,开门就见太行、王屋山。在一怒之下他开始移山,最后幸亏天神帮忙搬走了。中国人不喜欢开门见山的性格可能就是愚公传下来的,说话做事老爱绕弯。当然我也不能免俗,前面废话了一大堆,现在接着来。
提起RTTI(runtime type identification,运行时间类型辨别),相信大家都很熟悉。C++的RTTI功能相当有限,主要由typeid和dynamic_cast提供[1]。至于这两者的实现方式[2],不是我们今天的话题,我们所关注的,乃是VCL所提供的“高级”RTTI的底层机制。
熟悉框架的朋友都知道,框架往往会提供“高级”的RTTI功能。我曾看过一个论调,说Java和Object Pascal比C++好,原因是因为它们的RTTI更“高级”。且不论滥用RTTI极为有害,事实上,C++用宏(macro)亦可以模拟出相同功能的RTTI[3]。
不过对于VCL类来说,您清楚其RTTI机制的运作情况吗?对于如下 class A: public TObject
{
...
}
...
A* p = new A;
为什么p->ClassName();就能返回类A的名字“A”呢?
为什么A::ClassName(p->ClassParent())就可以返回A的基类名“TObject”呢?
为什么……?
其实这都是编译器暗箱操作的结果。说白了,编译器先在某个地方把类名写好,到时候去取出来就行。关键在于,如何去取出来呢?显然有指针指向这些数据,那么这些指针放在什么地方呢?
记得《阿里巴巴和四十大盗》的故事吧?宝藏是早就存在的,如果知道口诀“芝麻,开门吧”,就可以拿到宝藏。同样,类的相关信息是编译器帮我们写好了的,我们所关心的,就是如何获取这些信息的“口诀”。
不过这一切,要从虚函数开始,我们得先复习一下C/C++的对象模型。
虚拟函数表VFT
C语言提供了基于对象(Object-Based)的思维模型,其对象模型非常清晰。比如
struct A
{
int i;
char c;
};
图 2结构的内存布局
在32位系统上,变量i占用4个字节,变量c占用1个字节。编译器可能还会在后面添加3个字节补齐。那么,sizeof(A)就是8。
C++提供了面向对象(Object-Oriented)的思维模型,其对象模型建立在C的基础上。对于没有虚函数的类,其模型与C中的结构(struct)完全一样。但如果存在虚函数,一般在类实体的某个部分会存在一个指针vptr,指向虚拟函数表VFT(Virtual Function Table)的入口。显然,对于同一个类的所有对象,这个vptr都是相同的。例如 class A
{
private:
int i;
char c;
public:
virtual void f1();
virtual void f2();
};
class B: public A
{
public:
virtual void f1();
virtual void f2();
};
当我们作如下调用的时候 A* p;
...
p->f2();
程序本身并不知道它会调用A::f还是B::f或是其它函数,只是通过类实体中的vptr,查到VFT的入口,再在入口中查询函数地址,进行调用。由于Borland C++编译器把vptr放在类实体的头部,因此下面均有此假设。
为了更充分地说明问题,我们从汇编级来分析一下。假设我们采用的是Borland C++编译器。 p->f2();
这句的汇编代码是 mov eax,[ebp-0x04]
push eax
mov edx,[eax]
call dword ptr [edx+0x04]
pop ecx
图3C++类实体的内存布局
第一句ebp-0x04是指针变量p的地址,第一句是把p所指向的对象的地址传送到eax;
第二句不用管它;
第三句是把对象头部的指针vptr传到edx,即已取得VFT的入口;
第四句是关键,edx再加4(32位系统上一个指针占4个字节),也就是调用了从VFT入口算起的第二个函数指针,即B::f2;
第五句不用管它。
相信大家对VFT和C++的对象模型有一个更深刻的认识吧?对于VFT的实现,各个编译器是不一样的。有兴趣的朋友不妨可以自行探索一下Microsft Visual C++和GCC的实现方法,比较一下它们的异同。
知道了VFT的结构,那么想想下面这个程序的结果是什么。 #include
using namespace std;
class A
{
int c;
virtual void f();
public:
A(int v = 0) { c = v;}
};
void main()
{
A a, b(20);
cout << *(void**)&a << endl;
cout << *(void**)&b << endl;
}
我想您应该能理解其中*(void**)&a吧?这是取得vptr的值,也就是a所在内存空间的前4个字节,一个指针。下面我们还会使用类似的语句。
无庸质疑,结果是输出两个完全相同的值。前面我们已经说过,对于同一个类的所有对象,其vptr值都是相同的。
那么这个VFT到底有什么作用呢?现在看来,似乎就是储存虚函数的地址。
虚拟方法表VMT
如何通过类的实体来找到类的相关RTTI信息呢?显然,VFT是同一个类的所有实体共享的数据,而RTTI正好也是。那么,把RTTI放在VFT里,就是个不错的选择。
往哪儿放呢?VFT从入口开始往后是各个虚函数的指针,那么RTTI只能放在两个地方:入口以前或者所有虚函数指针之后。显然,放在入口以前更好,至少我们不用关心虚函数的多少,RTTI的位置也可以相对确定。
VCL就采用了这个办法来放置RTTI,不过把VFT换了名字,叫虚拟方法表VMT(Virtual Method Table)。VMT的结构是怎样的呢?Borland所提供的帮助文件里没有任何相关资料,不过我们在Include\Vcl\system.hpp中就能找到如下蛛丝马迹。 static const Shortint vmtSelfPtr = 0xffffffb4;
static const Shortint vmtIntfTable = 0xffffffb8;
static const Shortint vmtAutoTable = 0xffffffbc;
static const Shortint vmtInitTable = 0xffffffc0;
static const Shortint vmtTypeInfo = 0xffffffc4;
static const Shortint vmtFieldTable = 0xffffffc8;
static const Shortint vmtMethodTable = 0xffffffcc;
static const Shortint vmtDynamicTable = 0xffffffd0;
static const Shortint vmtClassName = 0xffffffd4;
static const Shortint vmtInstanceSize = 0xffffffd8;
static const Shortint vmtParent = 0xffffffdc;
static const Shortint vmtSafeCallException = 0xffffffe0;
static const Shortint vmtAfterConstruction = 0xffffffe4;
static const Shortint vmtBeforeDestruction = 0xffffffe8;
static const Shortint vmtDispatch = 0xffffffec;
static const Shortint vmtDefaultHandler = 0xfffffff0;
static const Shortint vmtNewInstance = 0xfffffff4;
static const Shortint vmtFreeInstance = 0xfffffff8;
static const Shortint vmtDestroy = 0xfffffffc;
注意这些常数值中的负数采用的是补码表示法。求一个负数的补码,先写出相应正数的补码表示,再按位求反,最后(在最低位)加1即可。对于求32位负数的补码,也可以用它本身减去0xffffffff再减1即可。以0xfffffffc为例,0xfffffffc – 0xffffffff – 1 = – 0x04,这就是结果。我们还可以从Borland提供的原始码Source\Vcl\system.pas获得,其中就是用负数表示。
看着这份表格,从这些变量名中,我们已经猜到了其大概的分布情况。这些数字之间的间隔都是[4],可以猜想这些都是指针:函数指针或者数据指针。从这些常数的名字我们就可以知道它们的作用,比如vmtClassName自然就是储存类名的指针。入口0以前,就是VCL对象的关键数据。无疑,它们蕴涵了TObject乃至VCL对象关键的秘密,也就是VMT的分布结构。
这以上只是我们的推测,我们还应该验证一下。我们知道的事实是,每一个对象必然都包含了其所属类的相关信息。比如任何一个C++类的实体,都包含一个指向虚拟函数表VFT的指针。VCL类的实体必然也包含一个指向虚拟方法表VMT的指针。 #include
#include
using namespace std;
class A: public TObject
{
int x;
virtual void f1() {}
virtual void f2() {}
public:
A(int v = 0): x(v) {}
};
void main()
{
A* p = new A;, * q = new A(100);
void* a = *(void**)p, * b = *(void**)q;
void* c = p->ClassType(), * d = q->ClassType();
cout << a << ' ' << b << endl;
cout << c << ' ' << d << endl;
cout << __classid(A) << endl;
delete p;
delete q;
}
结果很有意思,输出的五个指针地址完全一样!a和b相同,从前面的例子我们就可以知道。然而TObject的ClassType方法和__classid操作符的返回值也跟这两者相同,这就有点意思了。查查帮助就可以知道,__classid是C++ Builder中新增的扩展关键字,返回类的VMT的入口地址;而TObject的ClassType方法则是返回对象的类信息,返回类型是TClass(也就是TMetaClass*)。这说明,每个VCL类实体的头部包含的指针,就是指向VMT的入口地址。而这个位置,也就是TObject的成员函数ClassType的返回值,亦即运算符__classid返回的类A的信息,只不过这个返回值是以TClass(即TMetaClass*)的形式存在。
图4VCL类的VMT入口
我们已经知道了VMT的结构,现在又找到了其入口,此时的兴奋不亚于阿里巴巴知道“芝麻,开门吧”这句咒语时的感受。既然知道了开门的咒语,还不赶快进去拿宝藏?
牛刀小试
乘着东风,我们来模拟一下VCL简单的RTTI功能。为方便起见,我们仿造TObject,写一个类FObject(呵呵,如果把TObject看成True Object,我们的FObject就是False Object)。要问下面这段代码从哪里来?大部分都Copy&Paste自Include\Vcl\systobj.h文件。 class FObject
{
public:
FObject(); /* Body provided by VCL {} */
Free();
TClassClassType();
voidCleanupInstance();
void *FieldAddress(const ShortString &Name);
/* class method */
static TObject * InitInstance(TClass cls, void *instance);
static ShortString ClassName(TClass cls);
static bool ClassNameIs(TClass cls, const AnsiString string);
static TClass ClassParent(TClass cls);
static void * ClassInfo(TClass cls);
static long InstanceSize(TClass cls);
static bool InheritsFrom(TClass cls, TClass aClass);
static void * MethodAddress(TClass cls, const ShortString &Name);
static ShortString MethodName(TClass cls, void *Address);
/* Hack: GetInterface is an untyped out object parameter and
* so is mangled as a void*. In practice, however, it is
* really a void**. Be sure when using this method to provide
* two levels of indirection and cast away one of them.
*/
bool GetInterface(const TGUID &IID, /* out */ void *Obj);
/* class method */
static PInterfaceEntry GetInterfaceEntry(const TGUID IID);
static PInterfaceTable * GetInterfaceTable(void);
ShortString ClassName()
{
return ClassName(ClassType());
}
bool ClassNameIs(const AnsiString string)
{
return ClassNameIs(ClassType(), string);
}
TClass ClassParent()
{
return ClassParent(ClassType());
}
void * ClassInfo()
{
return ClassInfo(ClassType());
}
long InstanceSize()
{
return InstanceSize(ClassType());
}
bool InheritsFrom(TClass aClass)
{
return InheritsFrom(ClassType(), aClass);
}
void * MethodAddress(const ShortString &Name)
{
return MethodAddress(ClassType(), Name);
}
ShortString MethodName(void *Address)
{
return MethodName(ClassType(), Address);
}
virtual HResult SafeCallException(TObject *, void *);
virtual void AfterConstruction();
virtual void BeforeDestruction();
virtual void Dispatch(void *Message);
virtual void DefaultHandler(void* Message);
private:
virtual TObject* NewInstance(TClass cls);
public:
virtual void FreeInstance();
virtual ~FObject(); /* Body provided by VCL {} */
};
当然FObject::ClassType我们已经会写了,那就是 TClass FObject::ClassType()
{
return *(TClass*)this;
}
我们会在后面陆续把这些成员函数填充完整。先举个例,拿类名(ClassName)开刀吧。
查查VMT表,vmtClassName = 0xffffffd4,我们就从这里下手。主要的步骤是:
找到VMT的入口;
通过vmtClassName找到储存类名的地址;
获取类名。
0xffffffd4也就相当于– 44,也就是VMT入口指向的地址开始,倒数第44字节到倒数第41字节这4个字节所代表的指针,指向类名。假设入口指向的地址是cls,那么vmtClassName所代表的地址就是(char*)cls – 44,亦即(char*)cls + vmtClassName。
注意一个字符串格式的问题,VCL既然是用Object Pascal写的,其中储存类名的字符串的格式必然是Pascal传统方式,也就是第1个字节为字符串的长度,紧接着为字符串的实际内容。在C++ Builder中,与之对应的类型是ShortString。
图5TObject::ClassName的运作方式
代码如下: ShortString FObject::ClassName(TClass cls)
{
ShortString* r = *(ShortString**)((char*)cls + vmtClassName);
return *r;
}
我们不妨测试一下。 #include
#include
#include
using namespace std;
... 插入FObject相应的代码...
void main()
{
auto_ptr list(new TList);
FObject* p = (FObject*)list.get();
cout << AnsiString(p->ClassName()).c_str() << endl;
cout << AnsiString(list->ClassName()).c_str() << endl;
}
输出结果在我们意料之内,都是“TList”。
对于函数ClassNameIs,我们就可以轻而易举地完成了。 bool FObject::ClassNameIs(TClass cls, const AnsiString string)
{
return string==ClassName(cls);
}
有朋友可能奇怪,你怎么知道TObject::ClassName是这样的呢?
三种办法:
猜,用经验推测;
看Borland提供的原始码;
看编译以后的汇编码。
在Borland提供的原始码中,我们可以看到TObject::ClassName的实现如下: class function TObject.ClassName: ShortString;
asm
{ ->EAX VMT}
{EDX Pointer to result string}
PUSH ESI
PUSH EDI
MOV EDI,EDX
MOV ESI,[EAX].vmtClassName
XOR ECX,ECX
MOV CL,[ESI]
INC ECX
REP MOVSB
POP EDI
POP ESI
end;
熟悉汇编的朋友就可以由此写出相应的C/C++代码来。对于不会的朋友,根据我们的讲解,相信也可以轻而易举地完成吧。
希望您在看这段的时候,不妨先用第1种办法,然后结合2、3看看,一定收获不小。
势如破竹
接下来就太简单了,我们不再举例,把相应的成员函数补充完整即可。您不妨先自己试着写写,探索一下,再与汇编代码和文中的代码作比较,一定乐趣无穷。
TObject::ClassInfo是做什么的?问我啊?我也不知道。VCL的帮助里说,用ClassInfo可以访问包含对象类型、祖先类和所有published属性信息的RTTI表。这个表只是内部使用,TObject提供了其它方法来访问RTTI信息。我们先写出它的实现。
图6TObject::ClassInfo的运作方式 void * FObject::ClassInfo(TClass cls)
{
return *(void**)((char*)cls + vmtTypeInfo);
}
Borland的说法可信吗?这个函数返回值的类型是void *,明摆着不愿意透露更多的信息。您不妨按上面ClassName的方法测试一下,对于TList,ClassInfo输出的结果居然是0!也就是一个空指针!什么东东?别急,后面我们会掀开这个void *的面纱,现在姑且卖个关子。
VCL框架中只存在单继承,这是由Object Pascal语言的特性决定的。这样,每一个类只有唯一一个父类,函数TObject::ClassParent就能帮您把父类找出来。 TClass FObject::ClassParent(TClass cls)
{
TClass* r = *(TClass**)((char*)cls + vmtParent);
return (r)? (*r) : 0;
}
由此,我们也能很轻松地模拟TObject::InheritsForm的实现。 bool FObject::InheritsFrom(TClass cls, TClass aClass)
{
while(aClass)
{
if(aClass==cls)return true;
cls = ClassParent(cls);
}
return false;
}
要知道一个对象所占的字节数,TObject::InstanceSize就可以达到目的。 long FObject::InstanceSize(TClass cls)
{
return *(long*)((char*)cls + vmtInstanceSize);
}
有朋友可能说,C++不是有sizeof操作符吗?为什么不用呢?在VCL中,sizeof有两个缺陷。首先sizeof是完全静态的,也就是说,如果您写sizeof(...),编译以后,这会被替换为一个常数,没有任何的求值过程,因此不能动态求值;其次,VCL类必须与指针或引用的形式存在。所以对于 TObject *a;
...
sizeof(*a)这样的表达式是错误的。而且即使TObject不是VCL类,使用sizeof(*a)还是相当于sizeof(TObject),没有实际价值。
结束语
现在我们已经打开了通向VCL类秘密的大门。回头一看,VMT跟VFT有什么区别与联系呢?其实VMT可以算是VFT具体化的一个结果,也就是说,VMT是在VFT基础上发展出来的一种具有“规范”性质的结构,所有的VCL类都遵循这个“规范”。这很像COM与C++纯虚基类的关系。
通过VMT,VCL放置了一些重要的信息,由此来实现RTTI。所以“高级”RTTI功能其实是相当低级和简单的一项技术。就其实现方式而言,大致有三种。MFC用宏(macro)模拟算是一类,完全符合C++标准,不需要对语言进行扩充,也不依赖于特定的编译器,不过给人臃肿的感觉;VCL则是完全由编译器实现,同时扩充了C++的语言特性,必须在Borland的编译器上编译,但是很简洁;另外建构KDE基础的跨平台框架Qt[4],则采用了折中的方式,扩充了C++的关键字,书写很简洁,在编译之前必须用Qt提供的程序MOC进行预处理,把扩充部分的代码改写为符合C++标准的代码,然后才可以在任何符合C++标准的编译器上编译。
代表作
实现方式
不依赖特定编译器
简洁程度
编译次数
MFC
宏插入
是
一般
1
VCL
编译器生成
否
好
1
Qt
预编译程序生成
是
较好
2(包括MOC)
注:如果长期仅在Windows平台下进行开发的朋友,可能没有听说过Qt的大名。事实上在Linux世界里,这可是个响当当的名头。Qt是一套完善的C++框架,横跨Unix/Linux、Windows、Mac OS诸多平台,内部机制相当有趣。Borland最新的Kylix和Delphi6所采用的跨平台框架CLX(分为BaseCLX、VisualCLX、DataCLX、NetCLX四个部分,BaseCLX与VCL顶部几个类相同),其可视化部分VisualCLX就建构在Qt上,这多少让我感到失望和不满。Qt本身就跨平台,VisualCLX建构在Qt上,自然也跨平台;但是CLX是用Object Pascal包装了一个C++框架,我不敢想象,C++ Builder6中的CLX是否又用C++再来包装这个包装了C++框架的Object Pascal框架呢?如果真是如此,其效率和调试难度……
对于“高级”RTTI的实现形式,VCL用了TMetaClass(其中TClass就是TMetaClass*)来配合存储类的信息,也就是所谓“类的类”,这非常普遍。MFC中的CObject与CRuntimeClass,JDK中的java.lang.Object和java.lang.Class都是如此。比如对于一个TObject *p,如何获取其父类名呢?我们必须借助TMetaClass:可以先用p->ClassType返回父类的信息(是一个TMetaClass*类型),再以此为参数传入TObject::ClassName就可以获得结果,也就是TObject::ClassName(p->ClassType())即可。
同时我们也应该拆穿所谓“拥有更高级RTTI的语言本身也更高级”的谎言。至少从我那少得可怜的经验来看,对于一套框架,除非需要和IDE配合,否则在绝大部分情况下,RTTI是完全没有必要的,甚至是有害的[5]。希望使用和设计框架的朋友三思。
致谢
非常感谢孟岩和孙春阳对本文所提出的宝贵意见。
参考
1. Bjarne Stroustrup. The C++ Programming Language, 3e. Addison-Wesley, Reading, MA. 1997.
2. Stanley Lippman. Inside the C++ Object Model. Addison-Wesley, Reading, MA. 1996
侯捷.《深度探索C++物件模型》.碁峰资讯股份有限公司.1998.
侯捷.《深度探索C++对象模型》.华中科技大学出版社.2001.
3. 侯捷.《深入浅出MFC》,2e.松岗电脑图资料股份有限公司/华中科技大学出版社.1997/2001.
4. 虫虫.《Qt最新消息》.C++ View.2001,7.
5. Robert C.Martin. “The Open-Closed Principle”. C++ Report. 1996, 1.
plpliuly,虫虫.《开放封闭原则OCP》.C++ View.2001,8.