12.4 菱形继承
菱形继承是最复杂的对象结构,菱形结构会将单一继承与多重继承进行组合,如图12-12所示。
图 12-12 菱形继承结构图
在图12-12中,类D属于多重继承中的子类,其父类为类B和类C,类B和类C拥有同一个父类A,如代码清单12-13所示。
代码清单12-13 菱形结构的类继承和派生—C++源码
//定义家具类,等同于类A
class CFurniture{
public:
CFurniture(){
m_nPrice=0;
}
virtual~CFurniture(){//家具类的虚析构函数
printf("virtual~CFurniture()\r\n");
}
virtual int GetPrice(){//获取家具价格
return m_nPrice;
};
protected:
int m_nPrice;//家具类的成员变量
};
//定义沙发类,继承自类CFurniture,等同于类B
class CSofa:virtual public CFurniture{
public:
CSofa(){
m_nPrice=1;
m_nColor=2;
}
virtual~CSofa(){//沙发类虚析构函数
printf("virtual~CSofa()\r\n");
}
virtual int GetColor(){//获取沙发颜色
return m_nColor;
}
virtual int SitDown(){//沙发可以坐下休息
return printf("Sit down and rest your legs\r\n");
}
protected:
int m_nColor;//沙发类成员变量
};
//定义床类,继承自类CFurniture,等同于类C
class CBed:virtual public CFurniture{
public:
CBed(){
m_nPrice=3;
m_nLength=4;
m_nWidth=5;
}
virtual~CBed(){//床类的虚析构函数
printf("virtual~CBed()\r\n");
}
virtual int GetArea(){//获取床面积
return m_nLength*m_nWidth;
}
virtual int Sleep(){//床可以用来睡觉
return printf("go to sleep\r\n");
}
protected:
int m_nLength;//床类成员变量
int m_nWidth;
};
//子类沙发床的定义,派生自类CSofa和类CBed,等同于类D
class CSofaBed:public CSofa, public CBed{
public:
CSofaBed(){
m_nHeight=6;
}
virtual~CSofaBed(){//沙发床类的虚析构函数
printf("virtual~CSofaBed()\r\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 m_nHeight;
}
protected:
int m_nHeight;//沙发类的成员变量
};
void main(int argc, char*argv[]){
CSofaBed SofaBed;
}
代码清单12-13中一共定义了4个类,分别为CFurniture、CSofa、CBed和CSofaBed。CFurniture为祖父类,从CFurniture类中派生了两个子类:CSofa与CBed,它们在继承时使用了virtual的方式,即虚继承。
使用虚继承可以避免共同派生出的子类产生多义性的错误。那么,为什么要将virtual加在两个父类上而不是它们共同派生的子类呢?这个问题与现实世界中动物的繁衍很相似,例如,熊猫在繁衍时要避免具有血缘关系的雄性与雌性“近亲繁殖”,因为“近亲繁殖”的结果会使繁殖出的后代出现基因重叠的问题,从而造成残缺现象。类CBed与类CSofa就如同是一对兄妹,它们的父亲为CSofaBed,当类CBed与类CSofa“近亲结合”后“生下”存在基因问题的子类CSofaBed时,也会存在基因重叠问题,因此通过虚继承来防止这个问题的发生。接下来介绍菱形结构中子类CSofaBed的对象在内存中是如何存放的,如图12-13所示。
图 12-13 CSofaBed的内存结构
图12-13中显示了CSofaBed在内存中的信息,初步观察内存中保存的数据可得知,有些数据类似地址值。这些地址值都有哪些含义吗?图12-14对各个地址数据进行了注解。
图 12-14 CSofaBed内存结构的注解
通过图12-14虽然可以知道各个数据所具有的含义,但是还存在一些模糊不清的数据无法理解,如CSofaBed_vt(new)和vt(new),它们又都代表着什么呢?带着这个疑问,我们将代码清单12-13转换成汇编代码,如代码清单12-14所示。
代码清单12-14 菱形结构的虚表指针转换过程
//C++源码对比,加入了父类指针的转换代码
vold main(int argc, char*argv[]){
CSofaBed SofaBed;
CFurniture*pFurniture=&SofaBed;//转换成祖父类指针
CSofa*pSofa=&SofaBed;//转换成父类指针
CBed*pBed=&SofaBed;//转换成父类指针
}
//C++源码与对应汇编代码讲解
vold main(int argc, char*argv[]){
CSofaBed SofaBed;
0040F718 push 1;是否构造祖父的标志,TURE表示构造,FALSE表示构造
0040F71A lea ecx,[ebp-28h];传入对象的首地址作为this指针
0040F71D call@ILT+10(CSofaBed:CSofaBed)(0040100f);调用构造函数
CFurniture*pFurniture=&SofaBed;
0040F722 lea eax,[ebp-28h];获取对象的首地址
0040F725 test eax, eax;检查代码
0040F727 jne main+32h(0040f732);跳转到0x0040f732
0040F729 mov dword ptr[ebp-38h],0
0040F730 jmp main+3Fh(0040f73f)
;取出对象的第二项数据vt_offset,此地址指向的数据如图12-14所示
0040F732 mov ecx, dword ptr[ebp-24h]
0040F735 mov edx, dword ptr[ecx+4];取出偏移值后存入edx中
0040F738 lea eax,[ebp+edx-24h];得到祖父类数据的所在地址
0040F73C mov dword ptr[ebp-38h],eax;利用中间变量保存祖父类的首地址
0040F73F mov ecx, dword ptr[ebp-38h]
0040F742 mov dword ptr[ebp-2Ch],ecx;赋值pFurniture
CSofa*pSofa=&SofaBed;
0040F745 lea edx,[ebp-28h];直接转换SofaBed对象的首地址为父类CSofa的指针
0040F748 mov dword ptr[ebp-30h],edx
CBed*pBed=&SofaBed;
0040F74B lea eax,[ebp-28h];获取对象SofaBed的首地址
0040F74E test eax, eax;地址检查
0040F750 je main+5Ah(0040f75a)
0040F752 lea ecx,[ebp-1Ch];获取第二个CSofaBed_vt(new)指针
0040F755 mov dword ptr[ebp-3Ch],ecx
0040F758 jmp main+61h(0040f761)
0040F75A mov dword ptr[ebp-3Ch],0
0040F761 mov edx, dword ptr[ebp-3Ch]
0040F764 mov dword ptr[ebp-34h],edx;保存转换后的SofaBed地址到pSofa中
}
从代码清单12-14中的指针转换过程可以看出,vt_offset指向的内存地址中保存的数据为偏移数据,如图12-15所示,图中每个vt_offset对应的数据有两项:第一项为-4,即vt_offset所属类对应的虚表指针相对于vt_offset的偏移值;第二项保存的是父类虚表指针相对于vt_offset的偏移值。
图 12-15 vt_offset指向的数据
根据对代码清单12-13的分析可知,3个虚表指针分别为0x00425034、0x00425028、0x0042501C,它们所指向的数据如图12-16所示。
图 12-16 各个虚表信息
如图12-16所示,这三个虚表指针所指向的虚表包含了子类CSofaBed含有的虚函数。有了这些记录就可以随心所欲地将虚表指针转换成任意的父类指针。在利用父类指针访问虚函数时,只能调用子类与父类共有的虚函数,子类继承自其他父类的虚函数是无法调用的,虚表中也没有相关的记录。当子类的父类也存在多个父类时,会在图12-15所显示的表格中依次记录它们的偏移。
学习了菱形结构中子类的内存布局后,接下来分析其子类的构造函数,看看这些数据是如何产生的,如代码清单12-15所示。
代码清单12-15 菱形结构的子类构造
CSofaBed SofaBed;
0040F730 push 1;压入参数1
0040F732 lea ecx,[ebp-34h];传递this指针
0040F735 call@ILT+10(CSofaBed:CSofaBed)(0040100f);调用构造函数
;构造函数实现
CSofaBed(){
;部分代码分析略
004011FE pop ecx;还原this指针
004011FF mov dword ptr[ebp-10h],ecx
00401202 mov dword ptr[ebp-14h],0;传入构造标记
;比较参数是否为0,为0则执行JE跳转,防止重复构造
00401209 cmp dword ptr[ebp+8],0
0040120D je CSofaBed:CSofaBed+6Eh(0040123e)
0040120F mov eax, dword ptr[ebp-10h]
;设置父类CSofa中的vt_offset域
00401212 mov dword ptr[eax+4],offset CSofaBed:'vbtable'(00425050)
00401219 mov ecx, dword ptr[ebp-10h]
;设置父类CBed中的vt_offset域
0040121C mov dword ptr[ecx+10h],offset CSofaBed:'vbtable'(00425044)
00401223 mov ecx, dword ptr[ebp-10h]
00401226 add ecx,20h;调整this指针
;调用祖父类构造函数,祖父类为最上级,它的构造函数和无继承关系的构造函数相同,这里不予分析
00401229 call@ILT+45(CFurniture:CFurniture)(00401032)
0040122E mov edx, dword ptr[ebp-14h];获取构造标记
00401231 or edx,1;将构造标记置为1
00401234 mov dword ptr[ebp-14h],edx;修改构造标记
00401237 mov dword ptr[ebp-4],0
0040123E push 0;压入0作为构造标记
00401240 mov ecx, dword ptr[ebp-10h];获取对象首地址作为this指针
00401243 call@ILT+110(CSofa:CSofa)(00401073);调用父类构造函数
00401248 mov dword ptr[ebp-4],1
0040124F push 0;压入0作为构造标记
00401251 mov ecx, dword ptr[ebp-10h]
00401254 add ecx,0Ch;调整this指针
00401257 call@ILT+130(CBed:CBed)(00401087);调用父类构造函数
0040125C mov eax, dword ptr[ebp-10h]
;CSofaBed对应CSofa的虚表指针
0040125F mov dword ptr[eax],offset CSofaBed:'vftable'(00425034)
00401265 mov ecx, dword ptr[ebp-10h]
;CSofaBed对应CBed的虚表指针
00401268 mov dword ptr[ecx+0Ch],offset CSofaBed:'vftable'(00425028)
0040126F mov edx, dword ptr[ebp-10h];通过this指针和vt_offset定位到祖
;父类的虚表指针
00401272 mov eax, dword ptr[edx+4];vt_offset存入eax中
00401275 mov ecx, dword ptr[eax+4];父类虚表指针相对于vt_offset的偏移存入eax中
00401278 mov edx, dword ptr[ebp-10h]
;CSofaBed对应CFurniture的虚表指针
0040127B mov dword ptr[edx+ecx+4],offset CSofaBed:'vftable'
(0042501c)
m_nHeight=6;
00401283 mov eax, dword ptr[ebp-10h]
00401286 mov dword ptr[eax+1Ch],6
}
004012B1 ret 4
代码清单12-15展示了子类CSofaBed的构造过程,它的特别之处是在调用时要传入一个参数。这个参数是一个标志信息。构造过程中要先构造父类,然后构造自己。CSofaBed的两个父类有一个共同的父类,如果没有构造标记,它们共同的父类将会被构造两次,因此需要使用构造标记来防止重复构造的问题,构造顺序如下:
CFurniture
CSofa(根据标记跳过CFurniture构造)
CBed(根据标记跳过CFurniture构造)
CSofaBed自身
CSofaBed也使用了构造标记,当CSofaBed也是父类时,这个标记将产生作用,跳过所有父类的构造,只构造自身。当标记为1时,则构造父类;当标记为0时,则跳过构造函数。构造时可以使用标记来防止重复构造,同样也不能出现重复析构的错误,那么这又如何实现呢?我们来看一下代码清单12-16。
代码清单12-16菱形结构的子类析构
//CSofaBed调用析构代理函数,因为是编译器自动添加的,所以无源码对照
CSofaBed:'vbase destructor':
;部分代码分析略
00401AE9 pop ecx
00401AEA mov dword ptr[ebp-4],ecx
00401AED mov ecx, dword ptr[ebp-4]
00401AF0 add ecx,20h
;调用CSofaBed的析构函数
00401AF3 call@ILT+90(CSofaBed:~CSofaBed)(0040105f)
00401AF8 mov ecx, dword ptr[ebp-4]
00401AFB add ecx,20h
;调用祖父类的析构函数
00401AFE call@ILT+60(CFurniture:~CFurniture)(00401041)
;CSofaBed:~CSofaBed实现
virtual~CSofaBed(){
;部分代码分析略
00401B5E pop ecx;还原this指针
00401B5F mov dword ptr[ebp-10h],ecx;调整this指针
00401B62 mov eax, dword ptr[ebp-10h]
;设置自身虚表
00401B65 mov dword ptr[eax-20h],offset CSofaBed:'vftable'(00425034)
00401B6C mov ecx, dword ptr[ebp-10h]
;设置自身虚表
00401B6F mov dword ptr[ecx-14h],offset CSofaBed:'vftable'(00425028)
00401B76 mov edx, dword ptr[ebp-10h]
00401B79 mov eax, dword ptr[edx-1Ch]
00401B7C mov ecx, dword ptr[eax+4]
00401B7F mov edx, dword ptr[ebp-10h]
;设置自身虚表。到此为止,3个虚表指针设置完毕,执行析构函数内的代码
00401B82 mov dword ptr[edx+ecx-1Ch],offset CSofaBed:'vftable'(0042501c)
00401B8A mov dword ptr[ebp-4],0
printf("virtual~CSofaBed()\r\n");
}
00401B9E mov eax, dword ptr[ebp-10h]
00401BA1 sub eax,20h;获取this指针
00401BA4 test eax, eax;检查this指针
00401BA6 je CSofaBed:~CSofaBed+83h(00401bb3)
00401BA8 mov ecx, dword ptr[ebp-10h]
00401BAB sub ecx,14h
00401BAE mov dword ptr[ebp-14h],ecx
00401BB1 jmp CSofaBed:~CSofaBed+8Ah(00401bba)
00401BB3 mov dword ptr[ebp-14h],0
00401BBA mov ecx, dword ptr[ebp-14h]
00401BBD add ecx,10h;调整this指针
00401BC0 call@ILT+75(CBed:~CBed)(00401050);调用父类析构函数
00401BC5 mov dword ptr[ebp-4],0FFFFFFFFh
00401BCC mov ecx, dword ptr[ebp-10h]
00401BCF sub ecx,14h;调整this指针
00401BD2 call@ILT+125(CSofa:~CSofa)(00401082);调用父类析构函数
;部分代码分析略
00401BF1 ret
根据对代码清单12-16的分析可知,菱形结构中子类的析构函数执行流程并没有像构造函数那样使用标记来防止重复析构,而是将祖父类放在最后调用。先依次执行两个父类CBed和CSofa的析构函数,然后执行祖父类的析构函数。Release版下的原理也是如此,这里就不再重复分析了。