12.1 识别类和类之间的关系(3)
我们先看看正确的情况,如代码清单12-3所示。
代码清单12-3 子类调用父类函数—Debug版
- // ShowNumber源码对照代码清单12-1
- void ShowNumber(int nNumber){
- ; 函数入口代码略
- 0040ECC9 pop ecx
- 0040ECCA mov dword ptr [ebp-4],ecx ; [ebp-4]中保留了this指针
- 41: SetNumber (nNumber);
- 0040ECCD mov eax,dword ptr [ebp+8] ; 访问参数nNumber并保存到eax中
- 0040ECD0 push eax
- ;由于this指针同时也是对象中父类部分的首地址,因此在调用父类成员函数时,this指针的值和子类
- ;对象等同
- 0040ECD1 mov ecx,dword ptr [ebp-4]
- 0040ECD4 call @ILT+45(CBase::SetNumber) (00401032)
- 42: m_nDervie = nNumber + 1;
- 0040ECD9 mov ecx,dword ptr [ebp+8]
- 0040ECDC add ecx,1 ; 将参数值加1
- 0040ECDF mov edx,dword ptr [ebp-4] ; edx获得this指针
- ; 参考内存结构,edx+4是子类成员m_nDervie的地址
- 0040ECE2 mov dword ptr [edx+4],ecx
- 43: printf("%d\r\n", GetNumber());
- 0040ECE5 mov ecx,dword ptr [ebp-4]
- 0040ECE8 call @ILT+60(CBase::GetNumber) (00401041)
- 0040ECED push eax
- 0040ECEE push offset string "%d\r\n" (0042501c)
- 0040ECF3 call printf (004012b0)
- 0040ECF8 add esp,8
- 44: printf("%d\r\n", m_nDervie);
- 0040ECFB mov eax,dword ptr [ebp-4] ; eax获得this指针
- ; 参考内存结构,eax+4是子类成员m_nDervie的地址
- 0040ECFE mov ecx,dword ptr [eax+4]
- 0040ED01 push ecx
- 0040ED02 push offset string "%d\r\n" (0042501c)
- 0040ED07 call printf (004012b0)
- 0040ED0C add esp,8
- ; 函数退出代码略
- }
-
- ; 父类成员函数SetNumber分析
- void SetNumber(int nNumber){
- 00401199 pop ecx ; 还原this指针
- 0040119A mov dword ptr [ebp-4],ecx ; [ebp-4]中保留了this指针
- m_nBase = nNumber;
- 0040119D mov eax,dword ptr [ebp-4] ; eax得到this指针
- 004011A0 mov ecx,dword ptr [ebp+8] ; ecx得到参数
- ; 这里的[eax]相当于[this+0],参考内存结构,是父类成员m_nBase
- 004011A3 mov dword ptr [eax],ecx
- }
父类中成员函数SetNumber在子类中并没有被定义,但根据派生关系,子类中可以使用父类的公有函数。编译器是如何实现正确匹配的呢?
如使用对象或对象的指针调用成员函数,编译器可根据对象所属作用域来使用“名称粉碎法”①,以实现正确匹配。在成员函数中调用其他成员函数时,可匹配当前作用域。
在调用父类成员函数时,虽然其this指针传递的是子类对象的首地址,但是在父类成员函数中可以成功寻址到父类中的数据。回想之前提及的对象内存布局,父类数据成员被排列在地址最前端,之后是子类数据成员。ShowNumber运行过程中的内存信息如图12-1所示。
|
| 图12-1 子类对象Dervie的内存布局 |
这时,首地址处为父类数据成员,而父类中的成员函数SetNumber在寻址此数据成员时,会将首地址的4字节数据作为数据成员m_nBase。由此可见,父类数据成员被排列在最前端的目的是为了在添加派生类后方便子类使用父类中的成员数据,并且可以将子类指针当父类指针使用。按照继承顺序依次排列各个数据成员,这样一来,不管是操作子类对象还是父类对象,只要确认了对象的首地址,对父类成员数据的偏移量而言都是一样的。对子类对象而言,使用父类指针或者子类指针都可以正确访问其父类数据。反之,如果使用一个子类对象的指针去访问父类对象,则存在越界访问的危险,如代码清单12-4所示。