缘起
笔者在学习模板类的时候,希望实现一个类似于STL的Array class
。在其中需要一个深拷贝构造函数和一个重载赋值运算符。于是我写了如下代码:
template<class Elem>
class Array
{
private:
Elem *_arr = nullptr;
size_t _capacity = 0;//数组容量
public:
typedef unsigned int size_t;
Array(size_t capacity=0 ):_capacity(capacity)
{
_arr = new Elem[_capacity];
cout << "Flag1" << endl;//标记构造函数1
}
Array(const Array<Elem>& arr)
{
(*this)=arr;//偷懒的做法
cout << "FLag2" << endl;//标记拷贝构造函数
}
~Array()
{
if(_arr)
delete[] _arr;
_arr = nullptr;
_capacity = 0;
cout << "Flag3" << endl;//标记析构函数
}
size_t size() const { return _capacity; }
//------------------重点在下面-------------------
Array<Elem> operator=(const Array<Elem>& arr)
{
this->~Array();
this->_capacity = arr._capacity;
this->_arr = new Elem[_capacity];
for (size_t i = 0; i < _capacity;++i)
this->_arr[i] = arr._arr[i];
return *this;
}
//----------------------------------------------
Elem &operator[](size_t ix) { return this->_arr[ix]; }
const Elem &operator[](size_t ix) const { return this->_arr[ix]; }
const Elem* begin() const { return _arr; }//为了支持基于范围的for循环
const Elem* end() const { return _arr + 20; }//
接下来我在主函数内调试代码:
int main()
{
Array<int> a;
Array<int> b(3);
a = b;
return 0;
}
结果控制台就刷了很多Flag3
标记然后崩溃了。这说明析构函数在不断被调用。
调试历程
这个bug非常诡异,因为整个代码并没有任何明显的循环或者递归存在。
启动逐语句调试,发现一旦运行到了最后的return *this;
语句就会莫名其妙开始执行拷贝构造函数,但拷贝构造函数内的(*this)=arr;
又会让他跳回operator=
函数内,从而形成了循环,不断执行析构函数。
事实上它仍旧形成了一个递归,不过这个递归是两个函数互相调用形成的。
不过为什么会在函数末尾执行拷贝构造函数呢?
我先修改了拷贝构造函数的代码,破坏了bug形成的递归结构:
Array(const Array<Elem>& arr)
{
this->~Array();
this->_capacity = arr._capacity;
this->_arr = new Elem[_capacity];
for (size_t ix = 0; ix < _capacity; ++ix)
this->_arr[ix] = arr._arr[ix];
cout << "FLag2" << endl;
}
再来运行一下:
可以看出前两个Flag1
和最后两个Flag3
都是对象a,b的正常构造-析构过程。而紧接着Flag1
后面的第一个Flag3
是我们在operator=
中显式调用的。而Flag2
代表的调用拷贝构造函数内又有调用析构函数。排除我们知道的,这中间还存在一个诡异的析构-拷贝过程。
猜想和论证
究竟是谁调用了拷贝构造函数?要搞清楚这个。可以改进一下标记:
cout <<_capacity << "FLag2" << endl;
运行:
现在可以作出猜想了,是代表返回值的那个对象作出拷贝。
如果修改返回对象为引用呢?
一切正常了,这说明返回值本身是一个对象实例,和形参一样。创建这个对象调用了拷贝构造函数。