代码改变世界

C++虚继承初探

  curer  阅读(3988)  评论(8编辑  收藏  举报

        昨天和同学对c++虚继承这部分 产生了一些争论,发觉自己对技术越来越浮躁了。不得不痛下决心。一看c++虚继承的内部实现(很浅很浅的看看)。

        以下内容来自自己实验,希望各位大哥指点。当然要想获得权威的解释,看《Inside C++ Object Model》

        让我们从最简单的开始。以下测试代码。

代码class Base{public:    Base()    {        printf("Base construct!\n");    }    //virtual void Test()=0;    virtual void f()    {        printf("Base\n");    }    virtual void f2()    {        printf("Base2\n");    }    virtual void f3()    {        printf("Base3\n");    }    void f4()    {        printf("Base4\n");    }};class Derived: public Base{public:    Derived()    {        printf("Derived construct!\n");    }    virtual void f()    {        printf("Derived\n");    }    virtual void f2()    {        printf("Derived2\n");    }    virtual void f3()    {        printf("Derived3\n");    }    void f4()    {        printf("Derived4\n");    }    /*virtual void Test()    {        printf("test\n");    }*/};int main(){    Base *p=new Base;    p->f();    p->f2();    p->f3();    p->f4();    /*Base *p = new Derived;*/    p = new Derived;    p->f();    p->f2();    p->f3();    p->f4();    //p->Test();    delete p;    return 0;}


 
以下是在我的环境下反汇编的部分代码。我的环境是vs2008 默认的Release。
代码.text:00401060 ; int __cdecl main(int argc, const char **argv, const char **envp).text:00401060 _main           proc near               ; CODE XREF: __tmainCRTStartup+10Ap.text:00401060.text:00401060 argc            = dword ptr  4.text:00401060 argv            = dword ptr  8.text:00401060 envp            = dword ptr  0Ch.text:00401060.text:00401060                 push    esi.text:00401061                 push    edi.text:00401062                 push    4               ; unsigned int.text:00401064                 call    ??2@YAPAXI@Z_0  ; operator new(uint).text:00401069                 mov     edi, ds:__imp__printf.text:0040106F                 mov     esi, eax.text:00401071                 add     esp, 4.text:00401074                 test    esi, esi.text:00401076                 jz      short loc_40108A.text:00401078                 push    offset aBaseConstruct ; "Base construct!\n".text:0040107D                 mov     dword ptr [esi], offset ??_7Base@@6B@ ; const Base::`vftable'.text:00401083                 call    edi ; __imp__printf.text:00401085                 add     esp, 4.text:00401088                 jmp     short loc_40108C


 
.text:0040107D mov dword ptr [esi], offset ??_7Base@@6B@ ; const Base::`vftable' 是关键,根据上面分析,将指向Base类
的虚表的指针保存到了向堆中分配的空间中,也就是 esi=**base_vtbl
代码.text:0040108C                 mov     eax, [esi].text:0040108E                 mov     edx, [eax]   ;这里就好理解了,eax=*base_vtbl,edx=base_vtbl.text:00401090                 mov     ecx, esi.text:00401092                 call    edx          ;调用虚表中的第一个函数以下类推.text:00401094                 mov     eax, [esi].text:00401096                 mov     edx, [eax+4].text:00401099                 mov     ecx, esi.text:0040109B                 call    edx.text:0040109D                 mov     eax, [esi].text:0040109F                 mov     edx, [eax+8].text:004010A2                 mov     ecx, esi.text:004010A4                 call    edx.text:004010A6                 push    offset aBase4   ; "Base4\n" ;这里看出了非虚函数的优势,效率高,直接调用函数.text:004010AB                 call    edi ; __imp__printf


 
这是Base虚表内容
代码.rdata:0040216C ; const Base::`vftable'.rdata:0040216C ??_7Base@@6B@   dd offset ?f@Base@@UAEXXZ ; DATA XREF: _main+1Do  ;这里每个标号都指向相应函数.rdata:0040216C                                         ; _main+62o.rdata:0040216C                                         ; Base::f(void).rdata:00402170                 dd offset ?f2@Base@@UAEXXZ ; Base::f2(void).rdata:00402174                 dd offset ?f3@Base@@UAEXXZ ; Base::f3(void).rdata:00402178                 dd offset ??_R4Derived@@6B@ ; const Derived::`RTTI Complete Object Locator' ;这个不懂


 
Base 还是比较简单的,让我们看Derived
代码.text:004010BD                 push    offset aBaseConstruct ; "Base construct!\n".text:004010C2                 mov     dword ptr [esi], offset ??_7Base@@6B@ ; const Base::`vftable'.text:004010C8                 call    edi ; __imp__printf.text:004010CA                 push    offset aDerivedConstru ; "Derived construct!\n".text:004010CF                 mov     dword ptr [esi], offset ??_7Derived@@6B@ ; const Derived::`vftable'.text:004010D5                 call    edi ; __imp__printf.text:004010D7                 add     esp, 8


 
可见在构造函数中和我们想象的完全一样,从基类开始,不过需要注意一点,最后esi=**Derived_vtbl
 以后的代码完全和在基类中调用函数一致。看来在VS2008中,c++的虚表其实就是数组(原来居然还以为是链表,不过似乎也有的编译器
 是用链表实现的)。这个例子的确不复杂,但是事实上却没有这么简单。看下一个稍微复杂一点的。
代码class A{public:    A()    {        printf("A construct\n");    }    virtual void f(){printf("A_F\n");}};class B{public:    B()    {        printf("B construct\n");    }    virtual void f(){printf("B_F\n");}    virtual void g(){printf("B_G\n");}};class C: public A,public B{public:    C()    {        printf("C construct\n");    }    void f(){printf("C_f\n");}};int _tmain(int argc, _TCHAR* argv[]){    A *a=new A;    B *b=new B;    C *c=new C;    a->f();    b->f();    b->g();    c->f();    return 0;}


 

     先不看结果,花几分钟思考一下,class C 的虚表结构是什么?

     首先看代码,发现在class C中首先有一点不同,这个是之前的在class A,classB,classC中都是默认构造函数的代码

代码.text:00401077                 push    8               ; unsigned int      ;以前class只放一个指针,现在2个了。.text:00401079                 call    ??2@YAPAXI@Z_0  ; operator new(uint).text:0040107E                 add     esp, 4.text:00401081                 test    eax, eax.text:00401083                 jz      short loc_40109D.text:00401085                 mov     dword ptr [eax+4], offset ??_7B@@6B@ ; const B::`vftable'.text:0040108C                 mov     dword ptr [eax], offset ??_7C@@6BA@@@ ; const C::`vftable'{for `A'}.text:00401092                 mov     dword ptr [eax+4], offset ??_7C@@6BB@@@ ; const C::`vftable'{for `B'}.text:00401099                 mov     edi, eax.text:0040109B                 jmp     short loc_40109F


 
这个是上面代码真正的反汇编代码,对比下,就可能对上面代码为什么有一个这么冗余的代码,似乎有些感觉了。
 代码.text:004010A6                 push    offset aAConstruct ; "A construct\n".text:004010AB                 mov     dword ptr [esi], offset ??_7A@@6B@ ; const A::`vftable'.text:004010B1                 call    edi ; __imp__printf.text:004010B3                 push    offset aBConstruct ; "B construct\n".text:004010B8                 mov     dword ptr [esi+4], offset ??_7B@@6B@ ; const B::`vftable'.text:004010BF                 call    edi ; __imp__printf.text:004010C1                 push    offset aCConstruct ; "C construct\n".text:004010C6                 mov     dword ptr [esi], offset ??_7C@@6BA@@@ ; const C::`vftable'{for `A'}.text:004010CC                 mov     dword ptr [esi+4], offset ??_7C@@6BB@@@ ; const C::`vftable'{for `B'}.text:004010D3                 call    edi ; __imp__printf.text:004010D5                 add     esp, 0Ch


 
下面的大部分容易理解,关键的是在class B的虚表中的f()。
代码; [thunk]:public: virtual void __thiscall C::f`adjustor{4}' (void)?f@C@@W3AEXXZ proc near               ;这时ecx 也就是this是指向class B的sub     ecx, 4                        ;这里很明显将原来的指向B:f(),指向了class C的虚表的开始部分。ecx放的是this指针jmp     ?f@C@@UAEXXZ    ; C::f(void)  ;这里顺理成章的变成了C::f(),this也在上部改变了?f@C@@W3AEXXZ endp


 
这里似乎就是传说中的“形式转换程序”,这个的确减少了虚表的体积。
再看后面的代码,函数调用的时候和之前完全一致,也就是在class C中定义的f(),虽然没有被显示的声明为virtual,但vs2008已经
把他默认当成虚函数调用了。至此,和同学的争论就此结束。
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述
点击右上角即可分享
微信分享提示