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

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

多态问题

笔者校招面试时被问到了著名问题「C++ 与 Java 如何实现多态」,然后不幸翻车。过于著名反而没有去准备,只知道跟虚函数表有关。面试之后比较了 C++ 和 Java 多态的实现的异同,一并记录在这里。

C++ 多态的虚指针实现

首先讨论 C++. 多态也即子类对父类成员函数进行了重写 (Override) 后,将一个子类指针赋值给父类,再对这个父类指针调用成员函数,会调用子类重写版本的成员函数。简单的例子:

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

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

int main() {
  Parent1 *p = new Child();
  p->sayHello();  // get "Hello from child!"
}

首先需要明白,对于底层实现而言,成员函数就是第一个参数为对象指针的函数,编译器自动将对象指针添加到函数参数中并命名为 this 指针,除此之外与普通函数并无本质不同。对于非多态的成员函数调用,与非成员函数调用过程基本是一致的,根据参数列表(参数列表中包含对象指针类型)和函数名在编译时确定实际调用的函数。

为了实现多态,不能只根据对象指针类型推断函数签名,也即例子中,p->sayHello() 这一行代码在执行时不能只根据 p 的类型确认调用的函数应该是 Parent::sayHello 还是 Child:sayHello。在多态机制下,每个类父类和子类都需要在其数据结构中多携带一个指针,这个指针指向该类的虚函数表。

类的虚函数表也即所有可能发生重写的函数指针表,对象创建时根据其实际类型决定其虚函数指针指向的虚函数列表。如在上文的例子中,Parent1 和 Child 类的虚函数列表都只有一个函数,分别是 Parent1::sayHello Child::sayHello. 编译器在编译时将会把函数调用翻译为「引用虚函数表中的第 N 个函数」这样的指令,比如本例中翻译为「引用虚函数表中第一个函数」。在运行时读取虚函数表中真正的函数指针。运行时 CPU 代价基本是一次指针解引用和一次下表访问。

Parent1 和 Child 对象都没有自定义的数据结构。运行以下代码能够确认 Parent1 和 Child 对象的真实数据结构大小都是 8 字节,也即只有虚函数列表指针。把 Parent1 和 Child1 对象作为 64 位整数输出,可以看到 p1, p2 的值相同,p3 与前两者不同。这个值也即相应类的虚函数表地址。

Parent1* p1 = new Parent1();
Parent1* p2 = new Parent1();
Parent1* p3 = new Child();
printf("sizeof Parent1: %d, sizeof Child: %dn",
  sizeof(Parent1), sizeof(Child));
printf("val on p1: %lldn", *(int64_t*)p1);
printf("val on p2: %lldn", *(int64_t*)p2);
printf("val on p3: %lldn", *(int64_t*)p3);