看过之前的 virtual function可以知道其实现模型: 每一个 class 有一个 virtual table. 内含该 class 之中 有作用的 virtual function 地址, 然后每个 object 有一个 vptr, 指向 virtual table 的所在. 在这一节中, 需要走访一组可能的设计, 然后根据单一继承, 多重继承和虚拟继承的多种情况, 从细节上来探究这个模型.
为了支持 virtual function 机制, 必须首先能够对于多态对象有某种形式的执行期判断法, 也就是说,以下的调用操作将需要 ptr 在执行期的某些相关信息,
ptr->Z();
如此一来才能够找到并调用 Z() 的适当实体.
最直截了当同时成本最高的方法就是把必要的信息加在 ptr 身上, 在这样的策略之下,一个指针(或是一个 reference) 含有两项信息:
1. 它所参考到的对象地址(作为指针的本来用途)
2. 对象类型的某种编码, 或是某个结构(用以决议出 Z() 函数实例的地址)
这两个方法带来两个问题:
1. 明显增加了空间负担
2.打断了与 C 程序间的兼容性(本贾尼小哥的初衷可是"更好的 C").
如果这额外的信息不能够和指针放在一起, 下一个可以考虑的地方就是把它放在对象本身, 但是哪一个对象需要这些信息呢? 把这些信息放在可能被继承的每一个聚合体上吗? 或许吧? 但考虑如下 C struct 声明:
struct Date {int m, d. y;};
严格地说, 这符合上述规范, 然而事实上它并不需要那些信息, 加上那些信息将使 C struct 膨胀并且打破连接兼容性, 却没有任何明显的补偿利益(百害无一利)
咋办呢? 只对那些明确使用了 class 关键词的声明. 才应该加上那些额外的执行器信息吗? 还有个例子:
//符合要求但不需要那份信息 class date {public: int m, d, y;}; //需要信息但不合要求 struct geom {public: virtual ~geom();...};
好吧, 我们需要的是一个 "以 class 的使用为基础, 而不在乎关键词是 class 或 struct 的规范." 如果 class 真的需要那份信息, 它就会存在, 反之则不存在. 那么问题来了, 到底什么时候需要那份信息? 明显是在必须支持某种形式的执行器多态的时候.
在 C++ 中, 多态表示以一个 public base class 的指针(或 reference), 寻址出一个 derived class object 的意思, 例如以下声明:
Point *ptr;
我们就可以指定 ptr 以寻址出一个 Point2d 对象:
ptr = new Point2d; //或是一个 Point3d 对象 ptr = new Point3d;
ptr 的多态机能主要扮演一个输送机制(transport mechanism) 的角色, 经由它, 我们可以在程序的任何地方采用一组 public derived 类型. 这种多态形式被称为消极的(passive) , 可以在编译时期完成--virtual base class 的情况除外.
当被指出的对象真正被使用时, 多态也就变成积极的了, 下面对于 virtual function 的调用就是一例:
//积极多态的常见例子 ptr->Z();
在 runtime type identification(RTTI) 性质于 1993 年被引入 C++ 语言之前, C++ 对积极多态的唯一支持就是对于 virtual function call 的决议操作. 有了 RTTI, 就能够在执行期查询一个多态的 pointer 或多态的 reference 了.
//积极多态的第二个例子 if(Point3d *p3d = dynamic_cast<Point3d*>(ptr)) return p3d->_z;
所以, 问题已经被区分出来, 那就是, 欲鉴定哪些 classes 展现多态特性, 我们需要额外的执行期信息, 关键词 class 和 struct 并不能够帮助我们,由于没有导入 polymorphic 之类的新关键词, 因此只识别一个 class] 是否支持多态, 唯一适当的方法就是看看它 是否有任何 virtual function. 只要class 拥有一个 virtual function, 它就需要这份额外的执行期信息.
下一个问题是, 要储存哪些额外信息? 也就是说, 如果我有这样的调用:
ptr->Z();
其中 Z() 是一个 virtual function, 那么什么信息才能让我们在执行期调用正确的 Z() 实体? 我需要知道:
1. ptr 所指向对象的真实类型, 以便选择正确的 Z() 实体
2. Z() 的实体位置, 以便我们能调用它.
在实现上, 我可以在每一个多态的 class object 身上增加两个 members:
1. 一个字符串或数字, 表示class 类型
2.一个指针, 指向某表格, 表格中带有程序的 virtual functions 的执行期地址.
那么表格中的 virtual functions 地址如何被建构起来? 在 C++ 中, virtual functions 可以在编译时期被获知, 此外, 这一组地址是固定不变的, 执行期不可能新增或替换之. 由于程序执行时, 表格的大小和内容都不会改变, 所以其建构和存取皆刻意由编译器完全掌握, 不需要执行期的任何介入.
然而, 执行期备妥那些函数地址, 只是解答的一半, 另一半是找到那些地址:
1. 为了找到表格, 每一个 class object 被安插上一个由编译器内部产生的指针, 指向该表格(vptr)
2. 为了找到函数地址, 每一个 virtual function 被指派一个表格索引值(slot)
这些工作都由编译器完成, 执行器要做的, 只是在特定的 virtual table slot 中激活virtual function
一个 class 只会有一个 virtual table, 每一个 table 内含对应的 class object 中所有 active virtual functions 函数实体的地址. 这些 active virtual functions 包括:
1. 这个 class 所定义的函数实体. 它会改写一个可能存在的 base class virtual function 函数实体.
2. 继承自 base ckass 的函数实体, 这是在 derived class 决定不改写 function 时才会出现的情况.
3. 一个 pure_virtual_called() 函数实体, 它既可以扮演 pure virtual function 的空间保卫者角色, 也可以当作执行期异常处理函数.
每一个 virtual function 都被指派一个固定的索引值, 这个索引在整个继承体系中保持与特定的 virtual function 关联, 例如在 Point class 体系中:
class Point { public: virtual ~Point(); virtual Point& mult(float) = 0; //其他操作 float X() const {return _x;} virtual float Y() const {return 0;} virtual float Z() const {return 0;} //... protected: Point(float x = 0.0); float _x; };
virtual destructor 被赋值 slot 1, 而 mult() 被赋值 slot 2. 此例中并没有 mult() 的函数定义, 所以 pure_virtual_called() 的函数地址会被放在 slot2 中, 如果该函数意外地被调用. 通常的结果是结束掉这个程序(我用的编译器, 处理方式是编译时报错). Y(), Z() 为 slot 3, 4. X() 的 slot 是多少? 人家就不是 virtual function 好吧.
单一继承下的 virtual table 布局如图所示:
当一个 class 派生自 Point 时, 会发生什么事? 例如 class Point2d 继承 Point 时, 有三种可能性:
1. 它可以继承 base class 所声明的 virtual functions 的函数实体. 正确地说, 是该函数实体的地址会被拷贝到 derived class 的 virtual table 的 相应的 slot 中.
2. 它可以使用自己的函数实体, 这表示它自己的函数实体地址必须放在对应的 slot 中
3. 它可以加入一个新的 virtual function, 这时候 virtual table 是尺寸会增大一个slot, 而新的函数地址则放在 slot 4.
好了现在我有一个这样的式子:
ptr->Z();
那么我如何有足够的知识在编译时期设定 virtual function 的调用呢?
1. 一般而言, 我并不知道 ptr 所指对象的真正类型, 然而我知道, 经由 ptr 可以存取到该对象的 virtual table.
2. 虽然我不知道哪一个 Z() 函数实体会被调用, 但我知道每一个 Z() 函数地址都放在 slot 4.
这些信息使得编译器可以将调用转化为:
(*ptr->vptr[4])(ptr);
其中唯一在执行期才能知道的是, slot 4 所指的到底是哪一个函数实体.