最近在项目上遇到了一个C++中常见的疑难杂症。有些令人哭笑不得的感觉。先不多说,看下文。
有两个Project -- A 和 B. A会被编译链接成为一个dll文件,而B会生成一个exe文件。
在Project A中有一个类 C 和 一个类 D,如下:
class D {
......
public:
void d_f1() {
// do not access any member variables here
}
void d_f2() {
d_f1();
m_value = 100;
......
}
......
private:
int m_value;
......
};
class C {
......
public:
C() {
#ifdef __XXX__
p = new D;
#endif
}
~C() {
#ifdef __XXX__
delete p;
#endif
}
void c_f() {
#ifdef __XXX__
p->d_f2();
#endif
}
......
private:
......
#ifdef __XXX__
D* p;
#endif
......
};
在Project B中有一个类 X 及其成员函数 f(), 其定义大约如下:
class X {
......
public:
X() { C m_var; }
public:
void f() {
m_var.c_f();
int a = 100;
.....
}
......
private:
C m_var;
......
};
具体现象如下:
X x;
x.f(); // 一切正常
...
x.f(); // 程序崩溃
因为上文已经将本问题极大地浓缩,相信聪明而经验丰富的你或许已经看出了问题所在。。。没看出来?!没关系,我们来debug一下:)不过实在要吐槽的是,本问题中的工程都是QT中的工程,而QT的Debugger虽然选的仍是微软的CDB,但是那个慢呀!按一下F10,至少要等半分钟才有反应,有时甚至好几分钟。所以不到万不得已,打log都比debug快得多。
在debug过程中,笔者发现,第2次进入f()以后,或者说第2次进入d_f2()后,仍然把d_f1()执行了,但是再接下去执行"m_value = 100;"的时候,程序就崩溃了。于是,问题来了:
1. 为什么第一次执行的时候不崩溃,而第二次就崩溃?
2. 为什么崩溃前还能把d_f1()执行了?
其实真正去做过debug就可以看出来,在访问到m_value的时候,已经显示对m_value的访问是非法的了!这就是崩溃的原因了。至于为何崩溃前仍能把d_f1()执行了,那是因为在d_f1()这个函数内并没有访问任何D的成员变量,而对于函数指针d_f1的访问,并没有涉及到访问其他人的内存。这就是第二个问题的答案了。
可是,第一个问题还没有回答,为何第一次没有崩溃呢?
因为第一次调用f()的时候,m_value这块内存是自己的;而第二次调用f()的时候,这块内存不是自己的了。
好吧,说了等于没说。为何第一次的时候是自己的,第二次就不是自己的了呢?
其实,Root Cause是在于,在Project A中是定义了宏 __XXX__ 的,而在Project B中并没有定义宏 __XXX__. 因为B.exe链接了A.dll,所以在第一次调用x.f()的时候,进入了A.dll的代码空间中,这里是存在宏 __XXX__,因此,对于 m_var 这个类 C 的实例,此时是认为其内存布局中是存在 D* p 的,于是便调用了 p->d_f2()。 此时因为是程序的早期,在d_f2()中对于 m_value 的访问并没有引发什么问题。(这里,笔者仍然是有疑问的:此时的d_f2()里的m_value所占用的内存空间到底是不是m_var的?似乎不是,但也没出错。)
但是,因为x这个实例是在栈上的,所以Project B中的 m_var 这个实例并不知道 __XXX__,所以 m_var 的内存空间中并没有 D* p. 而在第一次 m_var.f() 调用完成后,"int a = 100;" 等后续语句就会继续使用栈空间,即 int a = 100; 及其后续语句就会把原先存放 D* p 的栈空间占用掉,所以等到第二次执行x.f()时,即进入A.dll的代码空间时,会去访问不是 x 实例自己空间以试图访问 m_value,但因为那其实是其他实例(比如 int a)的空间,所以导致了程序崩溃。
修复的方法不言自明了, 只要在Project B中也加入宏 __XXX__ 即可。
(完)