C++虚表&多态的实现原理分析
作者:爱上小公举
C++虚表&多态实现原理
这里我们只看继承中的多态 ,本文程序调试在VS2017
先看一个小问题,下面的类A实例化出的对象占几个字节呢?
#include<iostream> using namespace std; class A { int m_a; public: void func(){ cout << "类A的func" << endl; } }; int main() { A a; cout << sizeof(a) << endl; system("pause"); return 0; }
答案是4字节,这是因为成员函数存放在公共的代码段, 所以只计算成员变量m_a所占字节的大小
调试一下来看,
我们如果将成员函数定义成虚函数又会如何呢?
我们来看:
#include<iostream> using namespace std; class A { int m_a; public: virtual void func(){ cout << "类A的func" << endl; } virtual int func1() { cout << "类A的func1" << endl; return 0; } }; int main() { A a,b; cout << sizeof(A) << endl; system("pause"); return 0; }
可以看到结果是8字节,emm.... 事出反常必有妖,我们调试一下,看看到底多出来了个什么东西
我们可以看到,在实例化出的对象a和b中多了一个_vfptr,它的类型时void**,是一个二级指针,指针在32位平台中占4字节,所以这里的结果是8(m_a的4字节+_vfptr的4字节),那么_vfptr到底是个什么东西?
类中有了虚函数之后才有了_vfptr,它们之间到底有着什么关系?
其实,_vfptr,其实就是虚函数指针 (virtual function pointer)
可以看到_vfptr 指向了一个 vftable(virtual function table) 虚函数表(也叫虚表),虚表中元素是void*类型 ,第一个元素是指向了虚函数func(),第二个元素指向了fun1()
虚函数指针(_vfptr) 和 虚函数表(vftable)
虚函数指针和虚表是什么
通过上面的调试,我们已经看到了,_vfptr是指向虚表的一个指针,那么我们也可以叫 _vfptr 为虚函数表指针
- 当一个类中有虚函数时,编译期间,就会为这个类分配一片连续的内存 (这就是虚表vftable),来存放虚函数的地址,类中只保存着
- 指向虚表的指针 (也就是虚函数表指针_vfptr) ,(虚函数其实和普通函数一样,存放在代码段) ,当这个类实例出对象时,每个对象
- 都会有一个虚函数表指针_vfptr (VS中虚表内存分配在代码段)
虚表本质上是一个在编译时就已经确定好了的void* 类型的指针数组 .
注意 : 虚函数表为了标志结尾,会在虚表最后一个元素位置保存一个空指针.所以看到的虚表元素个数比实际虚函数个数多一个
C++中的虚函数的实现一般是通过虚函数表 (C++规范并没有规定具体用哪种方法,但大部分的编译器都用虚函数表的方法) 大多数编译器(如本文用的VS)中虚函数表指针都在对象的最前面位置,意味着能通过对象的地址就能遍历虚函数表(能够在多层继承或多重继承中保持较高性能)
虚函数是为了继承时的多态才有的概念,上面简单了解了一下虚表,我们再来看继承关系中的虚表
继承中的虚表
在有虚函数的类(有虚表的类)被继承后, 虚表也会被拷贝给派生类. 注意,编译器会给派生类新分配一片空间来拷贝基类的虚表,将这个虚表的指针给派生类, 而并不是沿用基类的虚表,在发生虚函数的重写时,重写的是派生类为了拷贝基类虚表新创建的这虚表中的虚函数地址
虚表为所有这个类的对象所共享. 注意,是通过给每个对象一个虚表指针_vfptr共享到的虚表.
单继承中的虚表
1. 单继承未重写虚函数: 会继承派生类的虚表,如果派生类中新增了虚函数,则会加继承的虚表后面
2. 单继承重写虚函数: 继承的虚表中被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址(如下面例子中把A::func()修改成了B::func()的地址)(注意: 此时基类的虚表并没有被修改,修改的是派生类自己的虚表)
所以, 重写实际上就是在继承基类虚表时,把基类的虚函数地址修改为派生类虚函数的地址
举个栗子
#include<iostream> using namespace std; class A { int m_a; public: virtual void func(){ cout << "类A的func" << endl; } virtual int func1() { cout << "类A的func1" << endl; return 0; } }; class B :public A { public: virtual void func() { cout << "类B的func" << endl; } virtual void func2() { cout << "类B的func2" << endl; } }; int main() { A a1; A a2; B b; system("pause"); return 0; }
调试如下:
可以看到对象b中的_vfptr所指向的虚表继承了类A的虚表 ,但是地址却和a1,a2的_vfptr不一样,也印证了前面所说,新分配了一片空间来拷贝基类的虚表.
还可以看到a1和a2的虚表地址相同,也印证了前面所说,类的虚表被所有对象所共享.
但我们却发现,B中也有虚函数,怎么没有了,讲道理这是不科学的,那么B类的虚函数的地址放到底哪去了呢?
思考一下,它肯定是存在的,要么另外建一张虚表放里面,要么放在继承A的虚表里.
实际上,在单继承中,派生类的虚函数地址会放在继承来的基类的虚表后面,只是VS这里没有显示出来.
我们也可以看到图中红色圈出来的vftable[4],虚表中也已经有了四个元素,既然VS不给力,只能自己想办法,可以在监视窗口,通过地址看到,B类中的虚函数指针指向的虚表,其实是这样的,如下
还可以看到,继承的A中的虚函数func()被重写之后,虚表中就放的是重写后的B中的func()的地址
多继承中的虚表
- 多继承不重写虚函数: 继承的多个基类中有多张虚表,派生类会全部拷贝下来,成为派生类的多张虚表,如果派生类有新的虚函数,会加在派生类拷贝的第一张虚表的后面(拷贝的第一张虚表是继承的第一个有虚函数(或虚表)的基类的)
- 多继承重写虚函数 : 规则与 不重写虚函数 相同,但需要注意的是,如果多个基类中含有相同的虚函数,例如func. 当派生类重写func这个虚函数后,所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表,并不是基类自己的虚表)
举个栗子
#include<iostream> using namespace std; class A { int m_a; public: virtual void funcA() { cout << "类A的funcA" << endl; } virtual void func() { cout << "类A的func" << endl; } }; class B { public: virtual void funcB() { cout << "类B的funcB" << endl; } virtual void func() { cout << "类B的func" << endl; } }; class C :public A,public B { public: virtual void func() { cout << "类C的func" << endl; } virtual void funcC() { cout << "类C的funcC" << endl; } }; int main() { C c; A a; system("pause"); return 0; }
调试如下 :
可以看到,派生类继承了两张虚表,A::func()和B::func()的地址修改为了C:::func()的地址
我们再次通过_vfptr的地址,在监视窗口可以看到,派生类中新增的虚函数,虚函数地址被加在了派生类拷贝基类的第一张虚表的后面.
多态的原理
我们回忆一下多态的两个构成条件
- 1.通过指向派生类对象的基类的指针或引用调用虚函数
- 2. 被调用的函数必须是被派生类重写过的虚函数
简单来说就是,利用了虚函数可以重写的特性,当一个有虚函数的基类有多个派生类时,通过各个派生类对基类虚函数的不同重写,实现通过指向派生类对象的基类指针或基类引用调用同一个虚函数,去实现不同功能的特性. 抽象来说就是,为了完成某个行为,不同的对象去完成时会产生多种不同的状态
总结
- 一个有虚函数的类,它实例出的所有对象通过虚表指针vfptr共享类的虚表
- 对象中存放的是虚函数(表)指针vfptr,不是虚表. vfptr是虚表的首地址,指向虚表
- 虚表中存放的时虚函数地址,不是虚函数,虚函数和普通函数一样,存放在代码段
- 虚表是在编译阶段生成的,一般分配在在代码段(常量区),例如VS中
派生类的虚表生成:
- a.先将基类中的虚表内容拷贝一份到派生类虚表中
- b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- d.如是多继承,则派生类新增加的虚函数地址添加在派生类拷贝(继承)的第一张虚表后面
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。