[C++]一个由C-Style类型转换引发的血案

没事搜了一下自己域名,无意间发现老早提交的open directory申请居然通过了…想到一个多月没写东西,大惭…

为了尽快阻止这个连续N天没有日志的记录,先找一篇凑数…

先上开胃小菜 Appetizer

有人在byr论坛C++版上问了这样一道C++面试题:

class A {
    public: void fun()    {    }
};

class B: public A {
    public: virtual void fun() {    }
};

class C: public B {
    public: void fun(){}
};

class D: virtual public A {
public:
    void fun(){}
};

int main(void)  {
    void *p;
    ((A*)NULL)->fun();
    ((C*)NULL)->fun(); // why fault here?
    ((D*)NULL)->fun();
    return 0;
}

为什么在((C*)NULL)->fun()这句挂了呢?


原因很简单:B的fun是虚函数, 而C继承B, 所以通过C对象指针调用fun的时候触发基类virtual方法的多态特性, 需要查通过vptr查询虚表, 但是对NULL强制类型转换并不会设置vptr(vptr的设置是在构造函数中完成的),编译器就把地址0(NULL)中的内容当vptr,一解引用就segment fault了。

其实一开始看((A*)NULL)->fun();这句也觉得别扭,没有构造对象就直接调用非static的成员方法,居然还没有运行错误。想了一想,实际上在编译期,编译器都会通过一个name mangling机制将类的成员函数转换成一个具有唯一名字的非成员函数。程序开始运行时,成员函数和非成员函数一样都被载入到内存。所以只要在调用的成员函数不用到需要由类构造函数构造的成分(比如成员变量和vptr),这种通过指针调用成员函数就不会出错。

其实这个问题并不复杂,但是jmpesp老兄在回帖中写了一道更恶心的题目。

主菜来了 Main Dishes

class A {  
public:  
    virtual void fun(float) { cout << "A"; }  
};  
 
class B {  
public:  
    virtual void fun(int) { cout << "B"; }  
};  
 
class C: public B, public A {  
public:  
    void fun(float) { cout << "C float"; }
    void fun(int)   { cout << "C int"; }
}; 

int main(void) {       
    C* p = new C;
    ((B*)(A*)p)->fun(1);
    delete p;
    return 0;
}

问该段代码的输出是什么?

自己编译运行了一下输出是:C float…

首先输出C是确定的,因为无论怎么转型, p都是指向一个C对象,而A、B中的fun都是虚函数,所以调用的肯定是C类方法。输出float看起来比较诡异,原因肯定在这个(B*)(A*)c-style转型上。

     cout << (A*)p << endl;
     cout << (B*)p << endl;
     cout << (B*)(A*)p << endl;

上面的代码得到以下输出:
00382E30
00382E34
00382E30

可见(B*)(A*)p和(A*)p是相同的。

分析如下:

C继承了A和B,所以一个C对象里包含了一个A对象和B对象。由上面的输出知道(A*)p使得p偏移到了C对象中的A对象,是个static_cast。 接下来(B*)再对指向A对象的p进行转型,这时候问题就出现了,B对象并不包含A对象,编译器也不知道p指向的A对象实际上是包含在一个C对象里,所以 (B*)这次转换,只是改变了p的静态类型,并没有改变p指向的位置,是个reinterpret_cast也就是说p还是指向一个A对象。所以通过p调用fun(1)的时候,查的是A的虚表,先查到virtual void A::fun(float),再到void C::fun(float)

这个问题的关键就在(B*)(A*)p这两个C-Style的函数式转型(functional cast)上:
(A*)p是从派生类C到基类A的转换,有可能是个向从派生类到基类的static_cast,也有可能是个简单的reinterpret_cast, 从运行结果上看(A*)是个static_cast,而由(A*)到(B*)的转换不可能是static_cast(static_cast (static_cast(p)))是编译不过的),所以只能是reinterpret_cast

这里又出现了一个问题,为什么(A*)p是个static_cast而不是reinterpret_cast?至少这与我的直觉不服,在ISO C++标准5.4节找到如下定义(见图):

也就是说一个C-style的强制类型转换如果可以解释成多个列表里的C++ style转型,取在列表里位置最前面的一个。static_cast在reinterpret_cast前,所以得到了下面的结果:

(B*)(A*)p 等价于 reinterpret_cast(static_cast(p))

由这道题目说明(Conclusion):

  1. 千万不要用C-style对对象指针进行转型,请用C++-style的static_cast、reinterpret_cast和dynamic_cast,以免造成意料之外的错误。
  2. C++标准是个好东西,它胜过任何技术手册,一旦对语言特性有困惑,查标准是最好的解决方法。
This entry was posted in 技术学习 and tagged . Bookmark the permalink.

3 Responses to [C++]一个由C-Style类型转换引发的血案

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据