在很多面试或口试题中经常涉及到类,而这其中又不得不说类的继承,虚继承,虚函数等问题,所以涉及到了类的内存布局,其中关于虚拟继承(virtual public)这个话题比较难以懂得,而且不同的编译器环境可能实现的类的内存布局不同,所以本文仅在vs2013编译环境下调试,如果你在像gcc这样的编译器中调试结果会不同(gcc中会把虚基指针和虚函数指针合并)。
首先我们要区分虚函数和虚拟继承:
虚拟继承和虚函数是完全无相关的两个概念。
虚函数实现原理
每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将基类指针绑定到实例化的对象实现多态。
虚拟继承实现原理
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
对比
1、虚继承对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。
2、虚基类依旧存在继承类中,占用存储空间;虚函数不占用存储空间。
3、虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。
测试
准备工作
VS2013使用命令行选项查看对象的内存布局
格式:
[filename].cpp /d1 reportSingleClassLayout[className]
测试代码GitHub地址
测试一:单个虚继承,不带虚函数
#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;
//测试一:单个虚继承,不带虚函数
class A
{
public:
A() : _ia(10){}
/*virtual*/
void f()
{
cout << "A::f()" << endl;
}
private:
int _ia;
};
class B
: virtual public A
{
public:
B() : _ib(20){}
void fb()
{
cout << "B::fb()" << endl;
}
/*virtual*/
void f()
{
cout << "B::f()" << endl;
}
/*virtual*/
void fb2()
{
cout << "B::fb2()" << endl;
}
private:
int _ib;
};
int main(void)
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
B b;
system("pause");
return 0;
}
生成项目将会在输出看到对象的内存布局,通过Ctrl+F可以搜索想查的类
1> class B size(12):
1> ±–
1> 0 | {vbptr} 虚基指针
1> 4 | _ib
1> ±–
1> ±-- (virtual base A)
1> 8 | _ia
1> ±–
可以看出虚继承与继承的区别:
总结1: 多了一个虚基指针
总结2: 虚基类位于派生类存储空间的最末尾
测试二:单个虚继承,带虚函数
打开测试一基类A中的virtual注释,即基类A中有虚函数,A中将会增加虚函数指针
总结1: 此时如果派生类没有自己的虚函数,此时派生类对象不会产生虚函数指针
1> class B size(16):
1> ±–
1> 0 | {vbptr} 虚基指针
1> 4 | _ib
1> ±–
1> ±-- (virtual base A)
1> 8 | {vfptr} 虚函数指针
1> 12 | _ia
1> ±–
再打开测试一派生类B中的virtual注释,即B中也有虚函数,B也会增加虚函数指针
总结2: 如果派生类拥有自己的虚函数,此时派生类对象就会产生自己本身的虚函数指针,并且该虚函数指针位于派生类对象存储空间的开始位置
1> class B size(20):
1> ±–
1> 0 | {vfptr}虚函数指针
1> 4 | {vbptr}虚基指针
1> 8 | _ib
1> ±–
1> ±-- (virtual base A)
1> 12 | {vfptr}虚函数指针
1> 16 | _ia
1> ±–
测试三:多重继承(带虚函数)
#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;
class Base1
{
public:
Base1() : _iBase1(1) {}
virtual void f()
{
cout << "Base1::f()" << endl;
}
virtual void g()
{
cout << "Base1::g()" << endl;
}
virtual void h()
{
cout << "Base1::h()" << endl;
}
private:
int _iBase1;
};
class Base2
{
public:
Base2() : _iBase2(11) {}
virtual void f()
{
cout << "Base2::f()" << endl;
}
virtual void g()
{
cout << "Base2::g()" << endl;
}
virtual void h()
{
cout << "Base2::h()" << endl;
}
private:
int _iBase2;
};
class Base3
{
public:
Base3() : _iBase3(111) {}
virtual void f()
{
cout << "Base3::f()" << endl;
}
virtual void g()
{
cout << "Base3::g()" << endl;
}
virtual void h()
{
cout << "Base3::h()" << endl;
}
private:
int _iBase3;
};
class Derived
: virtual public Base1
, public Base2
, public Base3
{
public:
Derived() : _iDerived(1111) {}
void f()
{
cout << "Derived::f()" << endl;
}
virtual void g1()
{
cout << "Derived::g1()" << endl;
}
private:
int _iDerived;
};
int main(void)
{
Derived d;
Base2 * pBase2 = &d;
Base3 * pBase3 = &d;
Derived * pDerived = &d;
pBase2->f();
cout << "sizeof(d) = " << sizeof(d) << endl;
cout << "&Derived = " << &d << endl; // 这三个地址值是不一样的
cout << "pBase2 = " << pBase2 << endl; //
cout << "pBase3 = " << pBase3 << endl; //
return 0;
}
输出结果:
输出分析:
打印出Derived 的f()函数,表示派生类覆盖虚基类的f()函数。
Derived的地址和pbase2的地址相同,表示对象Derived首地址是继承来的base2
1> class Derived size(32):
1> ±–
1> | ±-- (base class Base2)
1> 0 | | {vfptr}虚函数指针
1> 4 | | _iBase2
1> | ±–
1> | ±-- (base class Base3)
1> 8 | | {vfptr}虚函数指针
1> 12 | | _iBase3
1> | ±–
1> 16 | {vbptr}虚基指针跟着继承的派生类
1> 20 | _iDerived
1> ±–
1> ±-- (virtual base Base1)
1> 24 | {vfptr}虚函数指针
1> 28 | _iBase1
1> ±–
总结1: 通过看派生类Derived内存布局知道每个基类都有自己的虚函数表,即每个base都有虚函数指针,而且通过Derived内存布局知道其基类的布局按照基类被声明时的顺序进行排列
1> Derived::$vftable@Base2@:
1> | &Derived_meta
1> | 0
1> 0 | &Derived::f
1> 1 | &Base2::g
1> 2 | &Base2::h
1> 3 | &Derived::g1
总结2: 通过看base2的虚表中的内容知道派生类如果有自己独有的虚函数,会被加入到首地址的虚函数表之中,因为虚基类位于派生类存储空间的最末尾,所以此时首地址是base2
1> Derived::$vftable@Base1@:
1> | -24
1> 0 | &thunk: this-=24; goto Derived::f
1> 1 | &Base1::g
1> 2 | &Base1::h
1> Derived::$vftable@Base3@:
1> | -8
1> 0 | &thunk: this-=8; goto Derived::f
1> 1 | &Base3::g
1> 2 | &Base3::h
总结3: 通过看base1和base3中的虚表内容,了解到派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的对应的虚函数的地址,而只是一条goto跳转指令
测试四:菱形虚继承
#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;
class A
{
public:
A() : _ia(10), _ca('A') {}
virtual void f()
{
cout << "A::f()" << endl;
}
virtual void Af()
{
cout << "A::Af()" << endl;
}
private:
int _ia;
char _ca;
};
class B : virtual public A
{
public:
B() : _ib(100), _cb('1') {}
virtual void f()
{
cout << "B::f()" << endl;
}
virtual void f1()
{
cout << "B::f1()" << endl;
}
virtual void Bf()
{
cout << "B::Bf()" << endl;
}
private:
int _ib;
char _cb;
};
class C : virtual public A
{
public:
C() : _ic(1000), _cc('2') {}
virtual void f()
{
cout << "C::f()" << endl;
}
virtual void f2()
{
cout << "C::f2()" << endl;
}
virtual void Cf()
{
cout << "C::Cf()" << endl;
}
private:
int _ic;
char _cc;
};
class D : public B, public C
{
public:
D() : _id(10000), _cd('3') {}
virtual void f()
{
cout << "D::f()" << endl;
}
virtual void f1()
{
cout << "D::f1()" << endl;
}
virtual void f2()
{
cout << "D::f2()" << endl;
}
virtual void Df()
{
cout << "D::Df()" << endl;
}
private:
int _id;
char _cd;
};
int main(void)
{
D d;
cout << sizeof(d) << endl;
return 0;
}
输出结果:
1> class D size(52):
1> ±–
1> | ±-- (base class B)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | _ib
1> 12 | | _cb
1> | | alignment member (size=3)
1> | ±–
1> | ±-- (base class C)
1> 16 | | {vfptr}
1> 20 | | {vbptr}
1> 24 | | _ic
1> 28 | | _cc
1> | | alignment member (size=3)
1> | ±–
1> 32 | _id
1> 36 | _cd
1> | alignment member (size=3)
1> ±–
1> ±-- (virtual base A)
1> 40 | {vfptr}
1> 44 | _ia
1> 48 | _ca
1> | alignment member (size=3)
1> ±–
虚基表内容
1> D::$vbtable@B@:
1> 0 | -4
1> 1 | 36 (Dd(B+4)A)
1> D::$vbtable@C@:
1> 0 | -4
1> 1 | 20 (Dd(C+4)A)
总结1: 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
总结2: 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移
最后总结一下虚函数与虚拟继承的内存布局规则:
单个虚继承,不带虚函数时,对于虚继承和继承的区别:
1.多了一个虚基指针。
2.虚基类位于派生类存储空间的最末尾。
单个虚继承,带虚函数时:
1.如果派生类没有自己的虚函数,此时派生类对象不会产生虚函数指针
2.如果派生类拥有自己的虚函数,此时派生类对象就会产生自己本身的虚函数指针,并且该虚函数指针位于派生类对象存储空间的开始位置。
多重继承,带虚函数时:
1.每个基类都有自己的虚函数表。
2.派生类如果有自己的虚函数,会被加入到第一个虚函数表之中。
3.内存布局中,其基类的布局按照基类被声明时的顺序进行排列。
4.派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的对应的虚函数的地址,而只是一条跳转指令。
菱形虚继承时:
虚基指针所指向的虚基表的内容:
1.虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
2.虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移