6
回答
C++多重继承中虚函数表的问题
终于搞明白,存储TCO原来是这样算的>>>   

      今天在研究多重继承的虚函数表的时候遇到了一些问题,设计了一个多重继承的类,然后在gcc内看他的内存,看内存的时候发现了一些问题,理解不能。类结构如下:

class Base1
{
public:
    Base1();
    virtual ~Base1();
    virtual void speakClearly();
    virtual Base1 *clone();// const;
    int data_Base1;
};
class Base2
{
public:
    Base2();
    virtual ~Base2();
    virtual void mumble();
    virtual Base2 *clone();// const;
    int data_Base2;
};
class Derived : public Base1, public Base2
{
public:
    Derived();
    virtual ~Derived();
    virtual Derived *clone();// const;
    int data_Derived;
};
     使用g++4.9.2编译代码,然后用gdb调试查看内存的时候发现虚函数表的内存结构如下:

    总共有3个问题:

1. 0x100002090 <_ZTV7Derived+16>: 0x10000145e <Derived::~Derived()> 0x100001526 <Derived::~Derived()>

虚函数表中为什么有两个析构函数,代码段地址分别为0x10000145e和0x100001526

2. 0x1000020c0 <_ZTV7Derived+64>: 0x10000151c <_ZThn16_N7DerivedD1Ev> 0x10000154c <_ZThn16_N7DerivedD0Ev>

此处地址0x10000151c和0x10000154c到底是什么代码,看着好奇怪,我把0x10000151c这个代码段的内存打印了出来,看看是否能有所帮助(不知道有没有朋友了解怎样可以通过gdb打印出代码段的c++代码,或者汇编代码,而不是只是二进制码,不然看着好累):

另外0x1000020d0 <_ZTV7Derived+80>: 0x1000011d4 <Base2::mumble()> 0x1000015bd <_ZTchn16_h16_N7Derived5cloneEv>

此处这个0x1000015bd地址处的函数代码应该跟上面的是同一个问题。

3. _ZTV5Base1和 _ZTI5Base1 这两个编译标记的前缀ZTV和ZTI代表的是什么意思,看着很奇怪的样子。


希望能有c++方面(或者应该是编译原理方面)的高人来帮助解答这些困惑,多谢啦

GCC
举报
sworda
发帖于3年前 6回/741阅
共有6个答案 最后回答: 3年前

你说的两个虚函数的问题,我猜是因为虚构函数是必须的(编译器没规定虚构函数是虚函数,但是考虑到子类的析构都是写成虚函数。是否意味着默认析构函数是会被编译器保留一个座位呢?无论是否有自己重载虚构)。所以编译器会生成一个默认析构函数,你写的虚构函数被编译器理解成了一个虚构的‘重载’。这只是猜测。

你说的问题估计很难有人能够回答你。有几个方式可以分析。不过都是在Windows平台。

方式一,用vs看调试信息吧。gdb用着挺操蛋的。

方式二,用ida Pro看编译出来的静态反汇编。

好吧,你的C++算比较OK了。别折腾多重继承了,个人觉得如果没有特殊需求,绝对不要用多继承。

引用来自“songtzu”的评论

你说的两个虚函数的问题,我猜是因为虚构函数是必须的(编译器没规定虚构函数是虚函数,但是考虑到子类的析构都是写成虚函数。是否意味着默认析构函数是会被编译器保留一个座位呢?无论是否有自己重载虚构)。所以编译器会生成一个默认析构函数,你写的虚构函数被编译器理解成了一个虚构的‘重载’。这只是猜测。

你说的问题估计很难有人能够回答你。有几个方式可以分析。不过都是在Windows平台。

方式一,用vs看调试信息吧。gdb用着挺操蛋的。

方式二,用ida Pro看编译出来的静态反汇编。

好吧,你的C++算比较OK了。别折腾多重继承了,个人觉得如果没有特殊需求,绝对不要用多继承。

你不妨删掉你自己定义的虚构的定义,仅使用编译器默认添加的析构函数。

再用gdb看看是否还有两虚构函数,如果只有一个了。说明我的猜测是对的。

--- 共有 1 条评论 ---
sworda看我后面的评论吧,这个回复没法贴图。。。 3年前 回复

@songtzu

非虚函数的析构函数

class Base1
{
public:
    Base1();
    ~Base1()
    {   
        cout << "Base1::~Base1()" << endl;
    }   
    virtual void speakClearly();
    virtual Base1 *clone();// const;
    int data_Base1;
};



虚析构函数:

class Base2
{
public:
    Base2();
    virtual ~Base2()
    {   
        cout << "Base2::~Base2()" << endl;
    }   
    virtual void mumble();
    virtual Base2 *clone();// const;
    int data_Base2;
};



在没有把析构函数定义成虚函数的时候它并不会出现在虚函数表中

而定义成虚函数之后,则会出现两个虚函数:

看着还是很奇怪,为什么总是两个,Base2是没有父类的,定义的这个对象也是Base2的对象

我试了下,第一个虚函数,是根据用于定义的虚析构生成的函数,在执行用户操作的后调用了通过地址直接调了基类的虚析构函数,调用,第二个虚函数只是简单通过地址的直接调用第一个自己写的虚析构,不知道生成这样的代码有何意义
--- 共有 4 条评论 ---
Ivnoidea直接 3年前 回复
sw55555第二个函数是通过虚表偏移来调用第一个函数么?还是直接调用? 3年前 回复
Ivnoidea第一个也在虚函数表中,也可以通过偏移来调用啊 3年前 回复
sw55555第一种应该是编译器可以推断出实例类型时调用的(应为编译器的优化),第二种则应该是通过虚表偏移来调用。 3年前 回复

引用来自“十分不强力”的评论

我试了下,第一个虚函数,是根据用于定义的虚析构生成的函数,在执行用户操作的后调用了通过地址直接调了基类的虚析构函数,调用,第二个虚函数只是简单通过地址的直接调用第一个自己写的虚析构,不知道生成这样的代码有何意义
vc汇编代码如下:  
Derived d;
 lea         ecx,[ebp-28h]  
 call        Derived::Derived (2011B3h)  
 mov         dword ptr [ebp-4],0  
    d.clone();
 lea         ecx,[ebp-28h]  
 call        Derived::clone (20115Eh)  
    d.mumble();
 lea         ecx,[ebp-20h]  
 call        Base2::mumble (20114Ah)  
    d.speakClearly();
 lea         ecx,[ebp-28h]  
 call        Base1::speakClearly (201172h)  
    Base1 *b1 = dynamic_cast<Base1*>(&d);
 lea         eax,[ebp-28h]  
 mov         dword ptr [ebp-34h],eax  
    b1->clone();
 mov         eax,dword ptr [ebp-34h]  
 mov         edx,dword ptr [eax]  
 mov         esi,esp  
 mov         ecx,dword ptr [ebp-34h]  
 mov         eax,dword ptr [edx+8]  
 call        eax  
 cmp         esi,esp  
 call        __RTC_CheckEsp (20119Fh)  
    b1->speakClearly();
 mov         eax,dword ptr [ebp-34h]  
 mov         edx,dword ptr [eax]  
 mov         esi,esp  
 mov         ecx,dword ptr [ebp-34h]  
 mov         eax,dword ptr [edx+4]  
 call        eax  
 cmp         esi,esp  
 call        __RTC_CheckEsp (20119Fh)  
    Base2 *b2 = dynamic_cast<Base2*>(&d);
 lea         eax,[ebp-28h]  
 test        eax,eax  
 je          main+0AEh (20467Eh)  
 lea         ecx,[ebp-28h]  
 add         ecx,8  
 mov         dword ptr [ebp-108h],ecx  
 jmp         main+0B8h (204688h)  
 mov         dword ptr [ebp-108h],0  
 mov         edx,dword ptr [ebp-108h]  
 mov         dword ptr [ebp-40h],edx  
    b2->clone();
 mov         eax,dword ptr [ebp-40h]  
 mov         edx,dword ptr [eax]  
 mov         esi,esp  
 mov         ecx,dword ptr [ebp-40h]  
 mov         eax,dword ptr [edx+8]  
 call        eax  
 cmp         esi,esp  
 call        __RTC_CheckEsp (20119Fh)  
    b2->mumble();
 mov         eax,dword ptr [ebp-40h]  
 mov         edx,dword ptr [eax]  
 mov         esi,esp  
 mov         ecx,dword ptr [ebp-40h]  
 mov         eax,dword ptr [edx+4]  
 call        eax  
 cmp         esi,esp  
 call        __RTC_CheckEsp (20119Fh)
--- 共有 2 条评论 ---
Ivnoidea测试了下在vs2013中设置徐析构后虚表中只有一个间接调用析构函数的函数,而析构函数本身的地址不在虚表中,而在gcc生成的代码中是析构函数本身也在虚表中 3年前 回复
sw55555第一种对象直接调用可以判断出要调用的函数,所以直接调用; 第二种则是使用父类指针调用编译器无法判断要调用的函数,于是使用虚表偏移来调用 3年前 回复
顶部