今天在写DuiMini的控件类时调试遇到了异常,经过几次定位才找到了问题代码段,步过时发现了一个诡异的BUG:我要调用的fun1函数居然进到了fun2函数里面!这是什么鬼?花了几个小时才解决,由于这个问题比较隐晦且编译器不会爆任何警告,特此记录。

一个栗子

简化的类层次结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
class Base1{
public:
virtual void foo1()=0;
};
class Base2{
public:
virtual void foo2()=0;
};
class Test:public Base1,public Base2{
public:
void foo1() override{
cout<<"Test foo1\n";
}
void foo2() override{
cout<<"Test foo2\n";
}
};

int main(){
Base1* obj1=new Test;
Base2* obj2=(Base2*)obj1;
obj1->foo1();
obj2->foo2();
return 0;
}

你猜输出是什么?是这个么?

Test foo1
Test foo2

然而很抱歉,结果是
result
以上代码在VS2017和Dev-C++中答案均相同。
也就是说我们的 obj2->foo2(); 实际上调用的是 obj1->foo1();!诡异吧!

WHY?

首先,C++使用一种称之为vtable的东西实现virtual函数多态调用。vtable每个类中都有一个,该类的所有对象公用,由编译器帮你生成,只要有virtual函数的类,均会有vtable。在继承过程中,由于类Base1和类Base2都有vtable,所以类Test继承了两个vtable。简单的分析一下new Test出来的对象内存结构,如下:

0 vtable_address_for_Base1 –> [Test::foo1, NULL]
4 vtable_address_for_Base2 –> [Test::foo2, NULL]

其实很简单,就两个vtable的指针,0和4代表相对地址,指针地址大小为4。
obj1的值为0,所以调用obj1->foo1()时,可以正确的找到Test::foo1这个函数执行。

但是当使用强行转换,将obj1转给obj2,那么实质上obj2的值也是0,当调用obj2->foo2()时,无法在第一个vtalbe中找到对应的函数,但是操蛋的编译器却不报错,而是选择执行函数Test::foo1,不知道具体原因,说不定编译器仅仅将所谓函数名看做是一个偏移量(瞎猜)。这种做法十分恶心,导致结果无法预期的(最后调用的函数会与函数申明的循序有关),不太会引起注意,使得bug十分隐晦。

Solution

正确的做法是使用C++恶心的强制转换语法dynamic_cast,像这样:

1
Base2* obj2=dynamic_cast<Base2*>(obj1);

这时候dynamic_cast会根据尖括号中的类型进行指针偏移,所以obj2的值为4,这样就会按照期望的方式执行。

草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草草

参考资料:https://www.cnblogs.com/bourneli/archive/2011/12/28/2305264.html