一、繼承的基本概念 繼承:子類繼承父類的屬性和行為 作用:代碼復用 繼承分類: 1. 按訪問屬性分為public、private、protected三類 1)public: 父類屬性無更改,pubic, private, protected 仍是自己本身(子類成員函數可以訪問父類的publi ...
一、繼承的基本概念
繼承:子類繼承父類的屬性和行為
作用:代碼復用
繼承分類:
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)當對象作為成員時,整個類對象的記憶體結構和多重繼承相似。當類中無虛函數時,整個類對象記憶體結構和多重繼承完全一樣。
當父類或成員對象存在虛函數時,通過觀察虛表指針的位置和構造、析構函數中填寫虛表指針的數目、順序及目標地址,還原繼承或成員關係