12.1 识别类和类之间的关系(4)
代码清单12-4 子类对象的指针访问父类对象存在的危险—Debug版
- // C++(www.cppentry.com)源码说明:类型定义见代码清单12-1
- int nTest = 0x87654093;
- CBase base;
- CDervie *pDervie = (CDervie *)&base;
- printf("%x\r\n", pDervie->m_nDervie);
- 对应的反汇编讲解如下:
- 54: int nTest = 0x87654093;
- 00401138 mov dword ptr [ebp-4],87654093h ; 局部变量赋初值
- 55: CBase base;
- 0040113F lea ecx,[ebp-8] ; 传递this指针
- 00401142 call @ILT+20(CBase:: CBase) (00401019) ; 调用构造函数
- 56: CDervie *pDervie = (CDervie *)&base;
- 00401147 lea eax,[ebp-8]
- 0040114A mov dword ptr [ebp-0Ch],eax ; 指针变量[ebp-0Ch]得到base的地址
- 57: printf("%x\r\n", pDervie->m_nDervie);
- 0040114D mov ecx,dword ptr [ebp-0Ch]
- ;注意,ecx中保留了base的地址,而[ecx+4]的访问超出了base的内存范围,实际上,这里访问局部变
- ;量nTest的内存空间
- 00401150 mov edx,dword ptr [ecx+4]
- 00401153 push edx
- 00401154 push offset string "%x\r\n" (0042201c)
- 00401159 call printf (00401210)
- 0040115E add esp,8
学习虚函数时,我们分析了类中的隐藏数据成员—虚表指针。正因为有这个虚表指针,调用虚函数的方式改为查表并间接调用,在虚表中得到函数首地址并跳转到此地址处执行代码。利用此特性即可通过父类指针访问不同的派生类。在调用父类中定义的虚函数时,根据指针所指向的对象中的虚表指针,可得到虚表信息,间接调用虚函数,即构成了多态。
以“人”为基类,可以派生出不同国家的人:中国人、美国人、德国人等。这些人有着一个共同的功能—说话,但是他们实现这个功能的过程不同,例如,中国人说汉语、美国人说英语、德国人说德语等。每个国家的人都有不同的说话方法,为了让“说话”这个方法有一个通用接口,可以设立一个“人”类将其抽象化。使用“人”类的指针或引用调用具体对象的“说话”方法,这样就形成了多态。此关系的描述如代码清单12-5所示。
代码清单12-5 人类说话方法的多态模拟类结构—C++(www.cppentry.com)源码
- class CPerson{ // 基类—"人"类
- public:
- CPerson(){}
- virtual ~CPerson(){}
- virtual void ShowSpeak(){ // 纯虚函数,后面会讲解
- }
- };
-
- class CChinese : public CPerson{ // 中国人:继承自人类
- public:
- CChinese(){}
- virtual ~CChinese(){}
- virtual void ShowSpeak(){ // 覆盖基类虚函数
- printf("Speak Chinese\r\n");
- }
- };
- class CAmerican : public CPerson{ // 美国人:继承自人类
- public:
- CAmerican(){}
- virtual ~CAmerican(){}
- virtual void ShowSpeak(){ // 覆盖基类虚函数
- printf("Speak American\r\n");
- }
- };
- class CGerman : public CPerson{ // 德国人:继承自人类
- public:
- CGerman(){}
- virtual ~CGerman(){}
- virtual void ShowSpeak(){ // 覆盖基类虚函数
- printf("Speak German\r\n");
- }
- };
- void Speak(CPerson * pPerson){ // 根据虚表信息获取虚函数首地址并调用
- pPerson->ShowSpeak();
- }
- // main函数实现代码
- void main(int argc, char* argv[]){
- CChinese Chinese;
- CAmerican American;
- CGerman German;
- Speak (&Chinese);
- Speak (&American);
- Speak (&German);
- }
在代码清单12-5中,利用父类指针可以指向子类的特性,可以间接调用各子类中的虚函数。虽然指针类型为父类,但由于虚表的排列顺序是按虚函数在类继承层次中首次声明的顺序依次排列的,因此,只要继承了父类,其派生类的虚表中的父类部分的排列就与父类一致,子类新定义的虚函数将会按照声明顺序紧跟其后。所以,在调用过程中,我们给Speak函数传递任何一个基于CPerson的派生对象地址都能够正确调用虚函数ShowSpeak。在调用虚函数的过程中,程序是如何通过虚表指针访问虚函数的呢?具体分析如代码清单12-6所示。
代码清单12-6 虚函数调用过程—Debug版
- // main函数分析略
- // Speak函数讲解
- void Speak (CPerson * pPerson){
- pPerson->ShowSpeak();
- 00401108 mov eax,dword ptr [ebp+8] // eax获取参数pPerson的值
- 0040110B mov edx,dword ptr [eax] // 取虚表首地址并传递给edx
- 0040110D mov esi,esp
- 0040110F mov ecx,dword ptr [ebp+8] // 设置this指针
- // 利用虚表指针edx,间接调用函数。回顾父类CPerson的类型声明,其中第一个声明的虚函数是析构函数,
- // 第二个声明的虚函数是ShowSpeak,所以ShowSpeak在虚表中的位置排第二,[edx+4]即ShowSpeak
- // 的函数地址
- 00401112 call dword ptr [edx+4]
- 00401115 cmp esi,esp
- 00401117 call __chkesp (004017c0)
- }