虚继承解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题
以一段代码为例进行GCC虚继承实现机制的分析(本代码来自C++反汇编与逆向分析技术揭秘):
#include <stdio.h>
class CFurniture{
public:
CFurniture(){
m_nPrice = 0;
}
virtual ~CFurniture(){
printf("virtual ~CFurniture.\n");
}
virtual int GetPrice(){
return m_nPrice;
}
protected:
int m_nPrice;
};
class CSofa: virtual public CFurniture{
public:
CSofa(){
m_nPrice = 1;
m_nColor = 2;
}
virtual ~CSofa(){
printf("virtual ~CSofa\n");
}
virtual int GetColor(){
return m_nColor;
}
virtual int SitDown(){
printf("sit down and rest your legs.\n");
return 0;
}
protected:
int m_nColor;
};
class CBed: virtual public CFurniture{
public:
CBed(){
m_nPrice = 3;
m_nLength = 4;
m_nWidth = 5;
}
virtual ~CBed(){
printf("virtual ~CBed.\n");
}
virtual int GetArea(){
return m_nLength * m_nWidth;
}
virtual int Sleep(){
printf("Go to sleep.\n");
return 0;
}
protected:
int m_nLength;
int m_nWidth;
};
class CSofaBed: public CSofa, public CBed{
public:
CSofaBed(){
m_nHeight = 6;
}
virtual ~CSofaBed(){
printf("virtual ~CSofaBed.\n");
}
virtual int SitDown(){
printf("sit down on sofa bed.\n");
return 0;
}
virtual int Sleep(){
printf("go to sleep on sofa bed.\n");
return 0;
}
virtual int GetHeight(){
return m_nHeight;
}
//virtual int GetPrice(){
// return m_nPrice * 2;
//}
protected:
int m_nHeight;
};
int main()
{
//CSofaBed sofaBed;
CSofa *CFurniture = new CSofaBed();
//CFurniture->GetPrice();
delete CFurniture;
}
这里给出了这段代码的构造部分反汇编代码分析以及程序加载后的内存分布(使用gdb),见链接:
点击打开链接
通过分析可知CSofaBed对象的内存布局如图:
这种布局,里面只有一个CFuniture对象,避免了上述的二义性和空间浪费问题
如何能正确的调用各种虚函数呢,GCC编译器定义了很多适配各种场景的虚表,以本例来说:
a. CSofaBed继承CSofa的虚表(因为两个对象的首地址相同,所以不存在仅适配CSofa类的虚表,只有CSofa+CSofaBed两个类合一起的虚表)
b. CSofaBed适配CBed的虚表
c. CSofaBed适配CFurniture的虚表
d. 定义CSofaBed类对象时,CSofa类虚表
e. 定义CSofaBed类对象时,CSofa类适配CFurniture类的虚表
e. 定义CSofaBed类对象时,CBed类虚表
f. 定义CSofaBed类对象时,CBed类适配CFurniture类的虚表
g.定义CSofa类对象时,CSofa类虚表
h.定义CSofa类对象时,CSofa类适配CFurniture类的虚表
i.定义CBed类对象时,CBed类虚表
j.定义CBed类对象时,CBed类适配CFurniture类的虚表
k. CFurniture类虚表
为什么CSofa和CBed类各自有那么多虚表呢,这是因为各个虚表适配的场景不同:
a. 有多个类自身的虚表,虚表前面的0x18偏移处,存放了此种情况下,父类CFurniture相对于本类的偏移地址,这样才能定位父类地址,并将父类的虚表指针指向各自类定义的适配与父类的虚表。不同场景下父类的偏移是不同的
例如,定义CSofaBed类对象时,父类相对于CSofa类的偏移在0x28处
定义CSofa类时,父类相对于CSofa类的偏移在0X10处
b. 有个适配父类的CFurniture虚表,这种虚表的前面偏移0x18处,存放了此种情况下,本类对象相对于父类对象空间地址的偏移量,这样在使用父类类型指针或引用调用虚函数时,从这个虚表的偏移处取出偏移量从而定位到本类的真实地址,然后再调用真正的虚函数。这个偏移往往是不同的:
例如,定义CSofaBed类对象时,要从CFurniture类地址空间向前偏移0x28,才能得到CSofa类对象地址
定义CSofa类对象时,从CFurniture类地址空间向前偏移0x10,才能得到CSofa类对象地址。
这种适配父类的虚表中的虚函数指针,往往是真实虚函数的包裹函数,其作用就是找到对象的真实地址,然后调用对象的真实虚函数,例如:
0000000000400ad7 <_ZTv0_n24_N5CSofaD1Ev>:
400ad7: 4c 8b 17 mov (%rdi),%r10
400ada: 49 03 7a e8 add -0x18(%r10),%rdi
400ade: e9 5d ff ff ff jmpq 400a40 <_ZN5CSofaD1Ev>
400ae3: 90 nop
注意CSofaBed适配CBed的虚表中的包裹函数没有使用在虚表前面0x18处找实际CSofaBed相对于CBed的偏移,而直接使用了CBed减0x10获取CSofaBed的地址,这是虚函数包裹函数的另外一种实现。
上面的分析,大致分析了C++菱形继承的实现原理,从汇编的角度看GCC的实现,比从高层抽象的解说,应该更加清晰明了。