考虑下面这种多态情况:
//thing1的类型已经确定为Library的一个object
Library thing1;
//class Book : public Library{...};
Book book;
//thing1 不是一个Book!
//book被裁切了。
thing1 = book;
//调用的是Library::check_in
thing1.check_in();
//OK:现在thing2参考到book
Library &thing2 = book;
//OK:现在引发的是Book::check_in()
thing2.check_in();
Book类继承自Library类,如果直接将子类Book的对象赋值给父类对象thing1,则thing1的类型在编译期间就已经确定,其所占的内存就是Library对象的内存,子类对象book会被裁切掉一部分来适应thing1的大小,thing1所调用的函数也只能是Library中的函数,不存在多态的可能。
只有通过pointer和reference的间接处理,才支持面向对象程序设计所需的多态性质。在以上的例子中,thing2的运用就是一个良好的例证。
在面向对象中,需要处理一个未知实例,它的类型虽然有所界定,却有无穷可能。这组类型受限于其继承体系,然而该体系理论上没有深度和广度的限制。原则上,被指定的object的真实类型在每一个特定执行点之前,是无法解析的。在c++中,只有通过pointers和references的操作才能够完成。举个例子:
//retrieve_some_material函数返回Library的某个子类指针
Library *px = retrieve_some_material();
Library &rx = *px;
//描述已知物:不可能有令人惊讶的结果产生
Library dx = *px;
对于px和rx,你没有办法确定的说出其到底指向何种类型的objects,只能够说它要么是一个Library,要么是其一个子类型。但对于dx,我们可以明确的说它是Library的一个object。
在c++中,多态只存在于一个个的public class体系中。C++通过下列方法支持多态:
1.经由一组隐式的转换操作。例如把一个子类的指针转化为一个指向其父类的指针:
Library *ps = new Book();
2.经由虚函数机制:
ps->check_in();
3.经由dynamic_cast和typeid运算符:
if( Book *pc = dynamic_cast<Book*>(ps)) ...
多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的base class中。例如在Library中,就可以为Book,Video,Puppet等子类型定义一个接口。这个共享接口是以虚函数机制引发的,它可以在执行期根据object的真正类型解析出到底是哪一个函数被调用。
那么,需要多少内存才能够表现一个class object? 一般而言要有:
1.其非静态成员函数的总和大小;
2.加上任何由于alignment的需求而填补上去的空间;
3.加上为了支持virtual而由内部产生的任何额外负担;通常为指向虚函数表的指针(vptr);
对于指针,不管它指向哪一种数据类型,指针本身所需的内存大小是固定的(通常是4字节)。但是,一个指向Library的指针是如何与一个指向整数的指针相区别的呢?
以内存需求的观点来看,没有什么不同!它们都需要足够的内存来放置一个机器地址(通常4字节)。“指向不同类型之各指针”间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的object类型不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小:
int *pi; //对于一个指向地址1000的整数指针,在32位机器上,将涵盖地址空间1000~1003;
Library *px; //对于一个Library,如果Library的对象所需的内存空间为16个字节的话,则px指针将横跨地址1000~1015;
加上多态之后
class ZooAnimal
{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void rotate();
protected:
int loc;
string name;
};
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和references都只需要一个word的空间(在32位机器上是4-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,从1000~1024;对于pz,所涵盖的地址只包含Bear object中的父类ZooAnimal部分,从1000~1016; 因此,除了ZooAnimal中出现的members,你不能够通过pz来直接处理Bear的任何members。唯一例外是通过virtual机制:
// 不合法:cell_block 不是ZooAnimal的一个member,
//虽然我们知道pz目前指向一个Bear Object.
pz->cell_block;
//Ok:经过一个显示的转换操作就没问题!
(static_cast< 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所指向的object类型可以决定rotate()所调用的实例。类型信息的封装并不是维护与pz之中,而是维护于vptr和vptr所指的虚函数表之间。因此,pz->rotate()会执行Bear 中的rotate函数。