0%

拷贝构造函数 赋值运算符和强保证异常安全代码

1
2
3
4
5
6
7
8
9
10
11
12
class Test{
public:
Test(){cout << "explicit constructor with no argument" << endl;}
//Test(Test t){cout << "copy constructor" <<endl;} //1
Test(const Test& t){cout <<"copy constructor" << endl;} //2
Test& operator=(const Test& t){
cout << "operator=(const Test& t)" <<endl;
} //3
Test& operator=(Test t){
cout << "operator=(Test t)" <<endl;
}//4
}

为什么拷贝构造函数不能写成1的形式,必须写成2 的形式?

之前的回答是 减少一次拷贝,因为传值的时候会调用拷贝构造函数,传引用可以避免掉,至于传const引用,主要是避免实参被修改。
但现在深入的再思考就会发现,传值的时候调用拷贝构造函数,但实际上这是一个无限套娃,调用拷贝构造函数的时候,传入参数也是传值,这又要调用拷贝构造函数。
循环往复,没有尽头了。

至于3和4的区别在哪里呢?
3和4是两种写法。

用写法3的时候,就是operator=的实现就是拷贝构造函数的另一种实现,唯一的区别是要多一步检查(检查= 两边的对象是否相同,如果相同的话不做任何操作,否则,就要释放掉当前的内存,申请新内存,并拷贝内容过来)。

写法4的时候,就是我的申请内存的操作都是放在拷贝构造函数里的,这样我们的operator=的实现就很简单了,因为构造的临时变量已经分配了内存,那么代码中只需要进行swap就好了。 而且在swap的时候,我们交换的是临时变量和this的内容,离开当前堆栈销毁临时变量的时候,释放的也是我们原先this指向的this的内容,还减少了我们释放旧内存的操作。

4相比于3的写法,个人认为最大的好处是确保了 the strong guarantee。因为operator=如果不调用拷贝构造函数的话,也要自己在内部申请内存,拷贝内容,而且内部申请内存 抛出异常的话,还要确保代码满足the strong gurantee。而在拷贝构造函数中申请内存的话,其实也要处理异常,不过这就保证operator=是nothrow的。
既然拷贝构造函数本身就有异常处理,那我们何苦在operator=中也要再加上抛出exception的风险呢。 而且swap本身也是nothrow的。

至于3和4的返回值都是 引用类型。如果我们返回的是普通值类型,那么返回值这里要调用拷贝构造函数,这里就会造成内存泄漏,就拿4来说,我们本身就调用拷贝构造生成了一个临时变量(虽然是临时变量,但内存不会被销毁)

下面是我们的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class TestArray{
public:
TestArray(): len_(0), data_(nullptr){
cout << "TestArray()"<<endl;
}
TestArray(const TestArray& ta):len_(ta.len_), data_(len_? new char[len_] : nullptr){
std::copy(ta.data_, ta.data_ + len_, data_);
}

TestArray& operator=(TestArray other){
swap(*this, other);
return *this;
}

~TestArray(){
cout << "destructor" << endl;
}
private:
size_t len_;
char* data_;

void swap(TestArray& one, TestArray& another)noexcept{
using std::swap;
swap(one.len_, another.len_);
swap(one.data_, another.data_);
}


};
int main(int argc, char* argv[])
{
TestArray a; //1
TestArray b(a); //2
TestArray c = a;//3
TestArray d;//4
d = a;//5
return 0;//6
}

1 的输出 应该是 TestArray这没有疑问
2 的输出是 Copy Constructor 也没问题
3 的输出,照着我最初的理解,是TestArray c; c = a; 那么输出应该是TestArray Copy Constructor 26 destructor,但最终的输出是 Copy Constructor。
这是什么原因呢? 在编译器看来,不会做这么麻烦的操作,因为c没有被构造,所以这里TestArray c = a 是初始化而不是赋值,可以被看作TestArray c(a)。这样理解的话,就能理解为什么3的输出只有一个copy constructor了。
4 的输出 是TestArray
5 =左边d是已经被初始化的值,所以,此时才适用于 赋值运算符,此时在调用=的时候,传值会导致调用copy constructor。所以输出是copy constructor 26。此时,swap之后,临时变量要被释放,此时会调用destructor。

接着在离开main函数的时候,依次调用d c b a的析构函数。

那么最终的输出是

1
2
3
4
5
6
7
8
9
10
11
TestArray
Copy Constructor
Copy Constructor
TestArray
Copy Constructor
26
destructor
destructor
destructor
destructor
destructor