15.5 C++如何实现晚捆绑
晚捆绑如何发生?所有的工作都由编译器在幕后完成。当告诉编译器要晚捆绑时(通过创建虚函数来告诉),编译器安装必要的晚捆绑机制。因为程序员常常从理解C++虚函数机制中受益,所以这一节将详细阐述编译器实现这一机制的方法。
关键字virtual告诉编译器它不应当执行早捆绑,相反,它应当自动安装对于实现晚捆绑必需的所有机制。这意味着,如果对Brass对象通过基类Instrument地址调用play(),将得到恰当的函数。
为了达到这个目的,典型的编译器[1]对每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数的地址。在每个带有虚函数的类中,编译器秘密地放置一个指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。当通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入能取得这个VPTR并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数并引起晚捆绑的发生。
为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的,所以不必担心。利用虚函数,即使在编译器还不知道这个对象的特定类型的情况下,也能调用这个对象中正确的函数。
下面几节将进行更详细的阐述。
15.5.1 存放类型信息
可以看到,在任何类中不存在显式的类型信息。而先前的例子和简单的逻辑告诉我们,必须有一些类型信息放在对象中;否则,类型将不能在运行时被建立。确实是这样的,但类型信息被隐藏了。为了看到这些信息,这里举一个例子,以便检查使用虚函数的类的长度,并与没有虚函数的类进行比较。
不带虚函数,对象的长度恰好就是所期望的长度:单个[2]int的长度。而带有单个虚函数的OneVirtual,对象的长度是NoVirtual的长度加上一个void指针的长度。它反映出,如果有一个或多个虚函数,编译器都只在这个结构中插入一个单个指针(VPTR)。因此OneVirtual和TwoVirtuals的长度没有区别。这是因为VPTR指向一个存放函数地址的表。我们只需要一个表,因为所有虚函数地址都包含在这个单个表中。
这个例子至少要求一个数据成员。如果没有数据成员,C++编译器会强制这个对象是非零长度,因为每个对象必须有一个互相区别的地址。如果我们想象在一个零长度对象的数组中索引寻址,就能理解这一点。把一个“哑”成员插入到对象中,否则这个对象就会是零长度。当类型信息由于存在这个关键字virtual而被插入时,这个“哑”成员的位置就被占用。在上例中,用注释符号将int a这一行去掉,就会看到这种情况。
[1]编译器可以按它们希望的任何方式执行虚操作,但是这里所讨论的方法是一种相当通用的方法。
[2]这里某些编译器可能含有长度功能,但是并不多见。