C++是如何实现多态的
结论:C++通过虚函数来实现多态的,根本原因是派生类和基类的虚函数表的不同。
构成多态的必要条件有如下3点:
- 存在继承关系
- 基类存在虚函数,且派生类有相同原型的函数遮蔽它
- 存在基类类型的指针指向派生类对象,且该指针调用了存在遮蔽关系的虚函数
如下图,就是一个简单的多态的例子:(实验环境:vs2019)
大猪眉头一皱,觉得事情并不简单。
针对上述实验结果,有个疑点:
- 基类指针是如何知道要调用派生类的f函数,而不是基类的f函数的呢?
这其中的奥秘,还是要从内存模型开始探索。谈到内存模型,那么我们先探讨最简单的——这个对象有多大吧(或者说这个对象占几个字节的内存吧)。
如上图所示,A的大小在32位编译模式下是4字节,在64位编译模式下是8字节,由此可见,A内存模型里面肯定有个指针!(原因:https://blog.csdn.net/qq_18138105/article/details/105209406)
那么继续深入分析对象的内存模型,这个指针到底指向什么呢?
实际上,这个指针指向一个数组,数组中的每个元素都是虚函数的入口地址,这个数组也就是虚函数表(存在于C++内存模型中的常量区)!由于虚函数表和对象在内存上是分开存储的(虚函数在C++内存模型中的代码区,对象在C++内存模型中的堆区,指向对象的指针在C++内存模型的栈区),因此,就需要在对象中需要安插一个指向这个虚函数表的指针!
我们再看一个例子:
各个对象的内存模型如下(我用cocos creator画的):
如上图所示,对象内存和虚函数表内存分开的,对象的*vfptr指向对应的虚函数表。(注意:和很多博客画的图不同,为了直观!我是把处于内存高地址的放上面,处于内存低地址的放下面)
仔细观察派生类B的虚函数表:
- 派生类如果存在对基类有遮蔽关系的虚函数,则在虚函数表中则取派生类的这个虚函数的入口地址,如&B::f
- 对于未被派生类遮蔽的基类的虚函数,派生类的虚函数表则取基类的这个虚函数的入口地址,如&A::g
- 派生类新增的虚函数,依次往虚函数表后面加,如&B::h
因此,我们通过指针调用虚函数时,先根据指针找到对象里的vfptr来定位到虚函数表,然后通过虚函数在虚函数表中的索引值来得到虚函数的入口地址。
比如 a->f(), 实际上编译器会这么处理 (*(*(a+0)+0))(a)
- a+0 是 a对象 vfptr 的地址
- *(a+0)是 vfptr的值, 又 vfptr 是指向虚函数表的指针,因此 *(a+0) 也是虚函数表的首地址
- 由于 A::f 函数在虚函数表中的索引是0,因此 (*(a+0)+0)就是获取 A::f 函数的入口地址
- 知道了A::f 函数的地址,*(*(a+0)+0) 就是对 A::f 的调用
- 把a对象的指针传入 A::f, 就是a作为 A::f 的 this指针!
同理,调用 b->f() ,也是一样的,只不过访问的是 B的虚函数表,最后调用的是 B::f, 而不是 A::f, 这就解释了 “基类指针是如何知道要调用派生类的f函数,而不是基类的f函数” 的问题,就是因为虚函数表的不同。
疑问虽然已经解决了,但是我们还是要继续细探究竟!经过下面的实验,得出的 结果和内存模型完全相符!
#include <iostream>
using namespace std;class A {
public:A() : a(100) {}virtual void f() {cout << "A::f" << endl;}virtual void g() {cout << "A::g" << endl;}
protected:int a;
};class B : public A {
public:B() : b(50) {}virtual void f() {cout << "B::f" << endl;}virtual void h() {cout << "B::h" << endl;}
protected:int b;
};// 定义一个 参数为 A*类型 返回值是 void 的 函数指针 Fun
typedef void (*Fun)(A*);// 指针值类型(64位编译模式下是long long, 32位编译模式下是int)
#ifdef _WIN64
#define ptr_value long long
#else
#define ptr_value int
#endif
// 根据对象指针和偏移量 获取 指针值类型的指针
#define ptr(obj, offset) ((ptr_value*)obj+offset)int main() {A* a = new A;A* b = new B;// 接下来探究 a对象 和 b对象 的 内存模型 //ptr_value a_vfptr_value = *ptr(a, 0); // a_vfptr的值 即 b的虚函数表的首地址((Fun)*ptr(a_vfptr_value, 0))(a); // A::f((Fun)*ptr(a_vfptr_value, 1))(a); // A::g ptr_value a_a = *ptr(a, 1); // a对象 的 int a成员cout << (int)a_a << endl; // 100ptr_value b_vfptr_value = *ptr(b, 0); // b_vfptr的值 即 b的虚函数表的首地址((Fun)*ptr(b_vfptr_value, 0))(b); // B::f((Fun)*ptr(b_vfptr_value, 1))(b); // A::g ((Fun)*ptr(b_vfptr_value, 2))(b); // B::hptr_value b_a = *ptr(b, 1); // b对象 的 int a成员cout << (int)b_a << endl; // 100ptr_value b_b = *ptr(b, 2); // b对象 的 int b成员cout << (int)b_b << endl; // 50return 0;
}
因此,只要理解了虚函数表,C++的多态自然就迎刃而解了。