多态与动态绑定 链接到标题
为了实现 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 的虚表如图所示
虚表是一个指针数组,其元素是虚函数的指针,数组中的每个元素对应一个虚函数的指针。普通的函数(即非虚函数),其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚函数指针的赋值发生在编译器的编译阶段,也就是在编译阶段,虚表就被构建出来了。
虚表指针 链接到标题
虚表是属于类的(有点像静态成员变量),而不属于某个具体的对象,一个类只需要一个虚表即可,同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr
,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
一个子类的父类如果包含虚函数,那么这个子类也拥有自己的虚表,所以这个子类的对象也包含一个虚表指针,用来指向它的虚表。
类 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;
; - 调用的是虚函数;
参考资料 链接到标题
- 《Cpp Primer》第五版
- C++ 虚函数表剖析