一、繼承的基本概念
? 繼承:子類繼承父類的屬性和行為
? 作用:代碼復用
繼承分類:
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)當物件作為成員時,整個類物件的記憶體結構和多重繼承相似,當類中無虛函式時,整個類物件記憶體結構和多重繼承完全一樣,
? 當父類或成員物件存在虛函式時,通過觀察虛表指標的位置和構造、解構式中填寫虛表指標的數目、順序及目標地址,還原繼承或成員關系
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/545112.html
標籤:其他
下一篇:1.模板
