主要内容
最近偶遇一段代码两个奇妙的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; // 某树算法
template <class T> class MyTree { // 对 IntervalTree 封装
private:
bool _isIndexed;
IntervalTree<uint32_t, T *> *_tree;
std::set<T *> data;
void buildIndex() { // 根据 data 构建树
if (_isIndexed)
return;
if (_tree != nullptr)
delete _tree;
tree = new IntervalTree<uint32_t, T *>(...);
_isIndexed = true;
}
public:
MyTree() : _isIndexed(false), _tree(nullptr) {}
~MyTree () { delete _tree; }
MyTree<T> &operator=(const MyTree<T> &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<Range> &x) {
MyTree<Range> y = x; // 触发默认构造函数
...
}
void foo2(MyTree<Range> y) { // 触发默认构造函数
...
}
int main() {
MyTree<Range> x;
MyTree<Range> 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);
会让 =
存在差异性,因此二者代码逻辑最好完全一致,以避免出现混淆。