学习导航
一、多态实现原理
在讲解多态的实现原理之前,我们首先要理解两个概念:
- 编译时决议:编译时确定调用函数的地址
- 运行时决议:运行时确定调用函数的地址
不难理解,多态的实现一定是运行时决议的结果,否则在编译时就会根据指针自身的类型确定调用的虚函数(例如使用基类的指针就一定会调用基类的函数,从而无法实现多态)。
通过调用监视窗口,我们不难发现,所有包含虚函数的对象都会带有一个名为vfptr
的指针变量,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function) 。
虚函数指针指向虚表,而虚表中存放着虚函数的地址。C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)说了这么多大家是不是被绕晕了,那我为大家捋一捋多态实现的基本思路:
如果虚函数的调用满足多态的两个构成条件,编译器就会“死板”的进行运行时决议,即在程序运行时从虚函数表中找到对应的虚函数地址,从而实现相应函数的调用。
二、不同情况下的虚函数表
(1)单继承无虚函数覆盖
(2)单继承有虚函数覆盖
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
我们回过头来看看重写的字面意思实际上:重写强调的是语法层的概念,表示派生类对基类虚函数的具体实现进行了重写;而覆盖是原理层的概念,表示子类拷贝了父类的虚表并进行修改——覆盖了构成重写的函数。
说到这,想必聪明的你已经理解了上面两张图中虚表的区别。(3)多继承无虚函数覆盖
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)(4)多继承有虚函数覆盖
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
重复继承和菱形序继承更加复杂,有兴趣的小伙伴可以参考大佬的文章:
C++ 对象的内存布局
三、对虚函数指针与虚函数表的深入理解
(1)如何取到虚函数指针?
我们之前提到,虚函数指针位于对象实例的最前面——32位平台为前4个字节,64位平台为前8个字节。我们可以用下面的代码取到虚函数指针,并打印虚函数列表:
(注意以下代码是在32位平台下运行的)
class Base{ public: virtual void f1(){cout << "Base::f1()" << endl;} }; class Derive: public Base{ public: virtual void f1(){cout << "Derive::f1()" << endl;} virtual void f2(){cout << "Derive::f2()" << endl;} }; typedef void(*V_Fun)(); void printVFTable(V_Fun* vfp) { for (int i = 0; i < 2; i++) { vfp[i]();} //(1) } int main() { Derive d; printVFTable((V_Fun*)(*(int*)&d)); // (2) return 0; }
[评注]:
因为VS下虚表是以空指针结束的,所以也可用它作为遍历结束的条件。如果程序崩溃,重新生成解决方法即可。
获得对象前4字节并强转为函数指针
所有的虚函数都会进入虚函数表,但监视经过优化看到的并不准确。
(2)为什么不能支持对象直接调用虚函数?
将子类对象赋值给父类对象的时候,并不会将子类的虚函数指针拷贝过去。如果允许拷贝,那么父类中存放的是到底父类虚表指针还是子类虚表指针就分不清楚,从而产生混乱。
因此,将子类对象赋值给父类对象后,父类对象的虚函数指针仍然指向父类对象中的虚函数,无法实现多态。
但是,使用指针或者引用就没有这样的顾虑,因为给指针赋值时不存在拷贝的问题。
(3)虚函数存在哪的?虚表存在哪的?
我们要捋清下面几个概念:
- 虚表存的是虚函数指针,不是虚函数
- 虚函数和普通函数一样的,都是存放在代码段中,只是他的指针又存到了虚表中
- 对象中存的不是虚表,存的是虚表指针,虚表存放在代码段上
验证虚表存在代码段上有这样一个思路: