善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。

2011-10-04

初始化,真的很重要

刚花了大半天的时间解决掉一个 Bug,再次证明了变量初始化的重要性。照例,还是先来看一小段代码:

map<string, TEOBJ> telist; // 全局对象
……
TEOBJ spte(3);
telist["debug"] = spte;

很简单吧?按道理这点代码应该没有什么问题。不过在最后一句中却抛出了异常,发生了崩溃。负责这部分代码的人昨天就请了假回家了,节后还有婚假,一走要近二十天,只好自己动手找原因。

这里的 TEOBJ,是一个自己定义的类。在我的这个案例中,它是一个底层库里面定义的数据对象类,用在了很多地方。基于这个前提,我首先判断这个 Bug 的故障点在赋值号的左边。

可是左边也是个 std::map 的相当标准的用法,STL 出错的可能性恐怕比我们自己的底层库更低,而且这个用法我以前也用过很多,没有出过什么问题。怀疑的眼光于是又回到了赋值号的右边。

那么到底是哪一边呢?我也拿不准了,于是排除法上马。通过几轮替换,我终于把问题基本定性为「由 TEOBJ 引起,但需要在赋值到 map 的 [] 运算符时产生」。

既然说到赋值,那么就该看看 TEOBJ 的相关声明了:

class TEOBJ {
public:
    int GroupID;
private:
    int GroupCode;
public:
    TEOBJ::TEOBJ() {};
    TEOBJ::TEOBJ(int nGroupID) {
        GroupID = nGroupID;
        GroupCode = TDataStore::Groups[GroupID]->Code;
    }
    TEOBJ::TEOBJ(const TEOBJ& r) {
        GroupID = r.GroupID;
        GroupCode = TDataStore::Groups[GroupID]->Code;
    }
    const TEOBJ& TEOBJ::operator=(const TEOBJ& r) {
        GroupID = r.GroupID;
        GroupCode = TDataStore::Groups[GroupID]->Code;
    }
};

这个 TEOBJ 重载了拷贝构造函数和赋值运算符,在其中通过查询一个数据字典确保 GroupCode 有准确的取值。

首先,赋值运算符重载这里的返回值有点小问题。const 引用类型的返回值与习惯上的 non-const 引用类型的返回值有所区别。不过这个最多造成编译时语法检查上的问题,不至于引起指针错误。

其次,就是这个 TDataStore::Groups[GroupID] 的数组的使用值得怀疑了。

实际案例比这个情况要复杂一些,而且我对于 BCB 的使用还不算熟悉,调试基本靠 OutputDebugString。问题就诡异在,往 TEOBJ::operator=() 里面只要任意加一行 OutputDebugString,故障就消失了。搞得好像是野指针一样。

我在此浪费了很多时间,走了不少弯路。后来,一咬牙用上了 windbg,配合 map2dbg,好歹是得到了 CallStack。但是 map2dbg 之后得到的符号名称又和我以前看到的有些不同,于是又错判了位置。

最后,我终于在 windbg + 排除法的协助下找到了问题。问题根本不在赋值运算符重载函数中。尽管这里代码中明显是用到了它,但它并没有出问题。抛出异常的是拷贝构造函数。这个就要从 map::operator[] 的实现说起了。

Borland C++ Builder 5 里面的 std::map,其实现代码在 map.h 里面,是一个 inline 函数。挖出来一看,倒也很简单:

mapped_type& operator[] (const key_type& k) {
    value_type tmp(k,T());
    return (*((insert(tmp)).first)).second;
}

可以看到,这里先生成了一个临时变量(其实本例中就是 pair<string, TEOBJ>)。在生成这个临时变量的时候,采用了无参数的 TEOBJ 构造函数。此时 TEOBJ::GroupID 还是没有初始化的,在 Release 版本中它可能是任意值(TEOBJ 所在的底层库就是 Release 编译)。

实际使用时,TEOBJ 都通过 TEOBJ::TEOBJ(int nGroupID) 来初始化,因此不会产生这种未被正确初始化的实例。但在 map::operator[] 中,由于是「先插入空值,再传出引用用于赋值」,这个时候就使得拷贝构造函数被调用,于是产生了类似的访问野指针的效果,导致故障的出现。

通过在拷贝构造函数中用 OutputDebugString 输出 GroupID 的取值,确认了这一点。

至此,真相大白。总结经验教训如下:

  1. 变量的初始化很重要,一个也不能放松,不能因为眼下不初始化也不会出问题,就放松警惕。做底层库的人尤其应该重视——你的代码不只是你一个人在用!
  2. 数组的下标,或者说指针的偏移量,这种变量与指针基本上是同样的性质。要提防野指针,它们也要算上。
  3. 调试手段无所谓牛刀不牛刀,好用就用,不要因为感觉问题小、简单,就先用笨办法尝试,直到一筹莫展了之后才考虑别的办法。如果我一开始就用 windbg,时间上可能会节省更多一些。

没有评论:

发表评论