繼承和多重繼承

来源:https://www.cnblogs.com/XiuzhuKirakira/archive/2023/02/26/17157406.html
-Advertisement-
Play Games

一、繼承的基本概念 ​ 繼承:子類繼承父類的屬性和行為 ​ 作用:代碼復用 繼承分類: 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)當對象作為成員時,整個類對象的記憶體結構和多重繼承相似。當類中無虛函數時,整個類對象記憶體結構和多重繼承完全一樣。

​ 當父類或成員對象存在虛函數時,通過觀察虛表指針的位置和構造、析構函數中填寫虛表指針的數目、順序及目標地址,還原繼承或成員關係


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ———————————————— 版權聲明:本文為CSDN博主「LW0512」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。 原文鏈接:https://blog.csdn.net/LW0512/article/details/120287699 ...
  • 常用API API概述 API全稱是應用程式編程介面,是Java寫好的程式,程式員可以直接調用。 Object類:toString方法 Object是祖宗類,它裡面的方法,一切子類對象都可以使用。 public String toString() 預設是返回當前對象的地址信息。 Student s ...
  • 跨平臺開發框架是客戶端領域的經典課題,幾乎從操作系統誕生開始就是我們軟體從業者們的思考命題。為了促進 Flutter 在 4 個端的成熟,企業微信研發團隊也和 Google 團隊針對電腦端 Flutter 穩定版的落地做了多輪技術溝通。終於在近期的版本實現同一個功能跨平臺 4 端同步上線。企業微信每... ...
  • 1. 緩衝I/O 1.1. 對於文件和套接字,壓縮和字元串編碼的操作,必須適當地對I/O進行緩衝 1.1.1. 兩個流操作的是位元組塊(來自緩衝流)而不是一系列的單位元組(來自ObjectOutputStream),它們會運行得更好 1.2. InputStream.read() 1.3. Output ...
  • 前言 ​ 經常在網上看到一些博客,介紹高併發。由於我們在實際開發過程遇到高併發的場景非常少,一直覺得很高大上, 那我們通過CPU,操作系統,和JDK 等方面揭開高併發的''神秘面紗''。 1: 什麼時候發生併發 ​ 在電腦中,CPU執行程式指令的。那我們看下下麵這個圖 思考: 當兩個程式在不同的C ...
  • Mybatis配置文件&SQL映射文件 1.配置文件-mybatis-config.xml 1.1基本說明 mybatis的核心配置文件(mybatis-config.xml),它的作用如配置jdbc連接信息,註冊mapper等,我們需要對這個配置文件有詳細的瞭解。 文檔地址:mybatis – M ...
  • 面向對象進階第三天 內部類 內部類是什麼? 類的5大成分(成員變數、成員方法、構造器、代碼塊、內部類)之一 類中的類 使用場景 當一個事物的內部,還有一個部分需要一個完整的結構進行描述時。 內部類有幾種 1、靜態內部類 是什麼?有static修飾,屬於外部類本身。 特點:只是位置在類裡面。類有的成分 ...
  • 模板 函數模板 template<typename T1,typename T2,……> 定義了必須使用,否則報錯 template<typename T> T add(T a,T b) { return a + b; } 根據具體的使用情況生成模板函數 add(1.1,2.1); //生成doub ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...