继承和多重继承

一、继承的基本概念

继承:子类继承父类的属性和行为

作用:代码复用

继承分类:

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)当对象作为成员时,整个类对象的内存结构和多重继承相似。当类中无虚函数时,整个类对象内存结构和多重继承完全一样。

​ 当父类或成员对象存在虚函数时,通过观察虚表指针的位置和构造、析构函数中填写虚表指针的数目、顺序及目标地址,还原继承或成员关系

posted @ 2023-02-26 19:28  修竹Kirakira  阅读(132)  评论(0编辑  收藏  举报