C++中的多态与多重继承实现与Java的区别

2020-03-22 14:01:00王旭

C++ 多态与多重继承

有一个非常有意思的问题:C++ 发生多重继承时,如何支持多态。刚刚提到,多态的原理是编译器将成员函数调用编译为「引用虚函数表中第 N 个函数」,虚函数表在对象数据结构中的位置和要调用虚函数列表中的第几个函数在编译时都是需要确定的。多重继承对象如果只有一个虚函数列表,那不同父类的虚函数列表中的位置就要发生冲突。如果有多个虚函数列表,编译时就难以确定虚函数列表指针在数据结构中的位置。C++ 采取了非常精妙的做法:将所有父类的数据结构(包括虚指针列表)在该对象的数据结构上依次排列,该对象的指针正常指向数据结构起始位置。当指针发生类型转换时,C++ 编译器会对指针的值尽可能的进行调整,使其指向该指针类型应该对应的位置。指针的值在这个过程中发生了变化。

比如,Child 类继承了 Parent1, Parent2 两个类,则在 Child 指针转换为 Parent1 指针时,不对指针的值进行调整,因为 Parent1 是 Child 的第一个父类。但将 Child 转换为 Parent2 时,需要将指针指增加 Parent1 数据结构长度的值,使指针指向对应 Parent2 数据结构开始位置。在本例子中,Parent1 数据结构只有虚函数列表指针,在 64 位机器上长度为 8. 因此,在 Child 指针转换为 Parent2 指针时,其值增加了 8.

class Parent1 {
  public:
  virtual void sayHello() { printf("Hello from parent1!n"); }
};

class Parent2 {
  public:
  virtual void sayHi() { printf("Hi from Parent2!n"); }
};

class Child : public Parent1, public Parent2 {
  public:
  virtual void sayHello() { printf("Hello from child!n"); }
  virtual void sayHi() { printf("Hi from child!n"); }
};

int main() {
  Child *p = new Child();
  printf("size of Child: %d", sizeof(Child));
  printf("pointer val as Child*: %lldn", int64_t(p));
  printf("pointer val as Parent1*: %lldn", int64_t((Parent1*)p));
  printf("pointer val as Parent2*: %lldn", int64_t((Parent2*)p));
}

运行这段代码,会发现 Child 数据结构大小增长到 16,也即两个指针。并且指针的值在后两次类型转换时是不同的,在 64 位机器上相差 8 个字节,也即 Parent1 的数据结构大小。另外如果将 p 转换成 Void 指针再转换为 Parent 指针,此时编译器就不能正确推断这个偏移量,此时就会发生未定义行为。

这个特性其实说明了一个非常有意思的事实:C++ 编译器在编译时能够推断指针的偏移量,那么编译器也应该可以推断该指针指向对象的真实类型。那么,既然可以编译时推断对象真实类型,那要虚函数表又有何用?直接推断正确的函数调用不就可以了吗?问题在于,如果真的在编译时推断多态函数调用,就意味着要为不同类型的对象生成不一样的二进制代码。同一行代码,根据指针值的不同,产生的函数调用不同。这样一来也意味第三方库需要提供源代码,来进行相关的推断,类似于模板库。这都是不可接受的,因此虚函数列表仍然有必要。借助虚函数列表,使用指针的代码能够生成一致的机器码。