关于c++多态
推荐《Inside The c++ Object Model 》, 文章转自其中一段。
多态是面向对象的一个重要特征,c++中多态是通过虚函数机制实现的,关于c++多态实现的一些基本知识,本文就不在细述。
通常类似这样:
Shape * ps = new circle;
ps->Rotate(); //调用的是虚函数
虽然ps是shape类型指针, 但是调用的是circle中的Rotate方法。这是毫无疑问的, 这样做会很易于我们程序的封装。多态的主要用途是经由一个共同的接口来影响类型的封装, 这个接口通常被定义在一个抽象的base class中。一个指针, 不管指向哪一种类型数据在我们的机器上他本身所需要的内存大小是固定的,如16位机器上是2byte,32位机器上是4byte,我们使用指针可以调用对象函数、成员, 是因为我们知道这个类型对象在内存中所占的区域大小, 通过指针自然能找到其中的成员地址以及虚函数列表指针。举个列子,下面有个ZooAnimal声明:
class ZooAnimal {
public:
ZooAnimal();
virtual ~ZooAnimal();
//...
virtual void rotate();
protected:
int loc;
String name;
};
ZooAnimal za("Zoey");
ZooAnimal *pza = & za;
其中class object za 和指针pza的可能布局如图下所示:
但是, 一个指向ZooAnimal的指针是如何的与一个指向整数的指针或template Array(如下, 与一个String一并产生)指针有所不同呢?
ZooAnimal * px;
int *pi;
Array<String> * pta;
以内存需求的观点来说, 没有什么不同!他们三个都需要足够的内存来存放一个机器地址(32位为4个bytes)。“指向不同类型之个指针”间的差异,既不在其指针表示法不同,也不再其内容(代表一个地址)不同,而是在其所寻址出来的Object类型不同。也就是说,“指针类型”会告诉编译器如何解释某个特定地址中的内存内容及其大小:
嗯, 那么, 一个指向地址1000而类型为void*的指针,将涵盖怎样的地址空间?是的, 我们不知道!这就是为什么一个类型为void的指针只能够含有一个地址,而不能通过它操作所指的object的缘故。
所以,转型(cast)其实是一种编译器指令,大部分情况先他并不改变一个指针所含的真正地址, 它只影响“被指出之内存的大小和其内容”的解释方式。
现在,让我们定义一个Bear, 作为一种ZooAnimal。当然,经由“public继承”可以完成这件任务:
class Bear:public ZooAnimal{
public :
Bear();
~Bear();
//..
void rotate();
virtual void dance();
protected:
enum Dances{...};
Dances dances_known;
int cell_block;
}
Bear b("Yogi");
Bear *pb = &b;
Bear &rb = *pb;
b、pb、rb会有怎样的内存需求呢?不管是pointer或者reference都只需要4个bytes(16位上2-bytes)空间。Bear Object需要24bytes, 也就是ZooAnimal的16bytes加上Bear所带来的8bytes,,如图下展示了可能的内存布局:
好, 假设,我们的Bear Object放在地址的1000处, 一个Bear指针和一个ZooAnimal指针有什么不同?
Bear b;
ZooAnimal *pz = &b;
Bear * pb = &b;
它们每个都指向Bear Object的第一个byte。 其间的差别, pb所涵盖的地址包含整个Bear object,而pz所涵盖的地址只包含Bear Object中ZooAnimal subObject 。
除了ZooAnimal subObject中出现的members, 你不能狗使用pz来直接处理Bear的任何members。唯一的列外是通过virtual机制:
//不合法:cell_block不是ZooAnimal的一个member
//虽然我们知道pz当前指向衣蛾Bear Object。
pz->cell_block;
//ok: 经过一个明白的downcast操作就没问题
((Bear*)pz)->cell_block;
//下面这样更好, 但它是一个run-time operation(成本较高)
if (Bear* pb2 = dynamic_cast<Bear*>(pz))
pb2->cell_block;
//Ok, 因为cell_block是Bear的一个member
pb->cell_block;
当我们写:
pz->rotate();
时,pz的类型将在编译时期决定以下的两点
1) 固定的可用接口。也就是说,pz只能够调用ZooAnimal的public接口
2) 该接口的access level(列如rotate()是ZooAnimal的一个public member)
在每一个执行点, pz所指的object类型可以决定rotate()所调用的实体。类型信息的封装并不是维护于pz之中,而是维护与link之中,此link存在于“Object的vptr” 和“vitual table”之间。
现在, 请看看这种情况:
Bear b;
ZooAnima za = b; //译注:这里会引起切割(sliced)
//调用ZooAnimal::rotate()
za.rotate();
为什么rotate()所调用的是ZooAnimal实体而不是Bear实体? 此外,如果初始化函数(译注:应用与行数assignment操作发生时)将一个object内容完整拷贝到另一个object中去, 为什么za的vtr不指向Bear的virtual table?
第二个问题的答案是,编译器在(1)初始化(2)指定(assignment)操作(将一个class object指定给另一个class object)之间做出了仲裁。编译器必须确保如果某个Object含有一个或一个以上的vptrs,那写vptrs的内容不会被base class object初始化或改变。
至于第一个问题的答案是:za并不是(而且也绝不会是)一个Bear,它是(并且只能是)一个ZooAnimal。多态所造成的“一个以上的类型”的潜在力量,并不能实际发挥在“直接存取objects”这件事上。有一个似是而非的观念:OO程序设计并不支持对Object的直接处理。举个例子,下面这一组定义:
{
//注:Panda继承Bear ,Bear继承ZooAnimal
ZooAnimal za;
ZooAnimal *pza;
Bear b;
Panda *pp =new Panda;
pza = &b;
}
其可能的内存布局如下图:
将za或b的地址,或pp所含的内容(也是个地址)指定给pza, 显然不是问题。一个Pointer或一个reference之所以支持多台,是因为他们并不引发内存中任何“与类型有关的内存委托操作(type-dependent commitment)”;会受到改变的只是它们所指向的内存的“大小和内容解释方式”而已。
然而,任何人如果改变Object za的大小(或是被指定为)一个derived class Object时, derived object就会被切割, 以塞入较小的base type内存中, derived type将没有留下任何蛛丝马迹。多态于是不再呈现,而一个严格的编译器可以再编译时期解析一个“通过该Object而触发的virtual function调用操作”,因而回避virtual机制。如果virtual function 被定义俄日inline,则更有效率上的大收获。
总而言之,多态是一种威力强大的设计机制,允许你继承一个抽象的public接口之后,封装相关的类型。然而需要付出的代价就是额外的间接性--不论是在“内存的获得”或是“类型的决断”上。c++通过class 的pointer和references 来支持多态,这种程序设计风格就是“面向对象”。