虚函数赋予了 C++ 运行时多态的能力(也就是动态绑定)

最重要的结论:

  • 虚表是的虚表;每个类都有独立的虚表
  • 虚表是编译时生成的
  • 如果自身或者父类有虚函数,那么这个类的对象就会多出一个字段:虚指针,指向这个类的虚表

如何查看带有虚表的内存布局:

  • g++: g++ -fdump-class-hierarchy -c main.cpp
  • clang: clang -cc1 -fdump-record-layouts main.cpp

几个例子

注:

  • 不考虑内存对齐的 align;
  • 64 位环境,所以指针是 8 字节

1. 有虚函数才有虚函数表

这样一个类:

  1. struct A {
  2. int a;
  3. virtual void foo() {}
  4. };

其内存布局为:

  1. 0 struct A
  2. 0 |__ (A vtable pointer)
  3. 8 |__ int a

2. 父类有虚函数,子类无虚函数

父类有虚函数,子类没有:

  1. struct A {
  2. int a;
  3. virtual void foo() {}
  4. };
  5. struct B : public A {
  6. int b;
  7. };

这种情况,B 的内存布局为:

  1. 0 struct B
  2. 0 |__ struct A
  3. 0 | |__ (A vtable pointer)
  4. 8 | |__ int a
  5. 12 |__ int b

3. 父类的子类都有虚函数,且虚函数不同

  1. struct A {
  2. int a;
  3. virtual void foo() {}
  4. };
  5. struct B : public A {
  6. int b;
  7. virtual void bar() {}
  8. };

B 的内存布局为:

  1. 0 struct B
  2. 0 |__ struct A
  3. 0 | |__ (A vtable pointer)
  4. 8 | |__ int a
  5. 12 |__ int b

4. 父类的子类都有虚函数,且虚函数相同

  1. struct A {
  2. int a;
  3. virtual void foo() {}
  4. };
  5. struct B : public A {
  6. int b;
  7. virtual void foo() {}
  8. };

B 的内存布局为:

  1. 0 struct B
  2. 0 |__ struct A
  3. 0 | |__ (A vtable pointer)
  4. 8 | |__ int a
  5. 12 |__ int b

5. 结论

  • 当前类存在虚函数,或者父类存在虚函数,那么对象中就会存在虚指针
  • 每个类都有独立的虚表,即使类之间存在继承关系,也不会共用虚表。
  • 虚表是编译时期生成的。

一个多继承的例子

注:这个例子是在 windows 的 visual studio 下得出的,32 位环境,所以指针大小是 4 字节

存在如下的类:
image.png

通过 visual studio 的 debugger,查看每个对象的内存布局:
image.png
image.png

可以看到:

  • 每个类都有自己独立的虚表
  • Derive1 虚表的前半部分和 Base1 的虚表是形式相同的;类 Derive1 虚表的后半部分和 Base2 的虚表是形式相同的;这让动态绑定得以实现

虚表如何实现动态绑定?

  • 情形:将子类指针转换为父类指针,调用某个方法,这个方法是一个虚方法,且父类和子类都有自己的实现
  • 根据子类的虚指针,找到子类的虚表,找到要调用的函数在虚表中的位置,调用
  • 注:为了实现动态绑定,父类虚表和子类虚表在相同位置的函数名称是相同的,但是实现不同

虚函数的应用

1. 析构函数是虚函数

一般情况下,析构派生类时,会先调用派生类的析构函数,再调用基类的析构函数。

但如果用基类指针指向派生类,然后delete这个指针,只会调用基类的析构函数。

在这种情况下如何正确地析构呢?可以将基类的析构函数定义为虚函数。