最近偶遇一段代码两个奇妙的bug,简单记录一下,
一、等号的差别即看似相同的赋值语句 = 号却可能有着完全不同的涵义。
class T {…};
T a = b;
与
T a;
a = b;
之间有着重要的差别。前者会调用构造函数 T(const T &b);,后者会调用重载操作符T &operator=(T &b) const;,另外作为参数传递的时候也有可能会触发构造函数。
void foo(T x); // 触发构造函数
void goo(T &x); // 不触发构造函数
因此,在重载赋值操作时,务必要记得等价地重载构造函数!!!!
二、delete 与浅拷贝在对指针 delete 时可能会触发意外的 bug。
class T { P * ptr; };
T a = b;
delete a.ptr;
在触发浅拷贝时,指针指向也会一并被拷贝,因此释放 a.ptr 等同于释放 b.ptr
代码展示以下为遇见的bug具体代码,以说明问题可能有多么严重。
class IntervalTree; // 某树算法 templateclass MyTree { // 对 IntervalTree 封装 private: bool _isIndexed; IntervalTree *_tree; std::set data; void buildIndex() { // 根据 data 构建树 if (_isIndexed) return; if (_tree != nullptr) delete _tree; tree = new IntervalTree (...); _isIndexed = true; } public: MyTree() : _isIndexed(false), _tree(nullptr) {} ~MyTree () { delete _tree; } MyTree &operator=(const MyTree &other) { this->_isIndexed = false; this->_tree = nullptr; this->data = other.data; return *this; } void insert(T *var) { data.insert(var); _isIndexed = false; } bool find(uint32_t s, uint32_t e) { if (!_isIndexed) buildIndex(); if (_tree == nullptr) return false; return !_tree->findOverlapping(s, e).empty(); } }
上述代码对外部类 IntervalTree 进行了简单的封装管理,以保证 data 的更新与树更新的同步关系,看上去就非常安全。。。。。
然而当我做如下操作的时候,bug悄然出现:
class Range; void foo1(MyTree&x) { MyTree y = x; // 触发默认构造函数 ... } void foo2(MyTree y) { // 触发默认构造函数 ... } int main() { MyTree x; MyTree z; ... foo1(x); // BUG 触发 (foo2同理) ... return 0; }
这里触发了严重的 BUG,并且不会报错
foo1 中看似是赋值语句的写法,实际调用的却是默认构造函数,浅拷贝每个域,从而导致 y._tree 与 x._tree 指向相同。另外换做 foo2 写法,作为函数参数传递时,同样调用一次默认构造函数,同样导致 y._tree 与 x._tree 指向相同。
当退出函数时,会触发 y 的析构函数释放 y._tree,导致 x._tree 被释放。同样,当重建 y 树时也会释放 y._tree,导致 x._tree 被释放。
在某种机缘巧合下,过了很久该代码都没有出现明显的错误!!!!
BUG 发现很长一段时间内该代码都没有表现出任何异常,并持续运行着,直到某一天我发现对另一个不相干的树 z 的查询结果检查与预期不符。。。。。
原来,x._tree 被释放后,意外指向了另一个树 z._tree 的某子树上,导致程序 “貌似正确地” 被执行了下去。
在接下来的某一步,代码对 x._tree 进行修改并重建,间接导致了 z._tree 的子树被修改。从而改变了 z._tree 的查询结果。 (不知为何这里也没有任何报错信息???)
再接着某一天,我对 z._tree 上的另一个算法进行深入追踪时,意外注意到某些异常值。。。。
小结1、delete 某个域时需要慎之又慎,需要注意到浅拷贝时,代码的正确性,最好每个类都写上构造函数 T(const T &b);
2、T &operator=(T &b) const; 与 T(const T &b); 会让 = 存在差异性,因此二者代码逻辑最好完全一致,以避免出现混淆。



