一、继承的基本概念
继承:子类继承父类的属性和行为
作用:代码复用
继承分类:
1. 按访问属性分为public、private、protected三类
1)public: 父类属性无更改,pubic, private, protected 仍是自己本身(子类成员函数可以访问父类的public和protected,子类对象可以访问public)
2)private: 父类属性全变为privates(子类不能访问父类属性)
3)protected: 父类public变为protected,其他不变(子类成员函数可以访问父类的public和protected,子类对象不能访问)
2. 按继承父类的个数分为单继承和多继承
类的成员函数由所有对象共享,但是每个对象有单独的成员变量,所以利用sizeof(对象时),字节数为所有成员变量的大小
普通继承:子类继承父类即继承父类的所有属性及行为,当多继承时,有父类的父类的两份拷贝
虚继承:菱形继承,共享一个虚基类
二、类与类的关系
1. 父类和子类
普通继承:先执行父类构造函数,再执行子类构造函数;先执行子类析构函数,再执行父类析构函数
1)当子类中没有构造函数或析构函数,父类却需要构造函数和析构函数时,编译器会为子类提供默认的构造函数与析构函数以调用父类的构造和析构函数
2)子类的内存结构:子类继承父类,类似在子类中定义了父类的对象,如此当产生Derive类的对象时,会先产生成员对象base,这需要调用其构造函数
当Derive类没有构造函数时,为了能够在Derive类对象产生时调用成员对象的构造函数,编译器同样会提供默认的构造函数,以实现成员构造函数的调用
class Base{...};
class Derive {
public:
Base base; //原来的父类Base 成为成员对象
int derive; // 原来的子类派生数据
};
3)子类内存中的数据排列:先安排父类的数据,后安排子类新定义的数据
注意:当子类中有构造函数,父类无构造函数,不会给父类提供默认的构造函数
普通子类继承父类 c++ 代码示例:
#include <stdio.h>
class Base { //基类定义
public:
Base() {
printf("Base\n");
}
~Base() {
printf("~Base\n");
}
void setNumber(int n) {
base = n;
}
int getNumber() {
return base;
}
public:
int base;
};
class Derive : public Base { //派生类定义
public: void showNumber(int n) {
setNumber (n);
derive = n + 1;
printf("%d\n", getNumber());
printf("%d\n", derive);
}
public:
int derive;
};
int main(int argc, char* argv[]) {
Derive derive;
derive.showNumber(argc);
return 0;
}
汇编标识:
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 0Ch
00401006 lea ecx, [ebp-0Ch] ;获取对象首地址作为this指针
00401009 call sub_401050 ;调用类Derive的默认构造函数 ①
0040100E mov eax, [ebp+8]
00401011 push eax ;参数2:argc
00401012 lea ecx, [ebp-0Ch] ;参数1:传入this指针
00401015 call sub_4010E0 ;调用成员函数showNumber ②
0040101A mov dword ptr [ebp-4], 0
00401021 lea ecx, [ebp-0Ch] ;传入this指针
00401024 call sub_401090 ;调用类Derive的默认析构函数 ③
00401029 mov eax, [ebp-4]
0040102C mov esp, ebp
0040102E pop ebp
0040102F retn
00401050 push ebp ;子类Derive的默认构造函数分析
00401051 mov ebp, esp
00401053 push ecx
00401054 mov [ebp-4], ecx
00401057 mov ecx, [ebp-4] ;以子类对象首地址作为父类的this指针 ①
0040105A call sub_401030 ;调用父类构造函数
0040105F mov eax, [ebp-4]
00401062 mov esp, ebp
00401064 pop ebp
00401065 retn
00401090 push ebp ;子类Derive的默认析构函数分析
00401091 mov ebp, esp
00401093 push ecx
00401094 mov [ebp-4], ecx
00401097 mov ecx, [ebp-4] ;以子类对象首地址作为父类的this指针 ①
0040109A call sub_401070 ;调用父类析构函数
0040109F mov esp, ebp
004010A1 pop ebp
004010A2 retn
子类中定义了其他对象作为成员,并在初始化列表中指定了某个成员的初始化值时:先构造父类,然后按声明顺序构造成员对象和初始化列表中指定的成员,最后构造自己
类中定义了其他对象作为成员,并在初始化列表中指定了某个成员的初始化值时的 c++ 示例代码:
class Member{
public:
Member() {
member = 0;
}
int member;
};
class Derive : public Base {
public:
Derive():derive(1) {
printf("使用初始化列表\n");
}
public:
Member member; //类中定义其他对象作为成员
int derive;
};
int main(int argc, char* argv[]) {
Derive derive;
return 0;
}
汇编标识:
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 10h
00401006 lea ecx, [ebp-10h] ;传递this指针
00401009 call sub_401050 ;调用Derive的构造函数 ①
0040100E mov dword ptr [ebp-4], 0
00401015 lea ecx, [ebp-10h] ;传递this指针
00401018 call sub_4010D0 ;调用Derive的析构函数 ⑥
0040101D mov eax, [ebp-4]
00401020 mov esp, ebp
00401022 pop ebp
00401023 retn
00401050 push ebp ; Derive构造函数
00401051 mov ebp, esp
00401053 push ecx
00401054 mov [ebp-4], ecx ;[ebp-4]保存了this指针
00401057 mov ecx, [ebp-4] ;传递this指针
0040105A call sub_401030 ;调用父类构造函数 ②
0040105F mov ecx, [ebp-4]
00401062 add ecx, 4 ;根据this指针调整到类中定义的对象member的首地址处
00401065 call sub_401090 ;调用Member构造函数 ③
0040106A mov eax, [ebp-4]
0040106D mov dword ptr [eax+8], 1 ;执行初始化列表 ④,this指针传递给eax后,[eax+8]是对成员数据derive进行寻址
00401074 push offset unk_412170 ;最后才是执行Derive的构造代码 ⑤
00401079 call sub_401130 ;调用printf函数
0040107E add esp, 4
00401081 mov eax, [ebp-4]
00401084 mov esp, ebp
00401086 pop ebp
00401087 retn
2. 使用父类指针访问子类对象
因为父类对象的长度不超过子类对象,使用父类指针访问子类对象不会造成访问越界
子类调用父类函数(showNumber函数汇编标识)
004010E0 push ebp ;showNumber函数
004010E1 mov ebp, esp
004010E3 push ecx
004010E4 mov [ebp-4], ecx ;[ebp-4]中保留了this指针
004010E7 mov eax, [ebp+8]
004010EA push eax ;参数2:n
004010EB mov ecx, [ebp-4] ;参数1:因为this指针同时也是对象中父类部分的首地址
;所以在调用父类成员函数时,this指针的值和子类对象等同 ①
004010EE call sub_4010C0 ;调用基类成员函数setNumber ②
004010F3 mov ecx, [ebp+8]
004010F6 add ecx, 1 ;将参数n值加1
004010F9 mov edx, [ebp-4] ;edx拿到this指针
004010FC mov [edx+4], ecx ;参考内存结构,edx+4是子类成员derive的地址,derive=n+1
004010FF mov ecx, [ebp-4] ;传递this指针
00401102 call sub_4010B0 ;调用基类成员函数getNumber ③
00401107 push eax ;参数2:Base.base
00401108 push offset aD ;参数1:"%d\n"
0040110D call sub_401170 ;调用printf函数
00401112 add esp, 8
00401115 mov eax, [ebp-4]
00401118 mov ecx, [eax+4]
0040111B push ecx ;参数2:derive
0040111C push offset aD ;参数1:"%d\n"
00401121 call sub_401170 ;调用printf函数
00401126 add esp, 8
00401129 mov esp, ebp
0040112B pop ebp
0040112C retn 4
父类中成员函数在子类中没有被定义,但在子类中可以使用父类的公有函数。编译器如何实现正确匹配?
如果使用对象或对象的指针调用成员函数,编译器可根据对象所属作用域通过“名称粉碎法”实现正确匹配。在成员函数中调用其他成员函数时,可匹配当前作用域
名称粉碎(name mangling):
C++编译器对函数名称的一种处理方式,即在编译时对函数名进行重组,新名称会包含函数的作用域、原函数名、每个参数的类型、返回值以及调用约定等信息
3. 使用子类指针访问父类对象
如果访问的成员数据是父类对象定义的,则不会出错;如果访问的是子类派生的成员数据,则会造成访问越界
子类指针访问父类对象(可能出现访问越界)
int main(int argc, char* argv[]) {
int n = 0x12345678;
Base base;
Derive *derive = (Derive*)&base;
printf("%x\n", derive->derive);
return 0;
}
汇编标识:
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 10h
00401006 mov dword ptr [ebp-10h], 12345678h ;局部变量赋初值
0040100D lea ecx, [ebp-4] ;传递this指针
00401010 call sub_401050 ;调用构造函数
00401015 lea eax, [ebp-4]
00401018 mov [ebp-8], eax ;指针变量[ebp-8]得到base的地址
0040101B mov ecx, [ebp-8]
0040101E mov edx, [ecx+4] ;注意,ecx中保留了base的地址,而[ecx+4]的访问超出了base的内存范围
00401021 push edx
00401022 push offset unk_412160
00401027 call sub_4010D0 ;调用printf函数
0040102C add esp, 8
0040102F mov dword ptr [ebp-0Ch], 0
00401036 lea ecx, [ebp-4] ;传递this指针
00401039 call sub_401070 ;调用析构函数
0040103E mov eax, [ebp-0Ch]
00401041 mov esp, ebp
00401043 pop ebp
00401044 retn
4. 多态
虚函数的调用过程使用了间接寻址方式,而非直接调用函数地址
1)父类指针指向子类对象可以调用子类对象的虚函数的原因:
由于虚表采用间接调用机制,因此在使用父类指针person调用虚函数时,没有依照其作用域调用Person类中定义的成员函数showSpeak
2)父类构造函数中调用虚函数
①当父类的子类产生对象时,会在调用子类构造函数前优先调用父类构造函数,并以子类对象的首地址作为this指针传递给父类构造函数
②在父类构造函数中,会先初始化子类虚表指针为父类的虚表首地址
③如果在父类构造函数中调用虚函数,虽然虚表指针属于子类对象,但指向父类的虚表首地址,可判断虚表所属作用域与当前作用域相同,转换成直接调用方式,最终造成构造函数内的虚函数失效。
class Person {
public:
Person() {
showSpeak(); //调用虚函数,不多态
}
virtual ~Person() {
}
virtual void showSpeak() {
printf("Speak No\n");
}
};
这样的意义:
按C++规定的构造顺序,父类构造函数会在子类构造函数之前运行,在执行父类构造函数时将虚表指针修改为当前类的虚表指针,也就是父类的虚表指针,因此导致虚函数的特性失效。如果父类构造函数内部存在虚函数调用,这样的顺序能防止在子类中构造父类时,父类根据虚表错误地调用子类的成员函数。
为什么不直接把构造函数或析构函数中的虚函数调用修改为直接调用方式使构造和析构函数中的虚函数多态性失效?
因为其他成员函数仍可以间接调用本类中声明的其他虚函数形成多态,如果子类对象的虚表指针没有更换为父类的虚表指针,会导致在访问子类的虚表后调用到子类中的对应虚函数
3)父类析构函数中调用虚函数
①子类对象析构时,设置虚表指针为自身虚表,再调用自身的析构函数
②如果有成员对象,则按声明的顺序以倒序方式依次调用成员对象的析构函数
③最后,调用父类析构函数。在调用父类的析构函数时,会设置虚表指针为父类自身的虚表
4)将析构函数定义为虚函数的原因
当使用父类指针指向子类堆对象时,使用delete函数释放对象的空间时,如果析构函数没有被定义为虚函数,那么编译器会按指针的类型调用父类的析构函数,从而引发错误。而使用了虚析构函数后,会访问虚表并调用对象的析构函数
//没有声明为虚析构函数
Person * p = new Chinese;
delete p; //部分代码分析略
00D85714 mov ecx,dword ptr [ebp+FFFFFF08h] ;直接调用父类的析构函数
00D8571A call 00D81456
// 声明为虚析构函数
Person * p = new Chinese;
delete p; //部分代码分析略
000B5716 mov ecx,dword ptr [ebp+FFFFFF08h] ;获取p并保存至ecx
000B571C mov edx,dword ptr [ecx] ;取得虚表指针
000B571E mov ecx,dword ptr [ebp+FFFFFF08h] ;传递this指针
000B5724 mov eax,dword ptr [edx] ;间接调用虚析构函数
000B5726 call eax
注意:
当没有使用对象指针或者对象引用时,调用虚函数指令的寻址方式为直接调用,从而无法构成多态
5)在 IDA 中综合分析
以下代码的整体流程:
①申请堆空间
②调用父类的构造函数
a.将父类的虚表指针写入对象首地址处
b.调用父类的showSpeak函数(直接调用)
③调用子类的构造函数
a.将子类的虚表指针写入对象首地址处
b.调用子类的showSpeak函数(直接调用)
④间接调用子类的showSpeak函数(查表,此时虚表指针为子类)
⑤传入delete标志,间接调用虚表中的析构代理函数(查表,此时虚表指针为子类)
a.调用子类析构函数
ⅰ.将子类虚表指针写入对象首地址处
ⅱ.调用getClassName函数(直接调用)
b.调用父类析构函数
ⅰ.将父类虚表指针写入对象首地址处
ⅱ.调用getClassName函数(直接调用)
c.根据标识调用delete释放内存空间
为什么调用析构代理函数时要压入是否释放内存的标志?
①因为析构函数和释放内存是两件事,可以选择只调用析构函数而不释放内存空间。
②因为显式调用析构函数时不能马上释放堆内存,所以在析构函数的代理函数中通过一个参数控制是否释放内存,便于程序员管理析构函数的调用
为什么编译器要在子类析构函数中再次将虚表设置为子类虚表?(即上述标红处)
因为编译器无法预知这个子类以后是否会被其他类继承,如果被继承,原来的子类就成了父类,当前对象的析构函数开始执行时,其虚表也是当前对象的,所以执行到父类的析构函数时,虚表必须改写为父类的虚表。故在每个对象的析构函数内,要加入自己虚表的代码
c++示例代码:
#include <stdio.h>
class Person{ //基类:人类
public:
Person() {
showSpeak(); //注意,构造函数调用了虚函数
}
virtual ~Person(){
showSpeak(); //注意,析构函数调用了虚函数
}
virtual void showSpeak(){
//在这个函数里调用了其他的虚函数getClassName();
printf("%s::showSpeak()\n", getClassName());
return;
}
virtual const char* getClassName()
{
return "Person";
}
};
class Chinese : public Person { //中国人,继承自"人"类
public:
Chinese() {
showSpeak();
}
virtual ~Chinese() {
showSpeak();
}
virtual const char* getClassName() {
return "Chinese";
}
};
int main(int argc, char* argv[]) {
Person *p = new Chinese;
p->showSpeak();
delete p;
return 0;
}
vs_x86汇编标识:
.text:004011D0 block = dword ptr -10h
.text:004011D0 var_C = dword ptr -0Ch
.text:004011D0 var_4 = dword ptr -4
.text:004011D0 argc = dword ptr 8
.text:004011D0 argv = dword ptr 0Ch
.text:004011D0
.text:004011D0 ; FUNCTION CHUNK AT .text:00402070 SIZE 00000017 BYTES
.text:004011D0
.text:004011D0 ; __unwind { // __ehhandler$_main
.text:004011D0 push ebp
.text:004011D1 mov ebp, esp
.text:004011D3 push 0FFFFFFFFh
.text:004011D5 push offset __ehhandler$_main
.text:004011DA mov eax, large fs:0
.text:004011E0 push eax
.text:004011E1 push ecx
.text:004011E2 push esi
.text:004011E3 mov eax, ___security_cookie
.text:004011E8 xor eax, ebp
.text:004011EA push eax
.text:004011EB lea eax, [ebp+var_C]
.text:004011EE mov large fs:0, eax
.text:004011F4 push 4 ; size
.text:004011F6 call ??2@YAPAXI@Z ; 申请4字节堆空间 ①
.text:004011FB mov esi, eax ; esi保存new调用的返回值
.text:004011FD add esp, 4 ; 平衡new调用的参数
.text:00401200 mov [ebp+block], esi
;在构造函数中先填写父类的虚表,然后按继承的层次关系逐层填写子类的虚表
;内联父类构造函数
.text:00401203 ; try {
.text:00401203 mov [ebp+var_4], 0 ; 调用父类的构造函数 ②
.text:0040120A mov ecx, esi ; this
.text:0040120C mov dword ptr [esi], offset Person_vtable ; 将虚表指针写入对象首地址 ③
.text:00401212 call Person_getClassName ;调用父类的getClassName(直接调用,此时对象首地址处为父类虚表) ④
.text:00401217 push eax
.text:00401218 push offset _Format ; "%s::showSpeak()\n"
.text:0040121D call _printf
.text:00401222 add esp, 8
.text:00401222 ; } // starts at 401203
;内联子类构造函数
.text:00401225 ; try {
.text:00401225 mov byte ptr [ebp+var_4], 1 ; 调用子类的构造函数 ⑤
.text:00401229 mov ecx, esi ; this
.text:0040122B mov dword ptr [esi], offset Chinese_vtable ; 将虚表指针写入对象首地址 ⑥
.text:00401231 call Chinese_getClassName ;调用子类的getClassName(直接调用) ⑦
.text:00401236 push eax
.text:00401237 push offset _Format ; "%s::showSpeak()\n"
.text:0040123C call _printf
.text:0040123C ; } // starts at 401225
.text:00401241 mov [ebp+var_4], 0FFFFFFFFh
.text:00401248 add esp, 8
.text:0040124B mov eax, [esi] ; 得到虚表指针,此时虚表指针为子类的虚表指针
.text:0040124D mov ecx, esi ; 传递this指针
.text:0040124F call dword ptr [eax+4] ; 间接调用虚表第二项的函数,即showspeak ⑧
.text:00401252 mov eax, [esi]
.text:00401254 mov ecx, esi
.text:00401256 push 1 ;传入delete释放标志,标识要释放内存空间,否则只调用析构函数
.text:00401258 call dword ptr [eax] ; 间接调用虚表中的虚析构函数,此时虚表指针为子类 ⑨
.text:0040125A xor eax, eax
.text:0040125C mov ecx, [ebp+var_C]
.text:0040125F mov large fs:0, ecx
.text:00401266 pop ecx
.text:00401267 pop esi
.text:00401268 mov esp, ebp
.text:0040126A pop ebp
.text:0040126B retn
.text:0040126B ; } // starts at 4011D0
.text:0040126B _main endp
; void __thiscall showSpeak(Person *this)
.text:00401090 showSpeak proc near
.text:00401090 mov eax, [this]
.text:00401092 call dword ptr [eax+8] ; 间接调用getClassName函数
.text:00401095 push eax
.text:00401096 push offset _Format ; "%s::showSpeak()\n"
.text:0040109B call _printf
.text:004010A0 add esp, 8
.text:004010A3 retn
.text:004010A3 showSpeak endp
;子类的虚析构代理函数
.text:00401140 _Destructor_00401140 proc near
.text:00401140 var_C = dword ptr -0Ch
.text:00401140 var_4 = dword ptr -4
.text:00401140 arg_0 = byte ptr 8
.text:00401140 push ebp
.text:00401141 mov ebp, esp
.text:00401143 push 0FFFFFFFFh
.text:00401145 push offset __ehhandler$??_GChinese@@UAEPAXI@Z
.text:0040114A mov eax, large fs:0
.text:00401150 push eax
.text:00401151 push esi
.text:00401152 mov eax, ___security_cookie
.text:00401157 xor eax, ebp
.text:00401159 push eax
.text:0040115A lea eax, [ebp+var_C]
.text:0040115D mov large fs:0, eax
.text:00401163 mov esi, this
;调用子类析构函数
.text:00401165 ; try {
.text:00401165 mov [ebp+var_4], 0
.text:0040116C mov dword ptr [esi], offset Chinese_vtable ;将子类虚表指针写入对象地址处 ①
.text:00401172 call Chinese_getClassName ;调用getClassName ②
.text:00401177 push eax
.text:00401178 push offset _Format ; "%s::showSpeak()\n"
.text:0040117D call _printf
.text:00401182 add esp, 8
.text:00401182 ; } // starts at 401165
;调用父类析构函数
.text:00401185 ; try {
.text:00401185 mov byte ptr [ebp+var_4], 1
.text:00401189 mov this, esi ; this
.text:0040118B mov dword ptr [esi], offset Person_vtable ;将父类虚表指针写入对象地址处 ③
.text:00401191 call Person_getClassName ;调用getClassName ④
.text:00401196 push eax
.text:00401197 push offset _Format ; "%s::showSpeak()\n"
.text:0040119C call _printf
.text:004011A1 add esp, 8
;释放内存空间
.text:004011A4 test [ebp+arg_0], 1 ; 检查delete标志
.text:004011A8 jz short loc_4011B5 ; 如果参数为1,则以对象首地址为目标释放内存
;否则本函数仅执行对象的析构函数
.text:004011AA push 4 ; __formal
.text:004011AC push esi ; block
.text:004011AD call ??3@YAXPAXI@Z ; 调用delete并平衡参数 ⑤
.text:004011B2 add esp, 8
.text:004011B5
.text:004011B5 loc_4011B5:
.text:004011B5 mov eax, esi
.text:004011B7 mov this, [ebp+var_C]
.text:004011BA mov large fs:0, this
.text:004011C1 pop this
.text:004011C2 pop esi
.text:004011C3 mov esp, ebp
.text:004011C5 pop ebp
.text:004011C6 retn 4
.text:004011C6 ; } // starts at 401185
.text:004011C6 ; } // starts at 401140
.text:004011C6 _Destructor_00401140 endp
;父类虚表
.rdata:004031B8 Person_vtable dd offset ??_EPerson@@UAEPAXI@Z ;虚析构函数
.rdata:004031BC dd offset showSpeak
.rdata:004031C0 dd offset Person_getClassName
.rdata:004031C4 align 10h
;子类虚表
.rdata:004031A8 Chinese_vtable dd offset _Destructor_00401140 ;虚析构函数
.rdata:004031AC dd offset showSpeak
.rdata:004031B0 dd offset Chinese_getClassName
.rdata:004031B4 dd offset ??_R4Person@@6B@ ; const Person::`RTTI Complete Object Locator'
显式调用析构函数的同时不能释放堆空间:
#include <stdio.h>
#include <new.h>
class Person{ // 基类——“人”类
public:
Person() {}
virtual ~Person() {}
virtual void showSpeak() {} // 纯虚函数,后面会讲解
};
class Chinese : public Person { // 中国人:继承自人类
public:
Chinese() {}
virtual ~Chinese() {}
virtual void showSpeak() { // 覆盖基类虚函数
printf("Speak Chinese\r\n");
}
};
int main(int argc, char* argv[]) {
Person *p = new Chinese;
p->showSpeak();
p->~Person(); //显式调用析构函数
//将堆内存中p指向的地址作为Chinese的新对象的首地址,调用Chinese的构造函数
//这样可以重复使用同一个堆内存,以节约内存空间
p = new (p) Chinese();
delete p;
return 0;
}
gcc_x86汇编标识:gcc编译器将析构函数和析构代理函数全部放入虚表,所以虚表中有两项析构函数
00401510 push ebp
00401511 mov ebp, esp
00401513 push ebx
00401514 and esp, 0FFFFFFF0h
00401517 sub esp, 20h
0040151A call ___main
0040151F mov dword ptr [esp], 4
00401526 call __Znwj ;调用new函数申请空间 ①
0040152B mov ebx, eax
0040152D mov ecx, ebx ;传递this指针
0040152F call __ZN7ChineseC1Ev ;调用构造函数,Chinese::Chinese(void) ②
00401534 mov [esp+1Ch], ebx
00401538 mov eax, [esp+1Ch]
0040153C mov eax, [eax]
0040153E add eax, 8 ;虚析构占两项,第三项为showSpeak
00401541 mov eax, [eax]
00401543 mov edx, [esp+1Ch]
00401547 mov ecx, edx ;传递this指针
00401549 call eax ;调用虚函数showSpeak ③
0040154B mov eax, [esp+1Ch]
0040154F mov eax, [eax]
00401551 mov eax, [eax] ;虚表第一项为析构函数,不释放堆空间
00401553 mov edx, [esp+1Ch]
00401557 mov ecx, edx ;传递this指针
00401559 call eax ;显式调用虚析构函数 ④
0040155B mov eax, [esp+1Ch]
0040155F mov [esp+4], eax ;参数2:this指针
00401563 mov dword ptr [esp], 4 ;参数1:大小为4字节
0040156A call __ZnwjPv ;调用new函数重用空间 ⑤
0040156F mov ebx, eax
00401571 mov ecx, ebx ;传递this指针
00401573 call __ZN7ChineseC1Ev ;调用构造函数,Chinese::Chinese(void) ⑥
00401578 mov [esp+1Ch], ebx
0040157C cmp dword ptr [esp+1Ch], 0
00401581 jz short loc_401596 ;堆申请成功释放堆空间
00401583 mov eax, [esp+1Ch]
00401587 mov eax, [eax]
00401589 add eax, 4
0040158C mov eax, [eax] ;虚表第二项为析构代理函数,释放堆空间
0040158E mov edx, [esp+1Ch]
00401592 mov ecx, edx ;传递this指针
00401594 call eax ;隐式调用虚析构函数 ⑦
00401596 mov eax, 0
0040159B mov ebx, [ebp-4]
0040159E leave
0040159F retn
;Chinese虚表有两个析构函数:
00412F8C off_412F8C dd offset __ZN6PersonD1Ev
;Person::~Person()
{
0040D87C push ebp
0040D87D mov ebp, esp
0040D87F sub esp, 4
0040D882 mov [ebp-4], ecx
0040D885 mov edx, offset off_412F8C
0040D88A mov eax, [ebp-4]
0040D88D mov [eax], edx
0040D88F nop
0040D890 leave
0040D891 retn ;不释放堆空间
}
00412F90 dd offset __ZN6PersonD0Ev
;Person::~Person()
{
0040D854 push ebp
0040D855 mov ebp, esp
0040D857 sub esp, 28h
0040D85A mov [ebp+var_C], ecx
0040D85D mov eax, [ebp+var_C]
0040D860 mov ecx, eax
0040D862 call __ZN6PersonD1Ev ;调用析构函数
0040D867 mov dword ptr [esp+4], 4
0040D86F mov eax, [ebp+var_C]
0040D872 mov [esp], eax ;void*
0040D875 call __ZdlPvj ;调用delete释放堆空间
0040D87A leave
0040D87B retn
}
三、多重继承
1. C类继承B类,C类继承A类
1)构造函数调用过程
①先调用父类Sofa的构造函数。
②在调用另一个父类Bed时,并不是直接将对象的首地址作为this指针传递,而是向后调整了父类Sofa的长度,以调整后的地址值作为this指针,最后再调用父类Bed的构造函数
③将父类的两个虚表指针依次写入对象首地址处
2)子类对象的内存构造
父类的虚表指针,在多重继承中,子类虚表指针的个数取决于继承的父类的个数,有几个父类便会出现几个虚表指针
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;
}
汇编标识:
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 1Ch
00401006 lea ecx, [ebp-1Ch] ;传递this指针
00401009 call sub_401090 ;调用构造函数
0040100E mov dword ptr [ebp-4], 0
00401015 lea ecx, [ebp-1Ch] ;传递this指针
00401018 call sub_401130 ;调用析构函数
0040101D mov eax, [ebp-4]
00401020 mov esp, ebp
00401022 pop ebp
00401023 retn
00401090 push ebp ;构造函数
00401091 mov ebp, esp
00401093 push ecx
00401094 mov [ebp-4], ecx
00401097 mov ecx, [ebp-4] ;以对象首地址作为this指针
0040109A call sub_401060 ;调用沙发父类的构造函数
0040109F mov ecx, [ebp-4]
004010A2 add ecx, 8 ;将this指针调整到第二个虚表指针的地址处
004010A5 call sub_401030 ;调用床父类的构造函数
004010AA mov eax, [ebp-4] ;获取对象的首地址
004010AD mov dword ptr [eax], offset ??_7SofaBed@@6B@ ;设置第一个虚表指针
004010B3 mov ecx, [ebp-4] ;获取对象的首地址
004010B6 mov dword ptr [ecx+8], offset ??_7SofaBed@@6B@_0 ;设置第二个虚表指针
004010BD mov edx, [ebp-4]
004010C0 mov dword ptr [edx+14h], 6
004010C7 mov eax, [ebp-4]
004010CA mov esp, ebp
004010CC pop ebp
004010CD retn
3)虚表指针的使用(父类指针访问子类对象)
在转换Bed指针时,会调整首地址并跳过第一个父类占用的空间。当使用父类Bed的指针访问Bed中实现的虚函数时,就不会错误地寻址到继承自Sofa类的成员变量了
多重继承子类对象转换为父类指针:
int main(int argc, char* argv[]) {
SofaBed sofabed;
Sofa *sofa = &sofabed;
Bed *bed = &sofabed;
return 0;
}
汇编标识:
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 28h
00401006 lea ecx, [ebp-28h] ;传递this指针
00401009 call sub_4010B0 ;调用构造函数
0040100E lea eax, [ebp-28h]
00401011 mov [ebp-0Ch], eax ;直接以首地址转换为父类指针,sofa=&sofabed
00401014 lea ecx, [ebp-28h]
00401017 test ecx, ecx
00401019 jz short loc_401026 ;检查对象首地址
0040101B lea edx, [ebp-28h] ;edx=this
0040101E add edx, 8
00401021 mov [ebp-4], edx ;即this+8,调整为Bed的指针,bed=&sofabed
00401024 jmp short loc_40102D
00401026 mov dword ptr [ebp-4], 0
0040102D mov eax, [ebp-4]
00401030 mov [ebp-10h], eax
00401033 mov dword ptr [ebp-8], 0
0040103A lea ecx, [ebp-28h] ;传递this指针
0040103D call sub_401150 ;调用析构函数
00401042 mov eax, [ebp-8]
00401045 mov esp, ebp
00401047 pop ebp
00401048 retn
4)多重继承的类对象析构函数
①将子类的虚表指针写入对象首地址处(两个地址都写)
②调用子类析构函数
③依次调用Bed类、Sofa类的析构函数
多重继承的类对象析构函数:
00401130 push ebp ;析构函数
00401131 mov ebp, esp
00401133 push ecx
00401134 mov [ebp-4], ecx
00401137 mov eax, [ebp-4] ;将第一个虚表设置为SofaBed的虚表
0040113A mov dword ptr [eax], offset ??_7SofaBed@@6B@
00401140 mov ecx, [ebp-4] ;将第二个虚表设置为SofaBed的虚表
00401143 mov dword ptr [ecx+8], offset ??_7SofaBed@@6B@_0
0040114A push offset aVirtualSofabed ;参数1:"virtual~SofaBed()\n"
0040114F call sub_401330 ;调用printf函数
00401154 add esp, 4
00401157 mov ecx, [ebp-4]
0040115A add ecx, 8 ;调整this指针到Bed父类,this+8
0040115D call sub_4010D0 ;调用父类Bed的析构函数
00401162 mov ecx, [ebp-4] ;this指针,无需调整
00401165 call sub_401100 ;调用父类Sofa的析构函数
0040116A mov esp, ebp
0040116C pop ebp
0040116D retn
四、单继承类和多继承类的区别总结
1. 单继承类
1)在类对象占用的内存空间中,只保存一份虚表指针
2)虚表中各项保存了类中各虚函数的首地址
3)构造时先构造父类,再构造自身,并且只调用一次父类构造函数
4)析构时先析构自身,再析构父类,并且只调用一次父类析构函数
2. 多重继承类
1)在类对象占用内存空间中,根据继承父类(有虚函数)个数保存对应的虚表指针。根据保存的虚表指针的个数,产生相应个数的虚表。
2)转换父类指针时,需要调整到对象的首地址。
3)构造时需要调用多个父类构造函数。构造时先构造继承列表中的第一个父类,然后依次调用到最后一个继承的父类构造函数。
4)析构时先析构自身,然后以构造函数相反的顺序调用所有父类的析构函数。
5)当对象作为成员时,整个类对象的内存结构和多重继承相似。当类中无虚函数时,整个类对象内存结构和多重继承完全一样。
当父类或成员对象存在虚函数时,通过观察虚表指针的位置和构造、析构函数中填写虚表指针的数目、顺序及目标地址,还原继承或成员关系
标签:多重,继承,text,mov,eax,ebp,子类,父类 From: https://www.cnblogs.com/XiuzhuKirakira/p/17157406.html