说来惭愧,学习C++好几年了,但是从来没有接触过虚继承,今天总算有时间让我架起windbg一探究竟。
下面以狮子(Lion) 和老虎(Tiger)为例来说明,它们继承自动物(Animal),它们所生的后代是狮虎兽(Liger)。
lass Animal
{
public:
Animal(){}
int age;
int weight;
};
class Lion : virtual public Animal
{
public:
Lion(){}
int speed;
};
class Tiger : virtual public Animal
{
public:
Tiger(){}
int power;
};
class Liger : public Lion, public Tiger
{
public:
Liger(){}
int color;
};
int main()
{
Liger* liger = new Liger();
liger->age = 5;
liger->color = 0xFF22FF22;
liger->power = 100;
liger->speed = 200;
liger->weight = 300;
Animal* animal = liger;
animal->age = animal->age + 1;
animal->weight = animal->weight + 1;
int p = sizeof(liger);
Lion* lion = liger;
p = lion->age;
p = lion->speed;
p = lion->weight;
Tiger* tiger = liger;
p = tiger->age;
p = tiger->power;
p = tiger->weight;
return 0;
}
Liger* liger = new Liger();
1) 编译器为liger在栈上分配一个大小为28的memory,并且调用Liger()构造函数。操作符new分配memory的功能是通过调用标准库中的malloc实现的。
2)编译器将liger的首地址,做为构造函数Liger()的参数,这个地址就是我们属性的this指针。虽然在C++代码中,我们认为这个构造函数是没有参数的,但实际上编译器隐式的为此函数提供了this指针。
3)刚进入Liger构造函数的时候,liger指向的memory是一块没有被加工过的空间(里面的数据是无效的),构造函数的功能就是对这个memory进行布局和赋值。
1) 编译器为liger在栈上分配一个大小为28的memory,并且调用Liger()构造函数。操作符new分配memory的功能是通过调用标准库中的malloc实现的。
2)编译器将liger的首地址,做为构造函数Liger()的参数,这个地址就是我们属性的this指针。虽然在C++代码中,我们认为这个构造函数是没有参数的,但实际上编译器隐式的为此函数提供了this指针。
3)刚进入Liger构造函数的时候,liger指向的memory是一块没有被加工过的空间(里面的数据是无效的),构造函数的功能就是对这个memory进行布局和赋值。
指针首地址 | 地址偏移 | liger指向的memory | 内容 | 赋值操作 |
Liger-> Lion-> | +00h | Liger::`vbtable' (008d6870) | 00000000 00000014 00000000 | Liger()构造函数中 |
+04h | speed | 200 | liger->speed = 200 | |
Tiger-> | +08h | Liger::`vbtable' (008d687c) | 00000000 0000000c 00000000 | Liger()构造函数中 |
+0Ch | power | 100 | liger->power = 100 | |
+10h | color | 0xFFFF | liger->color = 0xFFFF | |
Animal-> | +14h | age | 5 | liger->age = 5; |
+18h | weight | 300 | liger->weight = 300 |
4)首先调用的父类构造函数为Animal(),它的this指针为liger+14h。同样编译器也将此this指针传递给Animal(),从而对Animal指向的memory进行布局和赋值。
5)调用Lion()构造函数,其首地址为liger+0,在Lion()构造函数中不会再次调用其父类Animal的构造函数(编译器决定)。
6)调用Tiger()构造函数,其首地址为liger+08h,也不会调用Animal的构造函数。
liger->age = 5;
7)得到vbtable (008d6870) 中地址偏移为+4的值00000014放入edx寄存器中,即edx = 14。
8)将5赋值给liger+edx的memory, 实现了对liger->age的赋值,即move dword ptr [ecx+14], 5。
liger->color = 0xFFFF;
9)将0xFFFF赋值给liger + 10h指向的memory。
liger->power = 100;
10)将100赋值给liger+0Ch指向的memory
liger->weight = 300;
11)获得vbtable (008d6870) 的地址
12)将虚表中偏移为+4的内容赋值给edx,即edx = 14h
13)将300赋值给liger + edx + 4 指向的memory。编译器根据当前的首地址决定使用其指向虚表,Animal中的成员变量都需要通过续表中的偏移量去寻找,非虚继承的成员变量不需要查找续表。
Animal* animal = liger;
animal->age = animal->age + 1
14)获得vbtable (008d6870)中的偏移量14h的值,将其+1。
animal->weight = animal->weight + 1;
15)将animal->age 偏移+4的memory+1
int p = sizeof(ligher)
16)指针的大小为4
Lion* lion = liger;
p = lion->age;
17)通过虚表vbtable (008d6870) 得到lion->age 赋值给局域变量p。lion和liger的地址是一样的。
p = lion->speed;
18)lion首地址+4
p = lion->weight;
19)通过虚表得到14,再加上4,得到weight的值。
Tiger* tiger = liger;
20)tiger = liger + 8,tiger的偏移量为+8,不难发现+8为Tiger虚表Liger::`vbtable' (008d687c)的位置。通过tiger指针引用Animal中的成员变量,都将根据这张虚表。
p = tiger->age;
p = tiger->power;
p = tiger->weight;
21)和lion中的操作类似,age和weight通过虚表,power直接通过首地址的偏移。通过查找虚表,memory中只需要一份公共基类Animal的拷贝。
22)虚表中有两个dword,偏移量+4的值为成员变量的偏移量,其他两个都是0 (不知道什么作用,它们在当前代码中没有使用到)
没有使用虚继承的时候,liger的内存布局为:
指针首地址 | 偏移量 | 成员变量 |
Liger ->
Lion ->
Animal->
| +00h | age |
+04h | weight | |
+08h | speed | |
Tiger-> Animal-> | +0Ch | age |
+10h | weight | |
+14h | power | |
+18h | color |
Tiger* tiger = liger; 编译器会将tiger指针指向首地址+0Ch的地方。
但是Animal* animal = liger会导致编译错误,因为内存布局中存在两个Animal,我们只能强制指定Animal* animal = (Tiger*)liger。
虚继承的意义:让子类只保存一份公共基类的成员变量,通过vbtable中的偏移查找每个父类的成员变量。