在我的前一篇文章"透过汇编另眼看世界之函数调用"中,我们通过汇编了解了虚函数调用的全部过程。在本文中我将分析多继承的情况下虚函数调用的情况。

首先还是写一些简单的代码作为本文分析的例子代码:

//the abstract base class
class IBase {
public:
    virtual void func1() = 0;
    virtual void func2() = 0;
};

class IDerive1 : public IBase {
public:
    //virtual functions inherited from IBase 
    virtual void func1() = 0;
    virtual void func2() = 0;

    //new virtual function
    virtual void foobar() = 0;
};

class IDerive2 : public IBase {
public:
    //virtual functions inherited from IBase 
    virtual void func1() = 0;
    virtual void func2() = 0;

    //new virtual function
    virtual void callMe() = 0;
};

class CMyObject : public IDerive1, public IDerive2 {
public:
    //virtual functions inherited from IBase 
    virtual void func1();
    virtual void func2();

    //virtual function inherited from IDerive1
    virtual void foobar();

    //virtual function inherited from IDerive2
    virtual void callMe();

public:
    CMyObject(): m_iValue(0) {}
private:
    int m_iValue;
};
/
//ingore the definitions of all the virtual functions in CMyObject class
int _tmain(int argc, _TCHAR* argv[])
{
  CMyObject obj;
 
  //retreive the IDerive1 interface from the object
  IDerive1* pDerive1 = (IDerive1*)&obj;
  pDerive1->func2();
  pDerive1->foobar();
 
  //retreive the IDerive2 interface from the object
  IDerive2* pDerive2 = (IDerive2*)&obj;
  pDerive2->func2();
  pDerive2->callMe();
 
  //retreive the IDerive2 interface from the IDerive1 interface
  pDerive2 = (IDerive2*)pDerive1;
  pDerive2->func2();
  pDerive2->callMe();
 
  return 0;
}

这里我采用的是和COM中使用的多继承类似的继承关系。IDerive1和IDerive2都继承自同一个抽象基类IBase,而且IDerive1和IDerive2本身还是抽象基类,CMyObject类多继承自IDerive1和IDerive2。

熟悉COM的朋友很自然的就会想到IBase就是COM中的IUnkown接口,而IDerive1和IDerive2就是COM中其他接口和自定义接口,而CMyObject就是COM中的"组件(Component)"。 之所以这样设计的原因是熟悉COM的朋友对这样的类的层次关系会感到很舒服,而且这样的多继承层次关系也是比较简单的,便于分析。

在分析汇编代码之前,我们还需要了解多继承下类对象的内存分布情况。多继承下的类对象的内存分布情况比较复杂,这也是为什么很多人说"不要随便使用多继承"。本文虽然使用了多继承,但是类对象的内存分布情况还是相对比较简单和容易控制的,两个基类都是抽象类,他们没有数据成员,只有一个虚指针,而类对象本身也只有一个int型的成员变量。对于CMyObject对象的内存分布情况,下面是我用VS2002调试器查看CMyObject对象的内存分布情况的截图:

下面是我根据上面的截图,并结合我自己对这部分内容的理解,画了一个简图:

Pointer CMyObject vTable for IDerive1
Pointer CMyObject vTable for IDerive2
m_iValue

下面就继续我们的汇编分析。在这里我并不想分析所有的汇编代码,原因之一就是有些汇编代码和前一篇文章的代码是一样的,这里就不用罗嗦了。另一个原因就是我只关心和本文主题有关的内容,那些和本文的主题没有太多联系的内容就不会出现在我的讨论中。

一。派生类指针到基类指针的转化。由CMyObject指针到IDerive1指针和IDerive2指针转化的汇编代码略有不同:

  ; IDerive1* pDerive1 = (IDerive1*)&obj     
  lea    eax, DWORD PTR _obj$[ebp] 
  mov    DWORD PTR _pDerive1$[ebp], eax
  ; IDerive2* pDerive2 = (IDerive2*)&obj
  lea    eax, DWORD PTR _obj$[ebp]
  test    eax, eax
  je    SHORT $L1774
  lea    ecx, DWORD PTR _obj$[ebp+4]
  mov    DWORD PTR tv73[ebp], ecx
  jmp    SHORT $L1775
$L1774:
  mov    DWORD PTR tv73[ebp], 0
$L1775:
  mov    edx, DWORD PTR tv73[ebp]
  mov    DWORD PTR _pDerive2$[ebp], edx

通过比较我们发现,当CMyObject类指针转化成第二个基类IDerive2指针的时候,除了判断了CMyObject类指针是否为空外,更重要的是,IDerive2指针的值是在CMyObject类指针值的基础上多加了4个字节(一个指针的大小?)。仔细想像,这个不难理解:在多继承的情况下,派生类对象的内存分布是按照基类在派生类中声明的顺序来排列的,在本文中,按照声明顺序,obj的内存分布应该也是基类IDerive1的数据成员,然后是IDerive2的数据成员,最后才是CMyObject的数据成员。由于IDerive1在最前面,而且只有一个虚指针,所以在指针转化的过程中,IDerive1的指针值和CMyObject的指针值是一样的,而IDerive2的指针值就要在CMyObject指针值的基础上加4。

二。基类指针之间转化。下面是由IDerive1指针转化的IDerive2指针的汇编代码:

    ;pDerive2 = (IDerive2*)pDerive1;
    mov    eax, DWORD PTR _pDerive1$[ebp]
    mov    DWORD PTR _pDerive2$[ebp], eax

感到奇怪的是,这里的转化直接将IDerive1的指针赋给了IDerive2的指针。这样的转化合理么?根据上面的分析,我们知道IDerive1的地址和IDerive2的值应该是不相等的,它们之间差4个字节,可是为什么这里编译器却将他们设为相等? 在这种情况下虚函数能正常调用么? 往下看看在说。

三。派生类的虚表。我奇怪的发现,CMyObject有两个虚表:

CONST    SEGMENT
??_7CMyObject@@6BIDerive1@@@ DD FLAT:?func1@CMyObject@@UAEXXZ ; CMyObject::`vftable'
    DD    FLAT:?func2@CMyObject@@UAEXXZ
    DD    FLAT:?foobar@CMyObject@@UAEXXZ
CONST    ENDS
CONST    SEGMENT
??_7CMyObject@@6BIDerive2@@@ DD FLAT:?func1@CMyObject@@W3AEXXZ ; CMyObject::`vftable'
    DD    FLAT:?func2@CMyObject@@W3AEXXZ
    DD    FLAT:?callMe@CMyObject@@UAEXXZ
CONST    ENDS

起初我还以为他们是一样的,但是通过undname.exe对虚表的符号名进行"反修饰",却得到了两个不同的符号名:
??_7CMyObject@@6BIDerive1@@@     const CMyObject::`vftable'{for `IDerive1'}
??_7CMyObject@@6BIDerive2@@@     const CMyObject::`vftable'{for `IDerive2'}

更奇怪的是,通过"反修饰"虚表的虚函数的符号名,我也得到两套不同的符号名:
?func1@CMyObject@@UAEXXZ          public: virtual void __thiscall CMyObject::func1(void)
?func2@CMyObject@@UAEXXZ          public: virtual void __thiscall CMyObject::func2(void)
?foobar@CMyObject@@UAEXXZ        public: virtual void __thiscall CMyObject::foobar(void)

?func1@CMyObject@@W3AEXXZ       [thunk]:public: virtual void __thiscall CMyObject::func1`adjustor{4}' (void)
?func2@CMyObject@@W3AEXXZ       [thunk]:public: virtual void __thiscall CMyObject::func2`adjustor{4}' (void)
?callMe@CMyObject@@UAEXXZ        public: virtual void __thiscall CMyObject::callMe(void)

当我看到"[thunk]"的时候突然就意识到:难道这就是江湖上传说的中的"thunk"? 传说中"thunk"是编译器插入的一小段代码,可以用来实现一些特殊的功能,例如在Win32环境下调用Win16 API,那在多继承下的虚函数调用中,"thunk"又起着什么作用呢?我在汇编代码中找到了"thunk"的代码:

?func1@CMyObject@@W3AEXXZ PROC NEAR            ; CMyObject::func1, COMDAT
    sub    ecx, 4
    jmp    ?func1@CMyObject@@UAEXXZ                  ; CMyObject::func1
?func1@CMyObject@@W3AEXXZ ENDP                    ; CMyObject::func1
?func2@CMyObject@@W3AEXXZ PROC NEAR   ; CMyObject::func2, COMDAT
   sub ecx, 4
   jmp ?func2@CMyObject@@UAEXXZ  ; CMyObject::func2
?func2@CMyObject@@W3AEXXZ ENDP    ; CMyObject::func2

由上面汇编代码可以看出,"thunk"代码并不是那么神秘,它只是简单的将寄存器的值减4(一个指针的大小?),然后跳转到另外一个函数。为什么是ECX?为什么是减4?ECX在虚函数调用的过程中不是存放this指针的寄存器么?结合着本文中的类的继承层次关系,我开始慢慢的明白了"thunk"的任务。在多继承的情况下,各基类指针的值应该是不一样的,只有第一个基类的指针值和派生类类对象的首地址是一致的,其他的基类指针和派生类对象的首地址存在一个偏移。假如多个基类也都从一个共同的基类继承而来,理论上说我们可以通过任何一个基类指针去调用这个共同基类的虚函数,这个虚函数调用会被解析到派生类的虚函数实现,而且派生类也只能有一个虚函数实现。为了使通过任何一个基类指针调用的虚函数都调用同一个函数,我们只需要将这样的虚函数调用"转化"到通过第一个基类指针来调用就可以了,而在第一个基类的虚表中存放虚函数的实现。这个转化的过程就是由"thunk"来完成的:它首先将基类指针调整到第一个基类的地址,也就是派生类对象的首地址,然后调用相应的虚函数。

有了这样的分析,我们就可以画出虚表的大致情况:

CMyObject vTable for IDerive1
&CMyObject::func1()
&CMyObject::func2() 
&CMyObject::foobar()

CMyObject vTable for IDerive2

&thunk for CMyObject::func1()
&thunk for CMyObject::func2()
&CMyObject::callme()

接着再回到基类指针之间转化的那个问题:

    pDerive2 = (IDerive2*)pDerive1;
    pDerive2->func2();
    pDerive2->callMe();

此时通过pDerive2能够获得虚表的应该是IDerive1的虚表,所以调用func2()的时候,应该没有thunk发生的。而调用callMe()的时候实际上调用的是foobar(),应该它在IDerive1虚表中偏移量和callMe()在IDerive2虚表中的偏移量是一样的。呜!!!,这个是个错误么?是个Bug么?我也不知道。

11/04/2006 于家中

V1.1

还是基类指针之间转化的问题

根据网友sting的回复,我也明白了这里为什么转化不成功的原因。由于IDerive1和IDerive2之间并没有什么继承关系(虽然他们是另一个派生类的基类),编译器就把他们当作两个"毫无关系"的类,在转化的过程中只能进行简单的赋值,这样的转化形式在C++被定义为reinterpret_cast。

这里有两个方法进行正确的转化:

1。先将一个基类转化到派生类,然后通过派生类再转化到另一个基类。相应的代码可以是这样的:
pDerive1 = static_cast<IDerive1*>( static_cast<CMyObject*>(pDerive2) );
pDerive2 = static_cast<IDerive2*>( static_cast<CMyObject*>(pDerive1) );

2。使用dynamic_cast来转化。要想使dynamic_cast能够正常的工作,我们需要开启"运行时类型标识(RTTI)"。运行时类型标识为处于同一个继承链上的所有类建立了一张"关系网",这样任何两个类之间就有了"千丝万缕"的关系,这样就为他们之间的直接转化提供了可能。相应的代码可以时这样的:
pDerive1 = dynamic_cast<IDerive1*>(pDerive2);
pDerive2 = dynamic_cast<IDerive2*>(pDerive1);

11/11/2006 于家中  
今天是11月11日,光棍节 。虽然我不是光棍,但是正和女朋冷战中 ,希望早日结束冷战

附注:
1。在VS2002中,我们可以通过下面的方式开启运行时类型标识:
  在解决方案资源管理器中选择Project --> C/C++ --> 语言 -->  启用运行时类型信息,选择"是" --> 确定

透过汇编另眼看世界之多继承下的虚函数函数调用相关推荐

  1. 透过汇编另眼看世界之函数调用

    在我的另外一篇文章中 ,我提到了要通过汇编语言来分析虚函数调用的真相.我们现在就开始踏上这次艰辛却非常有意思的旅程.其他闲话少说,直接进入主题.本文中使用的C++代码: #include " ...

  2. 透过汇编另眼看世界之DLL导出函数调用

    前言:我一直对DLL技术充满好奇,一方面是因为我对DLL的导入/导出机制还不是特别的了解,另一面是因为我发现:DLL技术在Windows平台下占有重要的地位,几乎所有的Win32 API都是以导出函数 ...

  3. Cpp 对象模型探索 / 继承关系下的虚函数手动调用

    一.多态机理 #include <iostream>class Father { public:virtual void Func1(){std::cout << " ...

  4. C++基本概念复习之二:多重继承、虚继承、纯虚函数(抽象类)

    一.多重继承: #include <iostream> using namespace std; class Horse { public: Horse(){cout<<&qu ...

  5. C++程序运行时内存布局之--无继承情况下的虚函数

    2019独角兽企业重金招聘Python工程师标准>>> 虚函数是C++实现多态的关键,没有虚函数,C++只能是OB,不能完成OO. 本文介绍的是没有继承情况下,带有虚函数的类在内存中 ...

  6. C++中虚函数的理解,以及简单继承情况下的虚函数的表!

    面向对象的三大特征=封装性+继承性+多态性 封装=将客观事物抽象成类,每个类对自身的数据和方法实行权限的控制 继承=实现继承+可视继承+接口继承 多态=将父类对象设置成为和一个或者更多它的子对象相等的 ...

  7. C++ 论公有继承时纯虚函数、虚函数、普通函数的行为表现及虚函数的重写(深度好文)

    文章目录 1 公有继承时三种类型的函数行为 1.1 纯虚函数 (pure virtual) 1.2 普通虚函数 **1.2.1 方法一** **1.2.2 方法二** 1.3 非虚函数 2 重写 (o ...

  8. 透过地图看深度世界:鲲鹏加持下,超图如何让数字政府再深化?

    文 | 曾响铃 来源 | 科技向令说(xiangling0815) 中心大屏幕上,一张三维地图正在动态展示着人口.企业.交通.绿化.水域等各类与政府管理职能直接相关的指标,来自遥感影像.激光点云.遍布 ...

  9. Unity2D学习笔记Day12:敌人统一死亡动画+Class的继承(含虚函数virtual,重写override)

    学习资源:B站 M_Studio<Unity教程2D入门>17 Unity Assets:Sunnyland Day12 调用frog死亡动画效果 这里我没有完全按照教程的方法,是自己写的 ...

最新文章

  1. ASP.NET WebApi技术从入门到实战演练
  2. php自动生成新闻页,自动发布新闻页面的php代码
  3. 2020-12-6(从反汇编理解指针和引用的区别)
  4. HeartBeat 集群组件概述
  5. TCP IP基础知识的复习
  6. 马斯克投15亿,价值47000美元的比特币,其实是一群数学家搞出来的……
  7. HDU2501_多米诺骨牌
  8. 开源软件 许可证密钥_自由和开源软件-1中的重要许可证
  9. python字典导入mongodb_python连接mongoDB进行数据提取→常用操作指南
  10. 习题3.11 表达式转换 (25分)
  11. Create React App 2.0 正式发布:Babel 7、webpack 4 等
  12. iOS Unicode转中文(UTF-8)
  13. jdk的wsimport方法实现webservice客户端调用服务
  14. python 项目目录结构
  15. 2022短视频去水印小程序带流量主/CPS/资源/工具/批量解析/修改MD5
  16. html中单元格向下合并单元格,html中单元格合并 HTML 怎么给合并单元格设置宽度...
  17. CSS3实现折角效果
  18. java判断闰年的方法_Java判断闰年的2种方法示例|chu
  19. 图解阿里达摩院内部结构
  20. nagios NSCA 被动检测

热门文章

  1. ThreadLocal的两种用法总结
  2. JDK9的JShell简单使用
  3. MapReduce的构思和框架结构
  4. 数据库-多表查询-笛卡尔积
  5. 深入理解Kafka(2)-Producer
  6. 【报错笔记】在eclipse中做Spring项目时,创建Spring容器时老是出错
  7. iOS相关,过年回来电脑上的证书都失效了
  8. SQL Server表结构和数据导入到MySQL
  9. 10 个非常有用的 AngularJS 框架
  10. Mysql学习笔记(三)运算符和控制流函数