c++ 多重继承
代码案例
#include <stdio.h>
class Sofa {
public:
Sofa() {
color = 2;
}
virtual ~Sofa() { // 沙发类虚析构函数
printf("virtual ~Sofa()\n");
}
virtual int getColor() { // 获取沙发颜色
return color;
}
virtual int sitDown() { // 沙发可以坐下休息
return printf("Sit down and rest your legs\r\n");
}
protected:
int color; // 沙发类成员变量
};
//定义床类
class Bed {
public:
Bed() {
length = 4;
width = 5;
}
virtual ~Bed() { //床类虚析构函数
printf("virtual ~Bed()\n");
}
virtual int getArea() { //获取床面积
return length * width;
}
virtual int sleep() { //床可以用来睡觉
return printf("go to sleep\r\n");
}
protected:
int length; //床类成员变量
int width;
};
//子类沙发床定义,派生自 Sofa 类和 Bed 类
class SofaBed : public Sofa, public Bed{
public:
SofaBed() {
height = 6;
}
virtual ~SofaBed(){ //沙发床类的虚析构函数
printf("virtual ~SofaBed()\n");
}
virtual int sitDown() { //沙发可以坐下休息
return printf("Sit down on the sofa bed\r\n");
}
virtual int sleep() { //床可以用来睡觉
return printf("go to sleep on the sofa bed\r\n");
}
virtual int getHeight() {
return height;
}
protected:
int height;
};
int main(int argc, char* argv[]) {
SofaBed sofabed;
return 0;
}
内存结构
从汇编观察
main函数
push ebp
mov ebp, esp
sub esp, 1Ch
lea ecx, [ebp+var_1C] ; var_1C size=0x18
call constructor_401090_SofaBed
mov [ebp+var_4], 0
lea ecx, [ebp+var_1C]
call destructor_401130_SofaBed
mov eax, [ebp+var_4]
mov esp, ebp
pop ebp
retn
一共分配了七个栈空间,其中var_4是用来存储main函数返回值的,可见sofabed的内存大小为0x18
constructor_401090_SofaBed
push ebp
mov ebp, esp
push ecx
mov [ebp+var_4], ecx
mov ecx, [ebp+var_4]
call constructor_401060_Sofa
mov ecx, [ebp+var_4]
add ecx, 8
call constructor_401030_Bed
mov eax, [ebp+var_4]
mov dword ptr [eax], offset SofaBed_vftable1
mov ecx, [ebp+var_4]
mov dword ptr [ecx+8], offset SofaBed_vftable2
mov edx, [ebp+var_4]
mov dword ptr [edx+14h], 6
mov eax, [ebp+var_4]
mov esp, ebp
pop ebp
retn
可以看到有两个thiscall函数,下面有两个虚指针赋值,内存是第一块和第三块,再下面是给sofabed对象的第六块内存赋值为6
constructor_401060_Sofa
push ebp
mov ebp, esp
push ecx
mov [ebp+var_4], ecx
mov eax, [ebp+var_4]
mov dword ptr [eax], offset ??_7Sofa@@6B@ ; const Sofa::`vftable'
mov ecx, [ebp+var_4]
mov dword ptr [ecx+4], 2
mov eax, [ebp+var_4]
mov esp, ebp
pop ebp
retn
在第一块内存赋值虚指针,在第二块内存赋值为2
constructor_401030_Bed
push ebp
mov ebp, esp
push ecx
mov [ebp+var_4], ecx
mov eax, [ebp+var_4]
mov dword ptr [eax], offset ??_7Bed@@6B@ ; const Bed::`vftable'
mov ecx, [ebp+var_4]
mov dword ptr [ecx+4], 4
mov edx, [ebp+var_4]
mov dword ptr [edx+8], 5
mov eax, [ebp+var_4]
mov esp, ebp
pop ebp
retn
由于constructor_401030_Bed在被调用的时候ecx加了8,所以这里的this指针其实是第三块内存的地址,在第四块和第五块内存分别赋值 4、5
至此所有的内存清楚了
-
第一块 原来是Sofa的虚表指针,后来被赋值为SofaBed的第一个虚表
-
第二块 color 初始化为2
-
第三块 原来是Bed的虚表指针,后来被赋值为SofaBed的第二个虚表
-
第四块 length 初始化为4
-
第五块 width 初始化为5
-
第六块 height 初始化为6
看来多重继承编译器会给对象的基类分别分配虚指针和成员变量,并依次排列,最后会放上自己的成员变量
那么这些虚表之间的关系是怎样的呢
虚表关系
Sofa::`vftable'
dd offset Sofa_vector_destructor
dd offset Sofa_getColor
dd offset Sofa_siteDown
Sofa类的虚表都是它自己的虚函数
Bed::`vftable'
dd offset Bed_vector_destructor
dd offset Bed_getArea
dd offset Bed_sleep
Bed类的虚表也都是它自己的虚函数
SofaBed_vftable1
dd offset SofaBed_vector_destructor
dd offset Sofa_getColor
dd offset SofaBed_siteDown
dd offset SofaBed_getHeight
SofaBed第一个虚表,前面三个都是覆盖Sofa的虚函数,最后一个是自己的虚函数
SofaBed_vftable2
dd offset SofaBed_vector_destructor_
dd offset Bed_getArea
dd offset SofaBed_sleep
SofaBed第二个虚表,都是覆盖Bed的虚函数
虚表关系图
SofaBed_vftable1
对应 Sofa::vftable
,SofaBed_vftable2
对应Bed::vftable
,如果SofaBed没有重写父类的虚函数,如getColor,就会在对应的位置放上父类的虚函数
构造与析构流程
构造流程
通过观察上面的汇编代码,构造流程为
- 首先按照继承顺序调用父类的构造函数
- 按照继承顺序覆盖父类虚指针
- 执行自身构造函数的代码
析构流程
destructor_401130_SofaBed
push ebp
mov ebp, esp
push ecx
mov [ebp+var_4], ecx
mov eax, [ebp+var_4]
mov dword ptr [eax], offset SofaBed_vftable1
mov ecx, [ebp+var_4]
mov dword ptr [ecx+8], offset SofaBed_vftable2
push offset aVirtualSofabed ; "virtual ~SofaBed()\n"
call _printf
add esp, 4
mov ecx, [ebp+var_4]
add ecx, 8
call destructor_4010D0_Bed
mov ecx, [ebp+var_4]
call destructor_401100_Sofa
mov esp, ebp
pop ebp
retn
通过观察上面的汇编代码,析构流程为
- 按照继承顺序还原自身虚指针
- 执行自身析构代码
- 按照继承相反顺序调用父类的析构函数
实现多态
多态的核心便是使用基类指针指向子类对象,多重继承的类的虚表指针并非只在对象首地址,那么父类指针是如何调用虚函数的呢
int main(int argc, char* argv[]) {
SofaBed sofabed;
Sofa *sofa = &sofabed;
Bed *bed = &sofabed;
return 0;
}
汇编
.text:0000000140001000 mov [rsp+argv], rdx
.text:0000000140001005 mov [rsp+argc], ecx
.text:0000000140001009 sub rsp, 78h
.text:000000014000100D lea rcx, [rsp+78h+sofaBed]
.text:0000000140001012 call SofaBed_ctor
.text:0000000140001017 lea rax, [rsp+78h+sofaBed]
.text:000000014000101C mov [rsp+78h+pSofa], rax
.text:0000000140001021 lea rax, [rsp+78h+sofaBed]
.text:0000000140001026 test rax, rax
.text:0000000140001029 jz short IF_END_14000103B
.text:000000014000102B lea rax, [rsp+78h+sofaBed]
.text:0000000140001030 add rax, 10h
.text:0000000140001034 mov [rsp+78h+pBed1], rax
.text:0000000140001039 jmp short ELSE_END_140001044
.text:000000014000103B ; ---------------------------------------------------------------------------
.text:000000014000103B
.text:000000014000103B IF_END_14000103B: ; CODE XREF: main+29↑j
.text:000000014000103B mov [rsp+78h+pBed1], 0
.text:0000000140001044
.text:0000000140001044 ELSE_END_140001044: ; CODE XREF: main+39↑j
.text:0000000140001044 mov rax, [rsp+78h+pBed1]
.text:0000000140001049 mov [rsp+78h+pBed], rax
.text:000000014000104E mov [rsp+78h+temp_return], 0
.text:0000000140001056 lea rcx, [rsp+78h+sofaBed]
.text:000000014000105B call SofaBed_dtor
.text:0000000140001060 mov eax, [rsp+78h+temp_return]
.text:0000000140001064 add rsp, 78h
.text:0000000140001068 retn
.text:0000000140001068 main endp
.text:0000000140001068
.text:0000000140001068 ; ---------------------------------------------------------------------------
.text:0000000140001069 algn_140001069: ; DATA XREF: .pdata:ExceptionDir↓o
.text:0000000140001069 align 10h
.text:0000000140001070
.text:0000000140001070 ; =============== S U B R O U T I N E =======================================
.text:0000000140001070
.text:0000000140001070
.text:0000000140001070 Bed_ctor proc near ; CODE XREF: SofaBed_ctor+1F↓p
.text:0000000140001070
.text:0000000140001070 arg_0 = qword ptr 8
.text:0000000140001070
.text:0000000140001070 mov [rsp+arg_0], rcx
.text:0000000140001075 mov rax, [rsp+arg_0]
.text:000000014000107A lea rcx, ??_7Bed@@6B@ ; const Bed::`vftable'
.text:0000000140001081 mov [rax], rcx
.text:0000000140001084 mov rax, [rsp+arg_0]
.text:0000000140001089 mov dword ptr [rax+8], 4
.text:0000000140001090 mov rax, [rsp+arg_0]
.text:0000000140001095 mov dword ptr [rax+0Ch], 5
.text:000000014000109C mov rax, [rsp+arg_0]
.text:00000001400010A1 retn
可以看到对象在转换Bed *指针时将地址加了0x10。看来多重继承是通过转换指针时调整对象首地址来实现多态的,因为在转换指针(由子类指针转换为父类指针)时必定知道子类与父类的关系,从而推算出该加多少地址
如果编译器转换的双方没有嫡系祖先关系的话,强制转换后就会有问题
int main(int argc, char* argv[]) {
SofaBed sofabed;
Sofa *sofa = &sofabed;
Bed *bed = (Bed *)sofa;
return 0;
}
error C2440: “初始化”: 无法从“Sofa *”转换为“Bed *”
vs2019会无法编译通过,其他编译器的话就算通过,强制转换也不会调整对象首地址,导致调用虚函数和访问成员变量错误