虚继承

1. 菱形继承

1)概念

​ B,C继承自A,D继承自B,C

2)问题

​ 一个派生类中保留间接基类的多份同名成员,可能出现命名冲突和冗余数据问题

2. 虚继承

c++代码示例:

#include <stdio.h>
//定义家具类,虚基类,等同于类A 
class Furniture  {
public:
  Furniture() {
    printf("Furniture::Furniture()\n"); 
    price = 0;
  }
  virtual ~Furniture(){                        //家具类的虚析构函数
    printf("Furniture::~Furniture()\n"); 
  }
  virtual int getPrice()  {                    //获取家具价格
    printf("Furniture::getPrice()\n"); 
    return price;
  }; 
protected:
  int price;                                   //家具类的成员变量
};

//定义沙发类,继承自类Furniture,等同于类B 
class Sofa : virtual public Furniture { 
public:
  Sofa() {
    printf("Sofa::Sofa()\n"); 
    price = 1;
    color = 2; 
  }
  virtual ~Sofa()  {                              //沙发类虚析构函数
    printf("Sofa::~Sofa()\n"); 
  }
  virtual int getColor()  {                       //获取沙发颜色
    printf("Sofa::getColor()\n"); 
    return color;
  }
  virtual int sitDown() {                         //沙发可以坐下休息
    return printf("Sofa::sitDown()\n"); 
  }
protected:
  int color;                                      // 沙发类成员变量
};

//定义床类,继承自类Furniture,等同于类C 
class Bed : virtual public Furniture  { 
public:
  Bed()  {
    printf("Bed::Bed()\n"); 
    price = 3;
    length = 4; 
    width = 5; 
  }
  virtual ~Bed(){                                 //床类的虚析构函数
    printf("Bed::~Bed()\n"); 
  }
  virtual int getArea(){                          //获取床面积
    printf("Bed::getArea()\n"); 
    return length * width;
  }
  virtual int sleep(){                            //床可以用来睡觉
    return  printf("Bed::sleep()\n"); 
  }
protected:
  int length; //床类成员变量 
  int width;
};

//子类沙发床的定义,派生自类Sofa和类Bed,等同于类D 
class SofaBed : public Sofa, public Bed { 
public:
  SofaBed()  {
    printf("SofaBed::SofaBed()\n"); 
    height = 6;
  }
  virtual ~SofaBed(){                             //沙发床类的虚析构函数
    printf("SofaBed::~SofaBed()\n"); 
  }
  virtual int sitDown(){                          //沙发可以坐下休息
    return printf("SofaBed::sitDown()\n"); 
  }
  virtual int sleep(){                            //床可以用来睡觉
    return printf("SofaBed::sleep()\n"); 
  }
  virtual int getHeight() {
    printf("SofaBed::getHeight()\n"); 
    return height;
  } 
protected:
  int height;                                     //沙发类的成员变量
};

int main(int argc, char* argv[]) { 
  SofaBed sofabed;
  Furniture *p1 = &sofabed;   //转换成虚基类指针 
  Sofa *p2 = &sofabed;        //转换成父类指针 
  Bed *p3 = &sofabed;         //转换成父类指针 
  printf("%p %p %p\n", p1, p2, p3);
  return 0; 
}

vs_x86汇编标识:

00401000  push    ebp
00401001  mov     ebp, esp
00401003  sub     esp, 40h
00401006  push    1                     ;是否构造虚基类的标志:1构造,0不构造 ①
00401008  lea     ecx, [ebp-40h]        ;传入对象的首地址作为this指针
0040100B  call    sub_4011D0            ;调用构造函数 ②
00401010  lea     eax, [ebp-40h]        ;获取对象的首地址
00401013  test    eax, eax
00401015  jnz     short loc_401020      ;检查代码
00401017  mov     dword ptr [ebp-4], 0
0040101E  jmp     short loc_40102D
00401020  mov     ecx, [ebp-3Ch]        ;取出对象中的Sofa类虚基类偏移表第二项数据
                                        ;即虚基类对象首地址相对于虚基类偏移表的偏移值
00401023  mov     edx, [ecx+4]          ;取出偏移值并存入edx 
00401026  lea     eax, [ebp+edx-3Ch]    ;根据虚基类偏移表,得到虚基类数据的所在地址 ③
0040102A  mov     [ebp-4], eax          ;利用中间变量保存虚基类的首地址
0040102D  mov     ecx, [ebp-4]
00401030  mov     [ebp-14h], ecx        ;p1=&sofabed 
00401033  lea     edx, [ebp-40h]        ;直接转换SofaBed对象的首地址为父类Sofa的指针 ④
00401036  mov     [ebp-10h], edx        ;p2=&sofabed 
00401039  lea     eax, [ebp-40h]        ;获取对象SofaBed的首地址 ⑤
0040103C  test    eax, eax
0040103E  jz      short loc_40104B      ;地址检查
00401040  lea     ecx, [ebp-40h]
00401043  add     ecx, 0Ch              ;获取Bed类对象的首地址
00401046  mov     [ebp-8], ecx          ;利用中间变量保存Bed类对象的首地址
00401049  jmp     short loc_401052
0040104B  mov     dword ptr [ebp-8], 0
00401052  mov     edx, [ebp-8]
00401055  mov     [ebp-0Ch], edx        ;p3=&sofabed
00401058  mov     eax, [ebp-0Ch]
0040105B  push    eax                   ;参数4:p3
0040105C  mov     ecx, [ebp-10h]
0040105F  push    ecx                   ;参数3:p2
00401060  mov     edx, [ebp-14h]
00401063  push    edx                   ;参数2:p1
00401064  push    offset aPPP           ;参数1:"%p %p %p\n"
00401069  call    sub_401650            ;调用printf函数
0040106E  add     esp, 10h
00401071  mov     dword ptr [ebp-18h], 0
00401078  lea     ecx, [ebp-40h]        ;传递this指针
0040107B  call    sub_4013E0            ;调用析构代理函数 ⑥
00401080  mov     eax, [ebp-18h]
00401083  mov     esp, ebp
00401085  pop     ebp
00401086  retn 

;虚继承结构的子类构造
004011D0  push    ebp
004011D1  mov     ebp, esp
004011D3  sub     esp, 8
004011D6  mov     [ebp-4], ecx
004011D9  mov     dword ptr [ebp-8], 0
004011E0  cmp     dword ptr [ebp+8], 0  ;比较参数是否为0,为0则执行跳转,防止重复构造虚基类
004011E4  jz      short loc_401209
004011E6  mov     eax, [ebp-4]
004011E9  mov     dword ptr [eax+4], offset unk_4122B0 ;设置父类Sofa中的虚基类偏移表 ②
004011F0  mov     ecx, [ebp-4]
004011F3  mov     dword ptr [ecx+10h], offset unk_4122B8;设置父类Bed中的虚基类偏移表 ③
004011FA  mov     ecx, [ebp-4]
004011FD  add     ecx, 20h             ;调整this指针为虚基类this指针
00401200  call    sub_401120           ;调用虚基类构造函数 ④
00401205  or      dword ptr [ebp-8], 1
00401209  push    0                    ;传入0作为构造标记
0040120B  mov     ecx, [ebp-4]         ;获取Sofa对象首地址作为this指针
0040120E  call    sub_401150           ;调用父类Sofa构造函数 ⑤
00401213  push    0                    ;传入0作为构造标记
00401215  mov     ecx, [ebp-4]         ;调整this指针
00401218  add     ecx, 0Ch             ;获取Bed对象首地址作为this指针
0040121B  call    sub_401090           ;调用父类Bed构造函数 ⑥
00401220  mov     edx, [ebp-4]
00401223  mov     dword ptr [edx], offset ??_7SofaBed@@6B@  ;覆盖Sofa类虚表指针(新) ⑦
00401229  mov     eax, [ebp-4]
0040122C  mov     dword ptr [eax+0Ch], offset ??_7SofaBed@@6B@_0  ;覆盖Bed类虚表指针(新) ⑧
00401233  mov     ecx, [ebp-4]         ;通过this指针和虚基类偏移表定位到虚基类的虚表指针
00401236  mov     edx, [ecx+4]         ;虚基类偏移表给edx 
00401239  mov     eax, [edx+4]         ;虚基类虚表指针相对于虚基类偏移表的偏移给eax
0040123C  mov     ecx, [ebp-4]         ;获取this指针 
0040123F  mov     dword ptr [ecx+eax+4], offset ??_7SofaBed@@6B@_1;覆盖Furniture类虚表指针(新) ⑨
00401247  push    offset aSofabedSofabed;"SofaBed::SofaBed()\n"
0040124C  call    sub_401650           ;调用printf函数
00401251  add     esp, 4
00401254  mov     edx, [ebp-4]
00401257  mov     dword ptr [edx+1Ch], 6;height = 6
0040125E  mov     eax, [ebp-4]
00401261  mov     esp, ebp
00401263  pop     ebp
00401264  retn    4

;子类析构代理函数
004013E0  push    ebp                     ;析构代理函数
004013E1  mov     ebp, esp
004013E3  push    ecx
004013E4  mov     [ebp-4], ecx
004013E7  mov     ecx, [ebp-4]
004013EA  add     ecx, 20h
004013ED  call    sub_401320              ;调用SofaBed的析构函数
004013F2  mov     ecx, [ebp-4]
004013F5  add     ecx, 20h                ;调整this指针为虚基类
004013F8  call    sub_4012B0              ;调用虚基类的析构函数
004013FD  mov     esp, ebp
004013FF  pop     ebp
00401400  retn

;子类的析构函数
00401320  push    ebp
00401321  mov     ebp, esp
00401323  push    ecx
00401324  mov     [ebp-4], ecx
00401327  mov     eax, [ebp-4]             ;调整this指针为Sofa,还原虚表指针为SofaBed
0040132A  mov     dword ptr [eax-20h], offset ??_7SofaBed@@6B@
00401331  mov     ecx, [ebp-4]             ;调整this指针为Bed,还原虚表指针为SofaBed
00401334  mov     dword ptr [ecx-14h], offset ??_7SofaBed@@6B@_0
0040133B  mov     edx, [ebp-4]
0040133E  mov     eax, [edx-1Ch]
00401341  mov     ecx, [eax+4]            ;从虚基类偏移表中获取虚基类偏移
00401344  mov     edx, [ebp-4]
;调整this指针为虚基类,还原虚表指针为SofaBed,到此为止,3个虚表指针还原完毕,执行析构函数内的代码
00401347  mov     dword ptr [edx+ecx-1Ch], offset ??_7SofaBed@@6B@_1

0040134F  push    offset aSofabedSofabed_0;"SofaBed::~SofaBed()\n"
00401354  call    sub_401650              ;调用printf函数
00401359  add     esp, 4
0040135C  mov     ecx, [ebp-4]
0040135F  sub     ecx, 4                  ;调整this指针为Bed
00401362  call    sub_401270              ;调用父类Bed析构函数
00401367  mov     ecx, [ebp-4]
0040136A  sub     ecx, 14h                ;调整this指针为Sofa
0040136D  call    sub_4012E0              ;调用父类Sofa析构函数
00401372  mov     esp, ebp
00401374  pop     ebp
00401375  retn



;Sofe类虚基类偏移表
.rdata:004122B0 dword_4122B0    dd 0FFFFFFFCh ;-4,虚基类偏移表所属类对应的对象首地址相对于虚基类偏移表的偏移值
.rdata:004122B4                   dd 1Ch      ;虚基类对象首地址相对于虚基类偏移表的偏移值

;Bed类虚基类偏移表
.rdata:004122B8 dword_4122B8    dd 0FFFFFFFCh 
.rdata:004122BC                   dd 10h

1)方式

​ class Bed : virtual public Furniture{};

2)作用

​ 可以避免共同派生出的子类产生多义性错误

3)子类对象内存结构

​ ①B父类的虚表指针,B父类虚基类偏移表,B父类独有的成员

​ ②C父类的虚表指针,C父类虚基类偏移表,C父类独有的成员

​ ③子类自己的成员

​ ④A祖父类的虚表指针,A祖父类自己独有的成员

注意:当子类存在多个虚基类时,会在虚基类偏移表中依次记录它们的偏移量

4)流程

​ ①传入构造标记1,表示需要构造虚基类

​ ②调用子类构造代理函数

​ a.设置父类Sofa中的虚基类偏移表

​ b.设置父类Bed中的虚基类偏移表

​ c.调用虚基类构造函数

​ d.传入构造标记0,表示不需要构造虚基类,调用父类Sofa构造函数

​ e.传入构造标记0,表示不需要构造虚基类,调用父类Bed构造函数

​ f.覆盖Sofa类虚表指针(新)

​ g.覆盖Bed类虚表指针(新)

​ h.覆盖Furniture类虚表指针(新)

​ i.调用子类构造函数

​ ③根据虚基类偏移表,得到虚基类数据的所在地址,转换子类对象的对应地址处为虚基类对象

​ ④直接转换SofaBed对象的首地址为父类Sofa的指针

​ ⑤通过对象首地址+偏移找到Bed类的地址,转换子类对象的对应地址处为Bed类对象

​ ⑥调用析构代理函数

​ a..调整this指针为Sofa,还原虚表指针为SofaBed

​ b.调整this指针为Bed,还原虚表指针为SofaBed

​ c.从虚基类偏移表中获取虚基类偏移,调整this指针为虚基类,还原虚表指针为SofaBed

​ d.调用子类析构函数

​ e.调用父类Bed析构函数

​ f.调用父类Sofa析构函数

​ g.调用虚基类析构函数

5)子类构造顺序

​ ①Furniture

​ ②Sofa(根据标记跳过Furniture构造)

​ ③Bed(根据标记跳过Furniture构造)

​ ④SofaBed自身

注意

​ ①vs编译器使用构造标记决定是否跳过所有父类的构造,只构造自身

​ ②GCC编译器生成一个父类构造代理函数,该函数不调用虚基类构造函数;对于正常实例化的父类对象,GCC编译器不会调用该构造代理函数

posted @ 2023-02-28 10:42  修竹Kirakira  阅读(46)  评论(0编辑  收藏  举报