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;
}

内存结构

从汇编观察

main函数

push    ebp
mov     ebp, esp
sub     esp, 1Ch
lea     ecx, [ebp+var_1C] ; var_1C size=0x18
call    constructor_401090_SofaBed
mov     [ebp+var_4], 0
lea     ecx, [ebp+var_1C]
call    destructor_401130_SofaBed
mov     eax, [ebp+var_4]
mov     esp, ebp
pop     ebp
retn

一共分配了七个栈空间,其中var_4是用来存储main函数返回值的,可见sofabed的内存大小为0x18

constructor_401090_SofaBed
push    ebp
mov     ebp, esp
push    ecx
mov     [ebp+var_4], ecx
mov     ecx, [ebp+var_4]
call    constructor_401060_Sofa
mov     ecx, [ebp+var_4]
add     ecx, 8
call    constructor_401030_Bed
mov     eax, [ebp+var_4]
mov     dword ptr [eax], offset SofaBed_vftable1
mov     ecx, [ebp+var_4]
mov     dword ptr [ecx+8], offset SofaBed_vftable2
mov     edx, [ebp+var_4]
mov     dword ptr [edx+14h], 6
mov     eax, [ebp+var_4]
mov     esp, ebp
pop     ebp
retn

可以看到有两个thiscall函数,下面有两个虚指针赋值,内存是第一块和第三块,再下面是给sofabed对象的第六块内存赋值为6

constructor_401060_Sofa
push    ebp
mov     ebp, esp
push    ecx
mov     [ebp+var_4], ecx
mov     eax, [ebp+var_4]
mov     dword ptr [eax], offset ??_7Sofa@@6B@ ; const Sofa::`vftable'
mov     ecx, [ebp+var_4]
mov     dword ptr [ecx+4], 2
mov     eax, [ebp+var_4]
mov     esp, ebp
pop     ebp
retn

在第一块内存赋值虚指针,在第二块内存赋值为2

constructor_401030_Bed
push    ebp
mov     ebp, esp
push    ecx
mov     [ebp+var_4], ecx
mov     eax, [ebp+var_4]
mov     dword ptr [eax], offset ??_7Bed@@6B@ ; const Bed::`vftable'
mov     ecx, [ebp+var_4]
mov     dword ptr [ecx+4], 4
mov     edx, [ebp+var_4]
mov     dword ptr [edx+8], 5
mov     eax, [ebp+var_4]
mov     esp, ebp
pop     ebp
retn

由于constructor_401030_Bed在被调用的时候ecx加了8,所以这里的this指针其实是第三块内存的地址,在第四块和第五块内存分别赋值 4、5

至此所有的内存清楚了

  • 第一块 原来是Sofa的虚表指针,后来被赋值为SofaBed的第一个虚表

  • 第二块 color 初始化为2

  • 第三块 原来是Bed的虚表指针,后来被赋值为SofaBed的第二个虚表

  • 第四块 length 初始化为4

  • 第五块 width 初始化为5

  • 第六块 height 初始化为6

看来多重继承编译器会给对象的基类分别分配虚指针和成员变量,并依次排列,最后会放上自己的成员变量

那么这些虚表之间的关系是怎样的呢

虚表关系

Sofa::`vftable'

dd offset Sofa_vector_destructor
dd offset Sofa_getColor 
dd offset Sofa_siteDown

Sofa类的虚表都是它自己的虚函数

Bed::`vftable'

dd offset Bed_vector_destructor                             
dd offset Bed_getArea
dd offset Bed_sleep

Bed类的虚表也都是它自己的虚函数

SofaBed_vftable1

dd offset SofaBed_vector_destructor
dd offset Sofa_getColor 
dd offset SofaBed_siteDown
dd offset SofaBed_getHeight 

SofaBed第一个虚表,前面三个都是覆盖Sofa的虚函数,最后一个是自己的虚函数

SofaBed_vftable2

dd offset SofaBed_vector_destructor_
dd offset Bed_getArea
dd offset SofaBed_sleep

SofaBed第二个虚表,都是覆盖Bed的虚函数

虚表关系图

SofaBed_vftable1 对应 Sofa::vftableSofaBed_vftable2对应Bed::vftable,如果SofaBed没有重写父类的虚函数,如getColor,就会在对应的位置放上父类的虚函数

构造与析构流程

构造流程

通过观察上面的汇编代码,构造流程为

  • 首先按照继承顺序调用父类的构造函数
  • 按照继承顺序覆盖父类虚指针
  • 执行自身构造函数的代码

析构流程

destructor_401130_SofaBed
push    ebp
mov     ebp, esp
push    ecx
mov     [ebp+var_4], ecx
mov     eax, [ebp+var_4]
mov     dword ptr [eax], offset SofaBed_vftable1
mov     ecx, [ebp+var_4]
mov     dword ptr [ecx+8], offset SofaBed_vftable2
push    offset aVirtualSofabed ; "virtual ~SofaBed()\n"
call    _printf
add     esp, 4
mov     ecx, [ebp+var_4]
add     ecx, 8
call    destructor_4010D0_Bed
mov     ecx, [ebp+var_4]
call    destructor_401100_Sofa
mov     esp, ebp
pop     ebp
retn

通过观察上面的汇编代码,析构流程为

  • 按照继承顺序还原自身虚指针
  • 执行自身析构代码
  • 按照继承相反顺序调用父类的析构函数

实现多态

多态的核心便是使用基类指针指向子类对象,多重继承的类的虚表指针并非只在对象首地址,那么父类指针是如何调用虚函数的呢

int main(int argc, char* argv[]) {
  SofaBed sofabed;
  Sofa *sofa = &sofabed;
  Bed *bed = &sofabed;
  return 0;
}

汇编

.text:0000000140001000                 mov     [rsp+argv], rdx
.text:0000000140001005                 mov     [rsp+argc], ecx
.text:0000000140001009                 sub     rsp, 78h
.text:000000014000100D                 lea     rcx, [rsp+78h+sofaBed]
.text:0000000140001012                 call    SofaBed_ctor
.text:0000000140001017                 lea     rax, [rsp+78h+sofaBed]
.text:000000014000101C                 mov     [rsp+78h+pSofa], rax
.text:0000000140001021                 lea     rax, [rsp+78h+sofaBed]
.text:0000000140001026                 test    rax, rax
.text:0000000140001029                 jz      short IF_END_14000103B
.text:000000014000102B                 lea     rax, [rsp+78h+sofaBed]
.text:0000000140001030                 add     rax, 10h
.text:0000000140001034                 mov     [rsp+78h+pBed1], rax
.text:0000000140001039                 jmp     short ELSE_END_140001044
.text:000000014000103B ; ---------------------------------------------------------------------------
.text:000000014000103B
.text:000000014000103B IF_END_14000103B:                       ; CODE XREF: main+29↑j
.text:000000014000103B                 mov     [rsp+78h+pBed1], 0
.text:0000000140001044
.text:0000000140001044 ELSE_END_140001044:                     ; CODE XREF: main+39↑j
.text:0000000140001044                 mov     rax, [rsp+78h+pBed1]
.text:0000000140001049                 mov     [rsp+78h+pBed], rax
.text:000000014000104E                 mov     [rsp+78h+temp_return], 0
.text:0000000140001056                 lea     rcx, [rsp+78h+sofaBed]
.text:000000014000105B                 call    SofaBed_dtor
.text:0000000140001060                 mov     eax, [rsp+78h+temp_return]
.text:0000000140001064                 add     rsp, 78h
.text:0000000140001068                 retn
.text:0000000140001068 main            endp
.text:0000000140001068
.text:0000000140001068 ; ---------------------------------------------------------------------------
.text:0000000140001069 algn_140001069:                         ; DATA XREF: .pdata:ExceptionDir↓o
.text:0000000140001069                 align 10h
.text:0000000140001070
.text:0000000140001070 ; =============== S U B R O U T I N E =======================================
.text:0000000140001070
.text:0000000140001070
.text:0000000140001070 Bed_ctor        proc near               ; CODE XREF: SofaBed_ctor+1F↓p
.text:0000000140001070
.text:0000000140001070 arg_0           = qword ptr  8
.text:0000000140001070
.text:0000000140001070                 mov     [rsp+arg_0], rcx
.text:0000000140001075                 mov     rax, [rsp+arg_0]
.text:000000014000107A                 lea     rcx, ??_7Bed@@6B@ ; const Bed::`vftable'
.text:0000000140001081                 mov     [rax], rcx
.text:0000000140001084                 mov     rax, [rsp+arg_0]
.text:0000000140001089                 mov     dword ptr [rax+8], 4
.text:0000000140001090                 mov     rax, [rsp+arg_0]
.text:0000000140001095                 mov     dword ptr [rax+0Ch], 5
.text:000000014000109C                 mov     rax, [rsp+arg_0]
.text:00000001400010A1                 retn

可以看到对象在转换Bed *指针时将地址加了0x10。看来多重继承是通过转换指针时调整对象首地址来实现多态的,因为在转换指针(由子类指针转换为父类指针)时必定知道子类与父类的关系,从而推算出该加多少地址

如果编译器转换的双方没有嫡系祖先关系的话,强制转换后就会有问题

int main(int argc, char* argv[]) {
  SofaBed sofabed;
  Sofa *sofa = &sofabed;
  Bed *bed = (Bed *)sofa;
  return 0;
}
error C2440: “初始化”: 无法从“Sofa *”转换为“Bed *”

vs2019会无法编译通过,其他编译器的话就算通过,强制转换也不会调整对象首地址,导致调用虚函数和访问成员变量错误

posted @ 2022-04-17 16:49  乘舟凉  阅读(49)  评论(0编辑  收藏  举报