C++虚函数表解析

概述

为了实现C++的多态,C++使用了一种动态绑定技术,该技术的核心就是虚函数表。
多态即使用同一段代码可以实现不同的行为;动态绑定即使用静态类型是基类的指针或引用调用虚函数,根据其动态类型的不同会导致调用不同版本(基类版本或派生类版本)的虚函数从而产生不同的行为。
下面剖析一下其中的奥秘。

虚函数表

每个包含虚函数的类中都存储着一个虚函数表,这是一个指针数组,每个元素对应一个指向虚函数的地址的指针。虚函数表是属于类的,而不是某个具体的对象,一个类的所有对象都使用这个虚函数表(类似于类的静态成员属于类而不是对象,但都可以共享)。为了达成此目的,每个对象在创建时内部保存着一个指向它的类的虚函数表的指针*_vptr。

假设有一个这样的类;

1
2
3
4
5
6
7
8
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }

virtual void g() { cout << "Base::g" << endl; }

virtual void h() { cout << "Base::h" << endl; }
};

此时创建给一个Base的对象b,则b的内部具体实现:

下面看下在一般继承下虚函数表是什么样子的。
假设存在一个继承关系:

1
2
3
4
5
6
7
8
class Derived : public Base{
public:
virtual void f1() { cout << "Derived::f1" << endl; }

virtual void g1() { cout << "Derived::g1" << endl; }

virtual void h1() { cout << "Derived::h1" << endl; }
}

在这个继承关系中,派生类没有覆盖基类的任何函数,那么它的虚函数表为:

可以看出:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

如果派生类覆盖了基类的虚函数实现,比如这个继承关系:

1
2
3
4
5
6
7
8
class Derived : public Base{
public:
virtual void f() { cout << "Derived::f1" << endl; }

virtual void g1() { cout << "Derived::g1" << endl; }

virtual void h1() { cout << "Derived::h1" << endl; }
}

那么该派生类的内部实现就是这样子的:

可以看出
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样一来,对于这样的代码:

1
2
Base *b = new Derive();
b.f();

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

在多重继承中,假设这样的一组继承关系:

如果派生类没有覆盖基类的虚函数,那么子派生类的虚函数表为:

可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

如果多重继承有虚函数覆盖的情况:

则派生类的虚函数表是这样的:

可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。

1
2
3
4
5
6
7
8
9
10
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1.f(); //Derive::f()
b2.f(); //Derive::f()
b3.f(); //Derive::f()
b1.g(); //Base1::g()
b2.g(); //Base2::g()
b3.g(); //Base3::g()


参考博客:
blog1
blog2