C++虚继承实现原理(虚基类表指针与虚基类表)
虚继承和虚函数是完全⽆相关的两个概念。
虚继承是解决C++多重继承问题的⼀种⼿段,从不同途径继承来的同⼀基类,会在⼦类中存在多份拷贝。这将存在两个问题:其⼀,浪费存储空间;第⼆,存在⼆义性问题,通常可以将派⽣类对象的地址赋值给基类对象,实现的具体⽅式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在⼀个基类的多份拷贝,这就出现了⼆义性。
虚继承可以解决多种继承前⾯提到的两个问题:
虚继承底层实现原理与编译器相关,⼀般通过虚基类指针和虚基类表实现,每个虚继承的⼦类都有⼀个虚基类指针(占⽤⼀个指针的存储空间,4字节)和虚基类表(不占⽤类对象的存储空间)(需要强调的是,虚基类依旧会在⼦类⾥⾯存在拷贝,只是仅仅最多存在⼀份⽽已,并不是不在⼦类⾥⾯了);当虚继承的⼦类被当做⽗类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了⼀个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就到了虚基类成员,⽽虚继承也不⽤像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
在这⾥我们可以对⽐虚函数的实现原理:他们有相似之处,都利⽤了虚指针(均占⽤类的存储空间)和虚表(均不占⽤类的存储空间)。
虚基类依旧存在继承类中,只占⽤存储空间;虚函数不占⽤存储空间。
虚基类表存储的是虚基类相对直接继承类的偏移;⽽虚函数表存储的是虚函数地址。
此篇博客有关于虚继承详细的内存分布情况
blog.csdn/xiejingfa/article/details/48028491
补充:
1、D继承了B,C也就继承了两个虚基类指针
2、虚基类表存储的是,虚基类相对直接继承类的偏移(D并⾮是虚基类的直接继承类,B,C才是)
#include<iostream>
using namespace std;
class A //⼤⼩为4
{
public:
int a;
};
class B :virtual public A //⼤⼩为12,变量a,b共8字节,虚基类表指针4
{
public:
int b;
};
class C :virtual public A //与B⼀样12
{
public:
int c;
};
class D :public B, public C //24,变量a,b,c,d共16,B的虚基类指针4,C的虚基类指针
{
public:
int d;
};
int main()
{
A a;
B b;
C c;
D d;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
cout << sizeof(d) << endl;
system("pause");
return 0;
}
⼆:从内存布局看C++虚继承的实现原理
准备⼯作
1、VS2012使⽤命令⾏选项查看对象的内存布局
微软的Visual Studio提供给⽤户显⽰C++对象在内存中的布局的选项:/d1reportSingleClassLayout。使⽤⽅法很简单,直接在[⼯具(T)]选项下
到“Visual Studio命令提⽰(C)”后点击即可。切换到cpp⽂件所在⽬录下输⼊如下的命令即可
c1 [filename].cpp /d1reportSingleClassLayout[className]
其中[filename].cpp就是我们想要查看的class所在的cpp⽂件,[className]指我们想要查看的class的类名。(下⾯举例说明...)
2、查看普通多继承⼦类的内存布局
既然我们今天讲的是虚基类和虚继承,我们就先⽤上⾯介绍的命令提⽰⼯具查看⼀下普通多继承⼦类的内存布局,可以跟后⽂虚继承⼦类的内存布局情况加以⽐较。
我们新建⼀个名叫NormalInheritance的cpp⽂件,输⼊⼀下内容。
/**
普通继承(没有使⽤虚基类)
*/
// 基类A
class A
{
public:
int dataA;
};
class B : public A
{
public:
int dataB;
};
class C : public A
{
public:
int dataC;
};
class D : public B, public C
{
public:
int dataD;
};
上⾯是⼀个简单的多继承例⼦,我们启动Visual Studio命令提⽰功能,切换到NormalInheritance.cpp⽂件所在⽬录,输⼊⼀下命令:
c1 NormalInheritance.cpp /d1reportSingleClassLayoutD
我们可以看到class D的内存布局如下:
从类D的内存布局可以看到A派⽣出B和C,B和C中分别包含A的成员。再由B和C派⽣出D,此时D包含了B和C的成员。这样D中就总共出现了2个A成员。⼤家注意到左边的⼏个数字,这⼏个数字表明了D中各成员在D中排列的起始地址,D中的五个成员变量(B::dataA、dataB、C::dataA、dataC、dataD)各占⽤4个字节,sizeof(D) = 20。
为了跟后⽂加以⽐较,我们再来看看B和C的内存布局:
虚继承的内存分布情况
上⾯我们看到了普通多继承⼦类的内存分布情况,下⾯我们进⼊主题,来看看典型的菱形虚继承⼦类的内存分布情况。
我们新建⼀个名叫VirtualInheritance的cpp⽂件,输⼊⼀下内容:
/**
虚继承(虚基类)
*/
#include <iostream>
// 基类A
class A
{
public:sizeof 指针
int dataA;
};
class B : virtual public A
{
public:
int dataB;
};
class C : virtual public A
{
public:
int dataC;
};
class D : public B, public C
{
public:
int dataD;
};
VirtualInheritance.cpp和NormalInheritance.cpp的不同点在与C和C继承A时使⽤了virtual关键字,也就是虚继承。同样,我们看看B、C、D类的内存布局情况:
我们可以看到,菱形继承体系中的⼦类在内存布局上和普通多继承体系中的⼦类类有很⼤的不⼀样。对于类B和C,sizeof的值变成了12,除了包含类A的成员变量dataA外还多了⼀个指针vbptr,类D除了继承B、C各⾃的成员变量dataB、dataA和⾃⼰的成员变量外,还有两个分别属于B、C的指针。
那么类D对象的内存布局就变成如下的样⼦:
vbptr:继承⾃⽗类B中的指针
int dataB:继承⾃⽗类B的成员变量
vbptr:继承⾃⽗类C的指针
int dataC:继承⾃⽗类C的成员变量
int dataD:D⾃⼰的成员变量
int A:继承⾃⽗类A的成员变量
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论