继承和多重继承

打印 上一主题 下一主题

主题 910|帖子 910|积分 2730

一、继承的基本概念

​        继承:子类继承父类的属性和行为
​        作用:代码复用
继承分类:

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类对象产生时调用成员对象的构造函数,编译器同样会提供默认的构造函数,以实现成员构造函数的调用
  1. class Base{...};
  2. class Derive  {
  3. public:
  4.   Base base; //原来的父类Base 成为成员对象
  5.   int derive; // 原来的子类派生数据
  6. };
复制代码
3)子类内存中的数据排列:先安排父类的数据,后安排子类新定义的数据
注意:当子类中有构造函数,父类无构造函数,不会给父类提供默认的构造函数
普通子类继承父类 c++ 代码示例:
  1. #include <stdio.h>
  2. class Base {  //基类定义
  3. public:
  4.   Base() {
  5.     printf("Base\n");
  6.   }
  7.   ~Base() {
  8.     printf("~Base\n");
  9.   }
  10.   void setNumber(int n) {
  11.     base = n;
  12.   }
  13.   int getNumber() {
  14.     return base;
  15.   }
  16. public:
  17.   int base;
  18. };
  19. class Derive : public Base  {  //派生类定义
  20. public:  void showNumber(int n) {
  21.     setNumber (n);
  22.     derive = n + 1;
  23.     printf("%d\n", getNumber());
  24.     printf("%d\n", derive);
  25.   }
  26. public:
  27.   int derive;
  28. };
  29. int main(int argc, char* argv[]) {
  30.   Derive derive;
  31.   derive.showNumber(argc);
  32.   return 0;
  33. }
复制代码
汇编标识:
  1. 00401000  push    ebp
  2. 00401001  mov     ebp, esp
  3. 00401003  sub     esp, 0Ch
  4. 00401006  lea     ecx, [ebp-0Ch]    ;获取对象首地址作为this指针
  5. 00401009  call    sub_401050        ;调用类Derive的默认构造函数 ①
  6. 0040100E  mov     eax, [ebp+8]
  7. 00401011  push    eax               ;参数2:argc
  8. 00401012  lea     ecx, [ebp-0Ch]    ;参数1:传入this指针
  9. 00401015  call    sub_4010E0        ;调用成员函数showNumber ②
  10. 0040101A  mov     dword ptr [ebp-4], 0
  11. 00401021  lea     ecx, [ebp-0Ch]    ;传入this指针
  12. 00401024  call    sub_401090        ;调用类Derive的默认析构函数 ③
  13. 00401029  mov     eax, [ebp-4]
  14. 0040102C  mov     esp, ebp
  15. 0040102E  pop     ebp
  16. 0040102F  retn
  17. 00401050  push    ebp               ;子类Derive的默认构造函数分析
  18. 00401051  mov     ebp, esp
  19. 00401053  push    ecx
  20. 00401054  mov     [ebp-4], ecx
  21. 00401057  mov     ecx, [ebp-4]      ;以子类对象首地址作为父类的this指针 ①
  22. 0040105A  call    sub_401030        ;调用父类构造函数
  23. 0040105F  mov     eax, [ebp-4]
  24. 00401062  mov     esp, ebp
  25. 00401064  pop     ebp
  26. 00401065  retn
  27. 00401090  push    ebp               ;子类Derive的默认析构函数分析
  28. 00401091  mov     ebp, esp
  29. 00401093  push    ecx
  30. 00401094  mov     [ebp-4], ecx
  31. 00401097  mov     ecx, [ebp-4]      ;以子类对象首地址作为父类的this指针 ①
  32. 0040109A  call    sub_401070        ;调用父类析构函数
  33. 0040109F  mov     esp, ebp
  34. 004010A1  pop     ebp
  35. 004010A2  retn
复制代码
​        子类中定义了其他对象作为成员,并在初始化列表中指定了某个成员的初始化值时:先构造父类,然后按声明顺序构造成员对象和初始化列表中指定的成员,最后构造自己
类中定义了其他对象作为成员,并在初始化列表中指定了某个成员的初始化值时的 c++ 示例代码:
  1. class Member{
  2. public:
  3.   Member()  {
  4.     member = 0;
  5.   }
  6.   int member;
  7. };
  8. class Derive : public Base  {
  9. public:
  10.   Derive():derive(1)  {
  11.     printf("使用初始化列表\n");
  12.   }
  13. public:
  14.   Member member;  //类中定义其他对象作为成员
  15.   int derive;
  16. };
  17. int main(int argc, char* argv[]) {
  18.   Derive derive;
  19.   return 0;
  20. }
复制代码
汇编标识:
  1. 00401000  push    ebp
  2. 00401001  mov     ebp, esp
  3. 00401003  sub     esp, 10h
  4. 00401006  lea     ecx, [ebp-10h]       ;传递this指针
  5. 00401009  call    sub_401050           ;调用Derive的构造函数 ①
  6. 0040100E  mov     dword ptr [ebp-4], 0
  7. 00401015  lea     ecx, [ebp-10h]       ;传递this指针
  8. 00401018  call    sub_4010D0           ;调用Derive的析构函数 ⑥
  9. 0040101D  mov     eax, [ebp-4]
  10. 00401020  mov     esp, ebp
  11. 00401022  pop     ebp
  12. 00401023  retn
  13. 00401050  push    ebp                  ; Derive构造函数
  14. 00401051  mov     ebp, esp
  15. 00401053  push    ecx
  16. 00401054  mov     [ebp-4], ecx         ;[ebp-4]保存了this指针
  17. 00401057  mov     ecx, [ebp-4]         ;传递this指针
  18. 0040105A  call    sub_401030           ;调用父类构造函数 ②
  19. 0040105F  mov     ecx, [ebp-4]
  20. 00401062  add     ecx, 4               ;根据this指针调整到类中定义的对象member的首地址处
  21. 00401065  call    sub_401090           ;调用Member构造函数 ③
  22. 0040106A  mov     eax, [ebp-4]
  23. 0040106D  mov     dword ptr [eax+8], 1 ;执行初始化列表 ④,this指针传递给eax后,[eax+8]是对成员数据derive进行寻址
  24. 00401074  push    offset unk_412170    ;最后才是执行Derive的构造代码 ⑤
  25. 00401079  call    sub_401130           ;调用printf函数
  26. 0040107E  add     esp, 4
  27. 00401081  mov     eax, [ebp-4]
  28. 00401084  mov     esp, ebp
  29. 00401086  pop     ebp
  30. 00401087  retn
复制代码
2. 使用父类指针访问子类对象

因为父类对象的长度不超过子类对象,使用父类指针访问子类对象不会造成访问越界
子类调用父类函数(showNumber函数汇编标识)
  1. 004010E0  push    ebp                    ;showNumber函数
  2. 004010E1  mov     ebp, esp
  3. 004010E3  push    ecx
  4. 004010E4  mov     [ebp-4], ecx           ;[ebp-4]中保留了this指针
  5. 004010E7  mov     eax, [ebp+8]
  6. 004010EA  push    eax                    ;参数2:n
  7. 004010EB  mov     ecx, [ebp-4]           ;参数1:因为this指针同时也是对象中父类部分的首地址
  8.                                          ;所以在调用父类成员函数时,this指针的值和子类对象等同 ①
  9. 004010EE  call    sub_4010C0             ;调用基类成员函数setNumber ②
  10. 004010F3  mov     ecx, [ebp+8]
  11. 004010F6  add     ecx, 1                 ;将参数n值加1
  12. 004010F9  mov     edx, [ebp-4]           ;edx拿到this指针
  13. 004010FC  mov     [edx+4], ecx           ;参考内存结构,edx+4是子类成员derive的地址,derive=n+1
  14. 004010FF  mov     ecx, [ebp-4]           ;传递this指针
  15. 00401102  call    sub_4010B0             ;调用基类成员函数getNumber ③
  16. 00401107  push    eax                    ;参数2:Base.base
  17. 00401108  push    offset aD              ;参数1:"%d\n"
  18. 0040110D  call    sub_401170             ;调用printf函数
  19. 00401112  add     esp, 8
  20. 00401115  mov     eax, [ebp-4]
  21. 00401118  mov     ecx, [eax+4]
  22. 0040111B  push    ecx                    ;参数2:derive
  23. 0040111C  push    offset aD              ;参数1:"%d\n"
  24. 00401121  call    sub_401170             ;调用printf函数
  25. 00401126  add     esp, 8
  26. 00401129  mov     esp, ebp
  27. 0040112B  pop     ebp
  28. 0040112C  retn    4
复制代码
父类中成员函数在子类中没有被定义,但在子类中可以使用父类的公有函数。编译器如何实现正确匹配?
​        如果使用对象或对象的指针调用成员函数,编译器可根据对象所属作用域通过“名称粉碎法”实现正确匹配。在成员函数中调用其他成员函数时,可匹配当前作用域
名称粉碎(name mangling):
C++编译器对函数名称的一种处理方式,即在编译时对函数名进行重组,新名称会包含函数的作用域、原函数名、每个参数的类型、返回值以及调用约定等信息
3. 使用子类指针访问父类对象

如果访问的成员数据是父类对象定义的,则不会出错;如果访问的是子类派生的成员数据,则会造成访问越界
子类指针访问父类对象(可能出现访问越界)
  1. int main(int argc, char* argv[]) {
  2.   int n = 0x12345678;
  3.   Base  base;
  4.   Derive *derive = (Derive*)&base;
  5.   printf("%x\n", derive->derive);
  6.   return 0;
  7. }
复制代码
汇编标识:
  1. 00401000  push    ebp
  2. 00401001  mov     ebp, esp
  3. 00401003  sub     esp, 10h
  4. 00401006  mov     dword ptr [ebp-10h], 12345678h ;局部变量赋初值
  5. 0040100D  lea     ecx, [ebp-4]                   ;传递this指针
  6. 00401010  call    sub_401050                     ;调用构造函数
  7. 00401015  lea     eax, [ebp-4]
  8. 00401018  mov     [ebp-8], eax                   ;指针变量[ebp-8]得到base的地址
  9. 0040101B  mov     ecx, [ebp-8]
  10. 0040101E  mov     edx, [ecx+4]                   ;注意,ecx中保留了base的地址,而[ecx+4]的访问超出了base的内存范围
  11. 00401021  push    edx
  12. 00401022  push    offset unk_412160
  13. 00401027  call    sub_4010D0                     ;调用printf函数
  14. 0040102C  add     esp, 8
  15. 0040102F  mov     dword ptr [ebp-0Ch], 0
  16. 00401036  lea     ecx, [ebp-4]                   ;传递this指针
  17. 00401039  call    sub_401070                     ;调用析构函数
  18. 0040103E  mov     eax, [ebp-0Ch]
  19. 00401041  mov     esp, ebp
  20. 00401043  pop     ebp
  21. 00401044  retn
复制代码
4. 多态

​        虚函数的调用过程使用了间接寻址方式,而非直接调用函数地址
1)父类指针指向子类对象可以调用子类对象的虚函数的原因:

​        由于虚表采用间接调用机制,因此在使用父类指针person调用虚函数时,没有依照其作用域调用Person类中定义的成员函数showSpeak
2)父类构造函数中调用虚函数

​        ①当父类的子类产生对象时,会在调用子类构造函数前优先调用父类构造函数,并以子类对象的首地址作为this指针传递给父类构造函数
​        ②在父类构造函数中,会先初始化子类虚表指针为父类的虚表首地址
​        ③如果在父类构造函数中调用虚函数,虽然虚表指针属于子类对象,但指向父类的虚表首地址,可判断虚表所属作用域与当前作用域相同,转换成直接调用方式,最终造成构造函数内的虚函数失效。
  1. class Person  {
  2. public:
  3.   Person()  {
  4.     showSpeak(); //调用虚函数,不多态
  5.   }
  6.   virtual ~Person() {
  7.   }
  8.   virtual void showSpeak() {
  9.     printf("Speak No\n");
  10.   }
  11. };
复制代码

这样的意义
​        按C++规定的构造顺序,父类构造函数会在子类构造函数之前运行,在执行父类构造函数时将虚表指针修改为当前类的虚表指针,也就是父类的虚表指针,因此导致虚函数的特性失效。如果父类构造函数内部存在虚函数调用,这样的顺序能防止在子类中构造父类时,父类根据虚表错误地调用子类的成员函数。
为什么不直接把构造函数或析构函数中的虚函数调用修改为直接调用方式使构造和析构函数中的虚函数多态性失效
​        因为其他成员函数仍可以间接调用本类中声明的其他虚函数形成多态,如果子类对象的虚表指针没有更换为父类的虚表指针,会导致在访问子类的虚表后调用到子类中的对应虚函数
3)父类析构函数中调用虚函数

​        ①子类对象析构时,设置虚表指针为自身虚表,再调用自身的析构函数
​        ②如果有成员对象,则按声明的顺序以倒序方式依次调用成员对象的析构函数
​        ③最后,调用父类析构函数。在调用父类的析构函数时,会设置虚表指针为父类自身的虚表
4)将析构函数定义为虚函数的原因

​        当使用父类指针指向子类堆对象时,使用delete函数释放对象的空间时,如果析构函数没有被定义为虚函数,那么编译器会按指针的类型调用父类的析构函数,从而引发错误。而使用了虚析构函数后,会访问虚表并调用对象的析构函数
  1. //没有声明为虚析构函数
  2. Person * p = new Chinese;
  3. delete p;   //部分代码分析略
  4. 00D85714  mov         ecx,dword ptr [ebp+FFFFFF08h]  ;直接调用父类的析构函数
  5. 00D8571A  call        00D81456
  6. // 声明为虚析构函数
  7. Person * p = new Chinese;
  8. delete p;   //部分代码分析略
  9. 000B5716  mov         ecx,dword ptr [ebp+FFFFFF08h] ;获取p并保存至ecx
  10. 000B571C  mov         edx,dword ptr [ecx]           ;取得虚表指针
  11. 000B571E  mov         ecx,dword ptr [ebp+FFFFFF08h] ;传递this指针
  12. 000B5724  mov         eax,dword ptr [edx]           ;间接调用虚析构函数
  13. 000B5726  call        eax
复制代码
注意
​        当没有使用对象指针或者对象引用时,调用虚函数指令的寻址方式为直接调用,从而无法构成多态
5)在 IDA 中综合分析

以下代码的整体流程
①申请堆空间
​        ②调用父类的构造函数
​          a.将父类的虚表指针写入对象首地址处
​          b.调用父类的showSpeak函数(直接调用)
​        ③调用子类的构造函数
​          a.将子类的虚表指针写入对象首地址处
​          b.调用子类的showSpeak函数(直接调用)
​        ④间接调用子类的showSpeak函数(查表,此时虚表指针为子类)
​        ⑤传入delete标志,间接调用虚表中的析构代理函数(查表,此时虚表指针为子类)
​          a.调用子类析构函数
​            ⅰ.将子类虚表指针写入对象首地址处
​            ⅱ.调用getClassName函数(直接调用)
​          b.调用父类析构函数
​            ⅰ.将父类虚表指针写入对象首地址处
  1.     ⅱ.调用getClassName函数(直接调用)
复制代码
​          c.根据标识调用delete释放内存空间
为什么调用析构代理函数时要压入是否释放内存的标志
​        ①因为析构函数和释放内存是两件事,可以选择只调用析构函数而不释放内存空间。
​        ②因为显式调用析构函数时不能马上释放堆内存,所以在析构函数的代理函数中通过一个参数控制是否释放内存,便于程序员管理析构函数的调用
为什么编译器要在子类析构函数中再次将虚表设置为子类虚表?(即上述标红处)
​        因为编译器无法预知这个子类以后是否会被其他类继承,如果被继承,原来的子类就成了父类,当前对象的析构函数开始执行时,其虚表也是当前对象的,所以执行到父类的析构函数时,虚表必须改写为父类的虚表。故在每个对象的析构函数内,要加入自己虚表的代码
c++示例代码:
  1. #include <stdio.h>
  2. class  Person{  //基类:人类
  3. public:
  4.   Person() {
  5.     showSpeak();  //注意,构造函数调用了虚函数
  6.   }
  7.   virtual ~Person(){
  8.     showSpeak();  //注意,析构函数调用了虚函数
  9.   }
  10.   virtual void showSpeak(){
  11.     //在这个函数里调用了其他的虚函数getClassName();
  12.     printf("%s::showSpeak()\n", getClassName());
  13.     return;
  14.   }
  15.   virtual const char* getClassName()
  16.   {
  17.     return "Person";
  18.   }
  19. };
  20. class Chinese : public Person  {  //中国人,继承自"人"类
  21. public:
  22.   Chinese()  {
  23.     showSpeak();
  24.   }
  25.   virtual ~Chinese()  {
  26.     showSpeak();
  27.   }
  28.   virtual const char* getClassName()  {
  29.      return "Chinese";
  30.   }
  31. };
  32. int main(int argc, char* argv[])  {
  33.   Person *p = new Chinese;
  34.   p->showSpeak();
  35.   delete p;
  36.   return 0;
  37. }
复制代码
vs_x86汇编标识:
  1. .text:004011D0 block           = dword ptr -10h
  2. .text:004011D0 var_C           = dword ptr -0Ch
  3. .text:004011D0 var_4           = dword ptr -4
  4. .text:004011D0 argc            = dword ptr  8
  5. .text:004011D0 argv            = dword ptr  0Ch
  6. .text:004011D0
  7. .text:004011D0 ; FUNCTION CHUNK AT .text:00402070 SIZE 00000017 BYTES
  8. .text:004011D0
  9. .text:004011D0 ; __unwind { // __ehhandler$_main
  10. .text:004011D0                 push    ebp
  11. .text:004011D1                 mov     ebp, esp
  12. .text:004011D3                 push    0FFFFFFFFh
  13. .text:004011D5                 push    offset __ehhandler$_main
  14. .text:004011DA                 mov     eax, large fs:0
  15. .text:004011E0                 push    eax
  16. .text:004011E1                 push    ecx
  17. .text:004011E2                 push    esi
  18. .text:004011E3                 mov     eax, ___security_cookie
  19. .text:004011E8                 xor     eax, ebp
  20. .text:004011EA                 push    eax
  21. .text:004011EB                 lea     eax, [ebp+var_C]
  22. .text:004011EE                 mov     large fs:0, eax
  23. .text:004011F4                 push    4               ; size
  24. .text:004011F6                 call    ??2@YAPAXI@Z    ; 申请4字节堆空间 ①
  25. .text:004011FB                 mov     esi, eax        ; esi保存new调用的返回值
  26. .text:004011FD                 add     esp, 4          ; 平衡new调用的参数
  27. .text:00401200                 mov     [ebp+block], esi
  28. ;在构造函数中先填写父类的虚表,然后按继承的层次关系逐层填写子类的虚表
  29. ;内联父类构造函数
  30. .text:00401203 ;   try {
  31. .text:00401203                 mov     [ebp+var_4], 0  ; 调用父类的构造函数 ②
  32. .text:0040120A                 mov     ecx, esi        ; this
  33. .text:0040120C                 mov     dword ptr [esi], offset Person_vtable ; 将虚表指针写入对象首地址 ③
  34. .text:00401212                 call    Person_getClassName ;调用父类的getClassName(直接调用,此时对象首地址处为父类虚表) ④
  35. .text:00401217                 push    eax
  36. .text:00401218                 push    offset _Format  ; "%s::showSpeak()\n"
  37. .text:0040121D                 call    _printf
  38. .text:00401222                 add     esp, 8
  39. .text:00401222 ;   } // starts at 401203
  40. ;内联子类构造函数
  41. .text:00401225 ;   try {
  42. .text:00401225                 mov     byte ptr [ebp+var_4], 1 ; 调用子类的构造函数 ⑤
  43. .text:00401229                 mov     ecx, esi        ; this
  44. .text:0040122B                 mov     dword ptr [esi], offset Chinese_vtable ; 将虚表指针写入对象首地址 ⑥
  45. .text:00401231                 call    Chinese_getClassName ;调用子类的getClassName(直接调用) ⑦
  46. .text:00401236                 push    eax
  47. .text:00401237                 push    offset _Format  ; "%s::showSpeak()\n"
  48. .text:0040123C                 call    _printf
  49. .text:0040123C ;   } // starts at 401225
  50. .text:00401241                 mov     [ebp+var_4], 0FFFFFFFFh
  51. .text:00401248                 add     esp, 8
  52. .text:0040124B                 mov     eax, [esi]      ; 得到虚表指针,此时虚表指针为子类的虚表指针
  53. .text:0040124D                 mov     ecx, esi        ; 传递this指针
  54. .text:0040124F                 call    dword ptr [eax+4] ; 间接调用虚表第二项的函数,即showspeak ⑧
  55. .text:00401252                 mov     eax, [esi]
  56. .text:00401254                 mov     ecx, esi
  57. .text:00401256                 push    1               ;传入delete释放标志,标识要释放内存空间,否则只调用析构函数
  58. .text:00401258                 call    dword ptr [eax] ; 间接调用虚表中的虚析构函数,此时虚表指针为子类 ⑨
  59. .text:0040125A                 xor     eax, eax
  60. .text:0040125C                 mov     ecx, [ebp+var_C]
  61. .text:0040125F                 mov     large fs:0, ecx
  62. .text:00401266                 pop     ecx
  63. .text:00401267                 pop     esi
  64. .text:00401268                 mov     esp, ebp
  65. .text:0040126A                 pop     ebp
  66. .text:0040126B                 retn
  67. .text:0040126B ; } // starts at 4011D0
  68. .text:0040126B _main           endp
  69. ; void __thiscall showSpeak(Person *this)
  70. .text:00401090 showSpeak       proc near
  71. .text:00401090                 mov     eax, [this]
  72. .text:00401092                 call    dword ptr [eax+8] ; 间接调用getClassName函数
  73. .text:00401095                 push    eax
  74. .text:00401096                 push    offset _Format  ; "%s::showSpeak()\n"
  75. .text:0040109B                 call    _printf
  76. .text:004010A0                 add     esp, 8
  77. .text:004010A3                 retn
  78. .text:004010A3 showSpeak       endp
  79. ;子类的虚析构代理函数
  80. .text:00401140 _Destructor_00401140 proc near
  81. .text:00401140 var_C           = dword ptr -0Ch
  82. .text:00401140 var_4           = dword ptr -4
  83. .text:00401140 arg_0           = byte ptr  8
  84. .text:00401140                 push    ebp
  85. .text:00401141                 mov     ebp, esp
  86. .text:00401143                 push    0FFFFFFFFh
  87. .text:00401145                 push    offset __ehhandler$??_GChinese@@UAEPAXI@Z
  88. .text:0040114A                 mov     eax, large fs:0
  89. .text:00401150                 push    eax
  90. .text:00401151                 push    esi
  91. .text:00401152                 mov     eax, ___security_cookie
  92. .text:00401157                 xor     eax, ebp
  93. .text:00401159                 push    eax
  94. .text:0040115A                 lea     eax, [ebp+var_C]
  95. .text:0040115D                 mov     large fs:0, eax
  96. .text:00401163                 mov     esi, this
  97. ;调用子类析构函数
  98. .text:00401165 ;   try {
  99. .text:00401165                 mov     [ebp+var_4], 0
  100. .text:0040116C                 mov     dword ptr [esi], offset Chinese_vtable ;将子类虚表指针写入对象地址处 ①
  101. .text:00401172                 call    Chinese_getClassName                   ;调用getClassName ②
  102. .text:00401177                 push    eax
  103. .text:00401178                 push    offset _Format  ; "%s::showSpeak()\n"
  104. .text:0040117D                 call    _printf
  105. .text:00401182                 add     esp, 8
  106. .text:00401182 ;   } // starts at 401165
  107. ;调用父类析构函数
  108. .text:00401185 ;   try {
  109. .text:00401185                 mov     byte ptr [ebp+var_4], 1
  110. .text:00401189                 mov     this, esi       ; this
  111. .text:0040118B                 mov     dword ptr [esi], offset Person_vtable ;将父类虚表指针写入对象地址处 ③
  112. .text:00401191                 call    Person_getClassName                   ;调用getClassName ④
  113. .text:00401196                 push    eax
  114. .text:00401197                 push    offset _Format  ; "%s::showSpeak()\n"
  115. .text:0040119C                 call    _printf
  116. .text:004011A1                 add     esp, 8
  117. ;释放内存空间
  118. .text:004011A4                 test    [ebp+arg_0], 1  ; 检查delete标志
  119. .text:004011A8                 jz      short loc_4011B5 ; 如果参数为1,则以对象首地址为目标释放内存
  120.                                                         ;否则本函数仅执行对象的析构函数
  121. .text:004011AA                 push    4               ; __formal
  122. .text:004011AC                 push    esi             ; block
  123. .text:004011AD                 call    ??3@YAXPAXI@Z   ; 调用delete并平衡参数 ⑤
  124. .text:004011B2                 add     esp, 8
  125. .text:004011B5
  126. .text:004011B5 loc_4011B5:
  127. .text:004011B5                 mov     eax, esi
  128. .text:004011B7                 mov     this, [ebp+var_C]
  129. .text:004011BA                 mov     large fs:0, this
  130. .text:004011C1                 pop     this
  131. .text:004011C2                 pop     esi
  132. .text:004011C3                 mov     esp, ebp
  133. .text:004011C5                 pop     ebp
  134. .text:004011C6                 retn    4
  135. .text:004011C6 ;   } // starts at 401185
  136. .text:004011C6 ; } // starts at 401140
  137. .text:004011C6 _Destructor_00401140 endp
  138. ;父类虚表
  139. .rdata:004031B8 Person_vtable   dd offset ??_EPerson@@UAEPAXI@Z ;虚析构函数
  140. .rdata:004031BC                 dd offset showSpeak
  141. .rdata:004031C0                 dd offset Person_getClassName
  142. .rdata:004031C4                 align 10h
  143. ;子类虚表
  144. .rdata:004031A8 Chinese_vtable  dd offset _Destructor_00401140 ;虚析构函数
  145. .rdata:004031AC                 dd offset showSpeak
  146. .rdata:004031B0                 dd offset Chinese_getClassName
  147. .rdata:004031B4                 dd offset ??_R4Person@@6B@ ; const Person::`RTTI Complete Object Locator'
复制代码
显式调用析构函数的同时不能释放堆空间:
  1. #include <stdio.h>
  2. #include <new.h>
  3. class Person{                        // 基类——“人”类
  4. public:
  5.   Person() {}
  6.   virtual ~Person() {}
  7.   virtual void showSpeak() {}        // 纯虚函数,后面会讲解
  8. };
  9. class Chinese : public Person {      // 中国人:继承自人类
  10. public:
  11.   Chinese() {}
  12.   virtual ~Chinese() {}
  13.   virtual void showSpeak() {         // 覆盖基类虚函数
  14.     printf("Speak Chinese\r\n");
  15.   }
  16. };
  17. int main(int argc, char* argv[]) {
  18.   Person *p = new Chinese;
  19.   p->showSpeak();
  20.   p->~Person(); //显式调用析构函数
  21.   //将堆内存中p指向的地址作为Chinese的新对象的首地址,调用Chinese的构造函数
  22.   //这样可以重复使用同一个堆内存,以节约内存空间
  23.   p = new (p) Chinese();
  24.   delete p;
  25.   return 0;
  26. }
复制代码
gcc_x86汇编标识:gcc编译器将析构函数和析构代理函数全部放入虚表,所以虚表中有两项析构函数
  1. 00401510    push    ebp
  2. 00401511    mov     ebp, esp
  3. 00401513    push    ebx
  4. 00401514    and     esp, 0FFFFFFF0h
  5. 00401517    sub     esp, 20h
  6. 0040151A    call    ___main
  7. 0040151F    mov     dword ptr [esp], 4
  8. 00401526    call    __Znwj                  ;调用new函数申请空间 ①
  9. 0040152B    mov     ebx, eax
  10. 0040152D    mov     ecx, ebx                ;传递this指针
  11. 0040152F    call    __ZN7ChineseC1Ev        ;调用构造函数,Chinese::Chinese(void) ②
  12. 00401534    mov     [esp+1Ch], ebx
  13. 00401538    mov     eax, [esp+1Ch]
  14. 0040153C    mov     eax, [eax]
  15. 0040153E    add     eax, 8                  ;虚析构占两项,第三项为showSpeak
  16. 00401541    mov     eax, [eax]
  17. 00401543    mov     edx, [esp+1Ch]
  18. 00401547    mov     ecx, edx                ;传递this指针
  19. 00401549    call    eax                     ;调用虚函数showSpeak ③
  20. 0040154B    mov     eax, [esp+1Ch]
  21. 0040154F    mov     eax, [eax]
  22. 00401551    mov     eax, [eax]              ;虚表第一项为析构函数,不释放堆空间
  23. 00401553    mov     edx, [esp+1Ch]
  24. 00401557    mov     ecx, edx                ;传递this指针
  25. 00401559    call    eax                     ;显式调用虚析构函数 ④
  26. 0040155B    mov     eax, [esp+1Ch]
  27. 0040155F    mov     [esp+4], eax            ;参数2:this指针
  28. 00401563    mov     dword ptr [esp], 4      ;参数1:大小为4字节
  29. 0040156A    call    __ZnwjPv                ;调用new函数重用空间 ⑤
  30. 0040156F    mov     ebx, eax
  31. 00401571    mov     ecx, ebx                ;传递this指针
  32. 00401573    call    __ZN7ChineseC1Ev        ;调用构造函数,Chinese::Chinese(void) ⑥
  33. 00401578    mov     [esp+1Ch], ebx
  34. 0040157C    cmp     dword ptr [esp+1Ch], 0
  35. 00401581    jz      short loc_401596        ;堆申请成功释放堆空间
  36. 00401583    mov     eax, [esp+1Ch]
  37. 00401587    mov     eax, [eax]
  38. 00401589    add     eax, 4
  39. 0040158C    mov     eax, [eax]              ;虚表第二项为析构代理函数,释放堆空间
  40. 0040158E    mov     edx, [esp+1Ch]
  41. 00401592    mov     ecx, edx                ;传递this指针
  42. 00401594    call    eax                     ;隐式调用虚析构函数 ⑦
  43. 00401596    mov     eax, 0
  44. 0040159B    mov     ebx, [ebp-4]
  45. 0040159E    leave
  46. 0040159F    retn            
  47. ;Chinese虚表有两个析构函数:
  48. 00412F8C off_412F8C    dd offset __ZN6PersonD1Ev
  49. ;Person::~Person()
  50. {
  51. 0040D87C                 push    ebp
  52. 0040D87D                 mov     ebp, esp
  53. 0040D87F                 sub     esp, 4
  54. 0040D882                 mov     [ebp-4], ecx
  55. 0040D885                 mov     edx, offset off_412F8C
  56. 0040D88A                 mov     eax, [ebp-4]
  57. 0040D88D                 mov     [eax], edx
  58. 0040D88F                 nop
  59. 0040D890                 leave
  60. 0040D891                 retn                         ;不释放堆空间
  61. }
  62. 00412F90                 dd offset __ZN6PersonD0Ev
  63. ;Person::~Person()
  64. {
  65. 0040D854                 push    ebp
  66. 0040D855                 mov     ebp, esp
  67. 0040D857                 sub     esp, 28h
  68. 0040D85A                 mov     [ebp+var_C], ecx
  69. 0040D85D                 mov     eax, [ebp+var_C]
  70. 0040D860                 mov     ecx, eax
  71. 0040D862                 call    __ZN6PersonD1Ev      ;调用析构函数
  72. 0040D867                 mov     dword ptr [esp+4], 4
  73. 0040D86F                 mov     eax, [ebp+var_C]
  74. 0040D872                 mov     [esp], eax           ;void*
  75. 0040D875                 call    __ZdlPvj             ;调用delete释放堆空间
  76. 0040D87A                 leave
  77. 0040D87B                 retn
  78. }
复制代码
三、多重继承

1. C类继承B类,C类继承A类

1)构造函数调用过程

​        ①先调用父类Sofa的构造函数。
​        ②在调用另一个父类Bed时,并不是直接将对象的首地址作为this指针传递,而是向后调整了父类Sofa的长度,以调整后的地址值作为this指针,最后再调用父类Bed的构造函数
​        ③将父类的两个虚表指针依次写入对象首地址处
2)子类对象的内存构造

​        父类的虚表指针,在多重继承中,子类虚表指针的个数取决于继承的父类的个数,有几个父类便会出现几个虚表指针
c++代码示例:
  1. #include <stdio.h>
  2. class Sofa {
  3. public:
  4.   Sofa() {
  5.     color = 2;
  6.   }
  7.   virtual ~Sofa()  {                        // 沙发类虚析构函数
  8.     printf("virtual ~Sofa()\n");
  9.   }
  10.   virtual int getColor()  {                 // 获取沙发颜色
  11.     return color;
  12.   }
  13.   virtual int sitDown() {                   // 沙发可以坐下休息
  14.     return printf("Sit down and rest your legs\r\n");
  15.   }
  16. protected:
  17.   int color;                                // 沙发类成员变量
  18. };
  19. //定义床类
  20. class Bed {
  21. public:
  22.   Bed() {
  23.     length = 4;
  24.     width = 5;
  25.   }
  26.   virtual ~Bed() {                          //床类虚析构函数
  27.     printf("virtual ~Bed()\n");
  28.   }
  29.   virtual int getArea() {                   //获取床面积
  30.     return length * width;
  31.   }
  32.   virtual int sleep() {                     //床可以用来睡觉
  33.     return printf("go to sleep\r\n");
  34.   }
  35. protected:
  36.   int length; //床类成员变量
  37.   int width;
  38. };
  39. //子类沙发床定义,派生自Sofa类和Bed类
  40. class SofaBed : public Sofa, public Bed{
  41. public:
  42.   SofaBed() {
  43.     height = 6;
  44.   }
  45.   virtual ~SofaBed(){                       //沙发床类的虚析构函数
  46.     printf("virtual ~SofaBed()\n");
  47.   }
  48.   virtual int sitDown() {                   //沙发可以坐下休息
  49.     return printf("Sit down on the sofa bed\r\n");
  50.   }
  51.   virtual int sleep() {                     //床可以用来睡觉
  52.     return printf("go to sleep on the sofa bed\r\n");
  53.   }
  54.   virtual int getHeight() {
  55.     return height;
  56.   }
  57. protected:
  58.   int height;
  59. };
  60. int main(int argc, char* argv[]) {
  61.   SofaBed sofabed;
  62.   return 0;
  63. }
复制代码
汇编标识
  1. 00401000  push    ebp
  2. 00401001  mov     ebp, esp
  3. 00401003  sub     esp, 1Ch
  4. 00401006  lea     ecx, [ebp-1Ch]               ;传递this指针
  5. 00401009  call    sub_401090                   ;调用构造函数
  6. 0040100E  mov     dword ptr [ebp-4], 0
  7. 00401015  lea     ecx, [ebp-1Ch]               ;传递this指针
  8. 00401018  call    sub_401130                   ;调用析构函数
  9. 0040101D  mov     eax, [ebp-4]
  10. 00401020  mov     esp, ebp
  11. 00401022  pop     ebp
  12. 00401023  retn
  13. 00401090  push    ebp                          ;构造函数
  14. 00401091  mov     ebp, esp
  15. 00401093  push    ecx
  16. 00401094  mov     [ebp-4], ecx
  17. 00401097  mov     ecx, [ebp-4]                 ;以对象首地址作为this指针
  18. 0040109A  call    sub_401060                   ;调用沙发父类的构造函数
  19. 0040109F  mov     ecx, [ebp-4]
  20. 004010A2  add     ecx, 8                       ;将this指针调整到第二个虚表指针的地址处
  21. 004010A5  call    sub_401030                   ;调用床父类的构造函数
  22. 004010AA  mov     eax, [ebp-4]                 ;获取对象的首地址
  23. 004010AD  mov     dword ptr [eax], offset ??_7SofaBed@@6B@       ;设置第一个虚表指针
  24. 004010B3  mov     ecx, [ebp-4]                 ;获取对象的首地址
  25. 004010B6 mov      dword ptr [ecx+8], offset ??_7SofaBed@@6B@_0 ;设置第二个虚表指针
  26. 004010BD  mov     edx, [ebp-4]
  27. 004010C0  mov     dword ptr [edx+14h], 6
  28. 004010C7  mov     eax, [ebp-4]
  29. 004010CA  mov     esp, ebp
  30. 004010CC  pop     ebp
  31. 004010CD  retn
复制代码
3)虚表指针的使用(父类指针访问子类对象)

​        在转换Bed指针时,会调整首地址并跳过第一个父类占用的空间。当使用父类Bed的指针访问Bed中实现的虚函数时,就不会错误地寻址到继承自Sofa类的成员变量了
多重继承子类对象转换为父类指针:
  1. int main(int argc, char* argv[]) {
  2.   SofaBed sofabed;
  3.   Sofa *sofa = &sofabed;
  4.   Bed *bed = &sofabed;
  5.   return 0;
  6. }
复制代码
汇编标识:
  1. 00401000  push    ebp
  2. 00401001  mov     ebp, esp
  3. 00401003  sub     esp, 28h
  4. 00401006  lea     ecx, [ebp-28h]         ;传递this指针
  5. 00401009  call    sub_4010B0             ;调用构造函数
  6. 0040100E  lea     eax, [ebp-28h]
  7. 00401011  mov     [ebp-0Ch], eax         ;直接以首地址转换为父类指针,sofa=&sofabed
  8. 00401014  lea     ecx, [ebp-28h]
  9. 00401017  test    ecx, ecx
  10. 00401019  jz      short loc_401026       ;检查对象首地址
  11. 0040101B  lea     edx, [ebp-28h]         ;edx=this
  12. 0040101E  add     edx, 8
  13. 00401021  mov     [ebp-4], edx           ;即this+8,调整为Bed的指针,bed=&sofabed
  14. 00401024  jmp     short loc_40102D
  15. 00401026  mov     dword ptr [ebp-4], 0
  16. 0040102D  mov     eax, [ebp-4]
  17. 00401030  mov     [ebp-10h], eax
  18. 00401033  mov     dword ptr [ebp-8], 0
  19. 0040103A  lea     ecx, [ebp-28h]         ;传递this指针
  20. 0040103D  call    sub_401150             ;调用析构函数
  21. 00401042  mov     eax, [ebp-8]
  22. 00401045  mov     esp, ebp
  23. 00401047  pop     ebp
  24. 00401048  retn
复制代码
4)多重继承的类对象析构函数

​        ①将子类的虚表指针写入对象首地址处(两个地址都写)
​        ②调用子类析构函数
​        ③依次调用Bed类、Sofa类的析构函数
多重继承的类对象析构函数:
  1. 00401130  push    ebp                    ;析构函数
  2. 00401131  mov     ebp, esp
  3. 00401133  push    ecx
  4. 00401134  mov     [ebp-4], ecx
  5. 00401137  mov     eax, [ebp-4]           ;将第一个虚表设置为SofaBed的虚表
  6. 0040113A  mov     dword ptr [eax], offset ??_7SofaBed@@6B@
  7. 00401140  mov     ecx, [ebp-4]           ;将第二个虚表设置为SofaBed的虚表
  8. 00401143  mov  dword ptr [ecx+8], offset ??_7SofaBed@@6B@_0
  9. 0040114A  push    offset aVirtualSofabed ;参数1:"virtual~SofaBed()\n"
  10. 0040114F  call    sub_401330             ;调用printf函数
  11. 00401154  add     esp, 4
  12. 00401157  mov     ecx, [ebp-4]
  13. 0040115A  add     ecx, 8                 ;调整this指针到Bed父类,this+8
  14. 0040115D  call    sub_4010D0             ;调用父类Bed的析构函数
  15. 00401162  mov     ecx, [ebp-4]           ;this指针,无需调整
  16. 00401165  call    sub_401100             ;调用父类Sofa的析构函数
  17. 0040116A  mov     esp, ebp
  18. 0040116C  pop     ebp
  19. 0040116D  retn
复制代码
四、单继承类和多继承类的区别总结

1. 单继承类

1)在类对象占用的内存空间中,只保存一份虚表指针
2)虚表中各项保存了类中各虚函数的首地址
3)构造时先构造父类,再构造自身,并且只调用一次父类构造函数
4)析构时先析构自身,再析构父类,并且只调用一次父类析构函数
2. 多重继承类

1)在类对象占用内存空间中,根据继承父类(有虚函数)个数保存对应的虚表指针。根据保存的虚表指针的个数,产生相应个数的虚表。
2)转换父类指针时,需要调整到对象的首地址。
3)构造时需要调用多个父类构造函数。构造时先构造继承列表中的第一个父类,然后依次调用到最后一个继承的父类构造函数。
4)析构时先析构自身,然后以构造函数相反的顺序调用所有父类的析构函数。
5)当对象作为成员时,整个类对象的内存结构和多重继承相似。当类中无虚函数时,整个类对象内存结构和多重继承完全一样。
​        当父类或成员对象存在虚函数时,通过观察虚表指针的位置和构造、析构函数中填写虚表指针的数目、顺序及目标地址,还原继承或成员关系

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

西河刘卡车医

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表