1 工具准备
工欲善其事,必先利其器,我们用到的工具是VS。下面介绍如何使用VS查看类内存分布,学会了你也能自己分析。
一、右击项目->属性
二、配置属性->C/C++->命令行
在其他选项这里写上/d1 reportAllClassLayout:可以看到所有相关类的内存布局
写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。
写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。
2 进入正题
一、一个类没有虚函数
class Base { int a; int b; public: void Function() {} };模型是这样的:
可能你看到的是这样的:
看到它了吗?点下去:
这个模型如我们所愿
二、一个类有虚函数
class Base { int a; int b; public: virtual void VirtualFunction1() {} virtual void VirtualFunction2() {} };模型是这样的:
vfptr(虚函数表指针) 放在开头,偏移0
vftable(虚函数表) 下面依次是虚函数,0 1 编号
(第一个0的解释:指向它的 vfptr 在内存中的偏移:0)
vftable(虚函数表) 下面依次是虚函数,0 1 编号
(第一个0的解释:指向它的 vfptr 在内存中的偏移:0)
三、单继承时
class Base { int a; int b; public: virtual void VirtualFunction() {} }; class Derived1 :public Base { int c; public: virtual void VirtualFunction() {} virtual void VirtualMine() {} };父类:
子类:
首先是继承自父类的:虚函数表指针、变量a b -- (base class Base)说明了这是基类Base
其次是自己的:变量c
还继承了父类的 vftable ,重写虚函数后覆盖了父类的虚函数,并在里面添加了自己的虚函数
第一个0表示:指向此 vftable 的指针 vfptr 在内存里面的偏移
下面的 0 1:依次编号
多态实现的理解:
Base * p = new Derived1();
父类指针指向子类变量,当调用 p->VirtualFunction(); 时,即:p->vfptr->VirtualFunction();
既然是父类指针,那么会按照父类内存布局去找vfptr,而C++把它放在0偏移处,就找到了子类的vfptr,最终调用到了子类的虚函数。
其次是自己的:变量c
还继承了父类的 vftable ,重写虚函数后覆盖了父类的虚函数,并在里面添加了自己的虚函数
第一个0表示:指向此 vftable 的指针 vfptr 在内存里面的偏移
下面的 0 1:依次编号
多态实现的理解:
Base * p = new Derived1();
父类指针指向子类变量,当调用 p->VirtualFunction(); 时,即:p->vfptr->VirtualFunction();
既然是父类指针,那么会按照父类内存布局去找vfptr,而C++把它放在0偏移处,就找到了子类的vfptr,最终调用到了子类的虚函数。
子类不重写虚函数:
不重写虚函数,vftable 里面就没有覆盖父类虚函数,这时候调用这个函数,就是父类的虚函数
四、多继承时(菱形继承)
class Base { int a; int b; public: virtual void VirtualFunction() {} }; class Derived1 :public Base { int c; public: virtual void VirtualFunction() {} }; class Derived2 :public Base { int d; public: virtual void VirtualFunction() {} }; class LastDerived :public Derived1, public Derived2 { int e; public: virtual void VirtualFunction() {} };Derived1:
内存情况和上面单继承一样的
Derived2:
同Derived1
LastDerived:
首先放的是继承自第一个父类 Derived1的:里面放了 Derived1 的所有继承下来的东西,包括 vfptr
首先放的是继承自第一个父类 Derived2的:里面放了 Derived2 的所有继承下来的东西,包括 vfptr
最后放的自己的变量e
首先放的是继承自第一个父类 Derived2的:里面放了 Derived2 的所有继承下来的东西,包括 vfptr
最后放的自己的变量e
两个 vftable :
一个是从Derived1继承下来的;
一个是从Derived2继承下来的,-16表示指向此虚函数表的指针vfptr在内存里面的偏移。
总的来说:继承了所有父类的东西,连基类的内容也继承了两套,冗余。
五、虚继承时
class Base { int a; int b; public: virtual void VirtualFunction() {} }; class Derived1 :virtual public Base { int c; public: virtual void VirtualFunction() {} }; class Derived2 :virtual public Base { int d; public: virtual void VirtualFunction() {} }; class LastDerived :public Derived1, public Derived2 { int e; public: virtual void VirtualFunction() {} };Derived1:虚继承自 Base
首先放的不是父类的内容了
首先放的是自己的: vbptr(虚基类指针,指向一个偏移量表 vbtable) 、变量c
其次放的是继承自父类的: vfptr、 变量a b
vbtable(虚基类表):
存放是基类的 vfptr 相对于 vbptr 的偏移
vftable(虚函数表):
首先是指向自己的 vfptr 在内存中的偏移
其次是虚函数地址
首先放的是自己的: vbptr(虚基类指针,指向一个偏移量表 vbtable) 、变量c
其次放的是继承自父类的: vfptr、 变量a b
vbtable(虚基类表):
存放是基类的 vfptr 相对于 vbptr 的偏移
vftable(虚函数表):
首先是指向自己的 vfptr 在内存中的偏移
其次是虚函数地址
扩展:若此时,再写一个新的虚函数,它不是虚继承自基类的虚函数,会再为 Derived1 生成一个虚函数表,它有了另一个身份:新的虚基类
可以看出,除了上面分析的几个指针、表外,又多了一个 vfptr 放在开始,下面也多了一个虚函数表,它又多了一个身份
由于有两个 vftable ,在后面用@Derived@、@Base@指明了
由于有两个 vftable ,在后面用@Derived@、@Base@指明了
Derived2:虚继承自 Base
和Derived1是一样的
LastDerived:实继承自Derived1 Derived2
可以看到的是,和非继承一样,先放父类,再放子类。只是父类基类的部分被剥离放在了最下面:
首先放的继承自第一个父类 Derived1 的 :vbptr 、变量c (由于Derived是虚继承自Base,所以先放的是vbptr,参照上一步 )
其次放的继承自第二个父类 Derived2 的: vbptr 、变量d
然后是自己的变量e
最后存放了继承自基类(与非虚继承不同是时,非虚继承是把基类部分放在父类继承来的内存里面)的:vfptr 、变量a b
Derived1 的 vbtable:
存放是基类的 vfptr 相对于 vbptr 的偏移:20 (20 - 0)
Derived2 的 vbtable:
存放是基类的 vfptr 相对于 vbptr 的偏移:12 (20 - 8)
vftable:
-20表示 :指向自己的 vfptr 在内存中的偏移,并且这个类已经重写了基类的虚函数
可以总结的是:
虚继承的作用是减少了对基类的重复,仅一份放在最后,代价是增加了 vbptr(虚基类指针)、vbtable(虚基类表)的负担
其次放的继承自第二个父类 Derived2 的: vbptr 、变量d
然后是自己的变量e
最后存放了继承自基类(与非虚继承不同是时,非虚继承是把基类部分放在父类继承来的内存里面)的:vfptr 、变量a b
Derived1 的 vbtable:
存放是基类的 vfptr 相对于 vbptr 的偏移:20 (20 - 0)
Derived2 的 vbtable:
存放是基类的 vfptr 相对于 vbptr 的偏移:12 (20 - 8)
vftable:
-20表示 :指向自己的 vfptr 在内存中的偏移,并且这个类已经重写了基类的虚函数
可以总结的是:
虚继承的作用是减少了对基类的重复,仅一份放在最后,代价是增加了 vbptr(虚基类指针)、vbtable(虚基类表)的负担
六、总结(当基类有虚函数时)
1. 每个类都有虚指针和虚表。
2. 如果不是虚继承,那么子类将父类的虚函数指针继承下来,并指向自身的虚函数表(发生在对象构造时)。有多少个虚函数,虚函数表里面的项就会有多少。
多重继承时,可能存在多个的基类虚表与虚指针。
3. 如果是虚继承,那么子类除了有虚函数指针、虚函数表外,还有一根虚基类指针 vbptr 指向一个虚基类表 vbtable(存放 vfptr 相对于 vbptr 的偏移,用来寻找基类 vfptr )。
多重继承时虚函数指针、虚函数表有且只有一份。
4. 非虚继承时:都是先放父类继承下来的东西,再放自己的东西。
虚继承时:先放自己的 vbptr (虚继承特有)、自己的东西,再放父类的东西。
2. 如果不是虚继承,那么子类将父类的虚函数指针继承下来,并指向自身的虚函数表(发生在对象构造时)。有多少个虚函数,虚函数表里面的项就会有多少。
多重继承时,可能存在多个的基类虚表与虚指针。
3. 如果是虚继承,那么子类除了有虚函数指针、虚函数表外,还有一根虚基类指针 vbptr 指向一个虚基类表 vbtable(存放 vfptr 相对于 vbptr 的偏移,用来寻找基类 vfptr )。
多重继承时虚函数指针、虚函数表有且只有一份。
4. 非虚继承时:都是先放父类继承下来的东西,再放自己的东西。
虚继承时:先放自己的 vbptr (虚继承特有)、自己的东西,再放父类的东西。