第12章 从内存角度看继承和多重继承
在C++(www.cppentry.com)中,类之间的关系与现实社会非常相似。类的继承与派生是一个从抽象到具体的过程。
什么是抽象到具体的过程呢?我们以“表”为例,表是可以用来计时的,这是大家对表的第一印象。那么表是圆的还是方的?体积大还是小?卖多少钱?大家可能一时说不上来,因为此时的“表”是一个抽象概念,没有任何实体,仅仅只是一个概念。这在面向对象领域中被称为抽象类,抽象类同样没有实例。
以“表”为父类,派生出“手表”,手表类中包含的信息就更多了。首先,手表不仅继承了表的特点,而且更加具体:个头不会太大,是戴在手上的,由机芯、表盘、表带等组成……当然,手表类也属于抽象类,还是不够具体。
接着继承手表类,派生出“江诗丹顿牌Patrimony系列81180-000P-9539型手表”,这就属于具体类了,它当然拥有父类“手表”的所有特点,同时还派生出其他数据,以区别于其他品牌。当你想购买这款手表时,销售员拿出一款“江诗丹顿牌某系列某型号的手表”,被你识破了,这个识破过程就叫做RTTI(Run-Time Type Identification,运行时类型识别)。你成功购买了“江诗丹顿牌Patrimony系列81180-000P-9539型手表”后,经调试校正后戴在你手上的那块手表,就是“江诗丹顿牌Patrimony系列81180-000P-9539型手表”类的产品之一,在C++(www.cppentry.com)中,这块表被称为实例,也被称为对象。
抽象类没有实例。例如“东西”可以泛指世间万物,但是它过于抽象,我们无法找到“东西”的实体。具体类可以存在实例,如“江诗丹顿牌Patrimony系列81180-000P-9539型手表”存在具体的产品。
指向父类对象的指针除了可以操作父类对象外,还能操作子类对象,正如“江诗丹顿手表属于手表”,此逻辑正确。指向子类对象的指针不能操作父类对象,正如“手表属于江诗丹顿手表”,此逻辑错误。
如果强制将父类对象的指针转换为子类对象的指针,如下所示:
- CDervie *pDervie = (CDervie *)&base; // base为父类对象,CDervie继承自base
这条语句虽然可以编译通过,但是存在潜在的危险。例如,如果说:“张三长得像张三他爹”,张三和他爹都能接受;如果说:“张三他爹长得像张三”,虽然也可以,但是不招人喜欢,可能会给你的社会交际带来潜在的危险。
介绍了以上的重要概念之后,我们来探索一下编译器实现以上知识点的技术内幕。
12.1 识别类和类之间的关系(1)
在C++(www.cppentry.com)的继承关系中,子类具备父类所有的成员数据和成员函数。子类对象可以直接使用父类中声明为公有和保护的数据成员与成员函数。在父类中声明为私有(private)的成员,虽然子类对象无法直接访问,但是在子类对象的内存结构中,父类私有的成员数据依然存在。C++(www.cppentry.com)语法规定的访问控制仅限于编译层面,在编译的过程中由编译器进行语法检查,因此访问控制不会影响对象的内存结构。本节将以公有(public)继承为例进行讲解,首先来看一下代码清单12-1中的代码。
代码清单12-1 定义派生类和继承类—C++(www.cppentry.com)源码
- class CBase{ // 基类定义
- public:
- CBase(){
- printf("CBase\r\n");
- }
- ~CBase(){
- printf("~CBase\r\n");
- }
- void SetNumber(int nNumber){
- m_nBase = nNumber;
- }
- int GetNumber(){
- return m_nBase;
- }
- public:
- int m_nBase;
- };
-
- class CDervie : public CBase{ // 派生类定义
- public:
- void ShowNumber(int nNumber){
- SetNumber (nNumber);
- m_nDervie = nNumber + 1;
- printf("%d\r\n", GetNumber());
- printf("%d\r\n", m_nDervie);
- }
- public:
- int m_nDervie;
- };
- // main函数实现
- void main(int argc, char* argv[]){
- CDervie Dervie;
- Dervie.ShowNumber(argc);
- }
代码清单12-1中定义了两个具有继承关系的类。父类CBase中定义了数据成员m_nBase、构造函数、析构函数和两个成员函数。子类中只有一个成员函数ShowNumber和一个数据成员m_nDervie。根据C++(www.cppentry.com)的语法规则,子类CDervie将继承父类中的成员数据和成员函数。那么,当申请了子类对象Dervie时,它在内存中如何存储,又是如何使用父类成员函数的呢?调试代码清单12-1,查看其内存结构及程序执行流程,其汇编代码如代码清单12-2所示。
代码清单12-2 代码清单12-1的调试分析—Debug版
- // C++(www.cppentry.com)源码与汇编代码对比分析
- void main(int argc, char* argv[]){
- ; 函数入口部分略
- CDervie Dervie;
- 0040108D lea ecx,[ebp-14h] ; 获取对象首地址作为this指针
- ; 调用类CDervie的构造函数,编译器为CDervie提供了默认的构造函数
- 00401090 call @ILT+50(CDervie::CDervie) (00401014)
- 00401095 mov dword ptr [ebp-4],0
- Dervie.ShowNumber(argc);
- 0040109C mov eax,dword ptr [ebp+8]
- 0040109F push eax
- 004010A0 lea ecx,[ebp-14h] ; 调用CDervie成员函数,传入this指针
- 004010A3 call @ILT+55(CDervie::ShowNumber) (0040101e)
- }
- 004010A8 mov dword ptr [ebp-4],0FFFFFFFFh
- 004010AF lea ecx,[ebp-14h]
- ; 调用类CDervie的析构函数,编译器为CDervie提供了默认的析构函数
- 004010B2 call @ILT+45(CDervie::~CDervie) (0040100f)
- 004010D1 ret
-
- // 子类CDervie的默认构造函数分析
- CDervie::CDervie:
- ; 函数入口部分略
- 00401219 pop ecx ; 还原this指针
- 0040121A mov dword ptr [ebp-4],ecx
- ; 以子类对象首地址作为父类的this指针,调用父类构造函数
- 0040121D mov ecx,dword ptr [ebp-4]
- 00401220 call @ILT+35(CBase::CBase) (00401028)
- 00401225 mov eax,dword ptr [ebp-4]
- ; 函数出口部分略
- 00401238 ret
-
- // 子类CDervie的默认析构函数分析
- CDervie::~CDervie:
- ; 函数入口部分略
- 004012B9 pop ecx
- 004012BA mov dword ptr [ebp-4],ecx
- 004012BD mov ecx,dword ptr [ebp-4]
- ; 调用父类析构函数
- 004012C0 call @ILT+5(CBase::~CBase) (0040100a)
- ; 函数出口部分略
- 004012D5 ret