多态与动态绑定 链接到标题

为了实现 C++ 的多态,C++ 使用了动态绑定技术,该技术的核心是虚函数表(简称虚表)。

类的虚函数表 链接到标题

每个包含了虚函数的类都包含一个虚表,一个子类如果继承了包含虚函数的父类,那么这个类也拥有自己的虚表,例如

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int data1_, data2_;
};
class B : public A {
public:
    void vfunc1() override;
    void func1();
};
class C : public B {
public:
    void vfunc2() override;
    void func2();
private:
    int data1_, data2_;
};

A 包含虚函数 vfunc1() ,B 继承自 A,A 的虚表如图所示 UMqKwRv6nlpISct

虚表是一个指针数组,其元素是虚函数的指针,数组中的每个元素对应一个虚函数的指针。普通的函数(即非虚函数),其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。

虚函数指针的赋值发生在编译器的编译阶段,也就是在编译阶段,虚表就被构建出来了。

虚表指针 链接到标题

虚表是属于类的(有点像静态成员变量),而不属于某个具体的对象,一个类只需要一个虚表即可,同一个类的所有对象都使用同一个虚表。

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

一个子类的父类如果包含虚函数,那么这个子类也拥有自己的虚表,所以这个子类的对象也包含一个虚表指针,用来指向它的虚表。 2IiUQ9NMtksgCpv

类 A 包括两个虚函数,故 A 的虚函数表包含两个指针,分别指向 A::vfunc1()A::vfunc2()

类 B 继承于类 A,故类 B 可以调用类 A 的函数,但由于类 B 重写了 B::vfunc1() 函数,故 B 的虚函数表的两个指针分别指向 B::vfunc1()A::vfunc2()

类 C 继承于类 B,故类 C 可以调用类 B 的函数,但由于类 C 重写了 C::vfunc2() 函数,故C 的虚表的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和 C::vfunc2()

对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数

动态绑定 链接到标题

C++ 的动态绑定即是通过虚表虚表指针来实现的。

承接上面的代码,假设我们实例化子类 B 的对象bObj,并声明基类 A 的指针 p,然后让 p 指向 bObj,即 A *p = &bObj;,那么,当我们使用 p 来调用 vfunc1() 时,由于 __vptr 也是基类的一部分,因此 p->__vptr 的值实际上是 &bOjb->__ptr,即 p->__vptr 指向了类 B 的虚函数表,因此 p->vfunc1() 实际上调用了 B::vfunc1()

因此,我们只要注意虚表指针的值,即可弄清楚到底调用了哪个类的函数。

我们把虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态,传统的函数调用我们称为静态绑定(函数调用在编译阶段就能确定下来了)。

执行函数的动态绑定需要符合以下三个条件:

  • 通过指针调用函数;
  • 指针发生从子类向父类的转换,例如 A *p = &bObj;
  • 调用的是虚函数;

参考资料 链接到标题