文章目录
拷贝构造函数和赋值运算符函数都是类中的特殊函数。什么时候执行拷贝构造函数/赋值运算符函数?拷贝初始化和直接初始化有什么区别?
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。 如果第一个参数不是引用类型,为了调用拷贝构造函数,必须拷贝它的实参,但为了拷贝它的实参,有需要调用拷贝构造函数,如此无限循环。
拷贝赋值运算符
拷贝赋值运算符接受一个与其所在类相同类型的参数。赋值运算符通常应该返回一个指向其左侧运算对象的引用(主要是为了链式表达式)。
class Test {
public:
Test() = default; // 默认构造函数
Test(const Test&) = default; // 拷贝构造函数
Test& operator=(const Test&) = default; // 赋值运算符
};
什么时候会执行拷贝构造函数
- 拷贝初始化,用=来定义变量
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
拷贝构造函数和拷贝赋值运算符的区别
上面也提到了可以用=来定义变量完成拷贝初始化,那么=啥时候是赋值呢?
int main() {
Test t;
Test t1 = t; // 拷贝初始化
t1 = Test(); // 拷贝赋值运算符
}
也就是对象不存在时,用=来创建就是拷贝初始化;若左侧对象已经存在,则=就是赋值运算符。
拷贝初始化和直接初始化
class Test {
public:
Test() = default; // 默认构造函数
Test(int x) {}
Test(const Test&) = default;// 拷贝构造函数};
int main() {
Test t;
Test t1(t); // 直接初始化
Test t2 = t; // 拷贝初始化
Test t3 = 5; // 拷贝初始化
}
当使用直接初始化时,实际上要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,要求编译器将右侧运算对象拷贝正在创建的对象中,如果需要的话还要进行类型转换,所以一般拷贝构造函数不声明为explicit。
当类中有引用和const成员时,赋值运算符如何操作
因为const数据成员和引用是初始化后就不能更改绑定的,所以赋值运算符会忽略这两个请求。
浅拷贝和深拷贝
如果类中有指针时,需要显式拷贝构造/赋值运算符函数,否则单单只拷贝了指针,会引发重复释放的问题。
class Test {
public:
Test();
Test(const Test&) = default;// 拷贝构造函数};
~Test();
private:
int *p;
};
Test::Test() : p(new int[5]) {}
Test::~Test() {
delete[] p;
p = nullptr;
}
int main() {
Test t;
Test t1 = t;
}
// 这个时候会重复释放p所指向的内存
如何编写一个异常安全的拷贝赋值运算符函数
拷贝赋值运算符函数有两个要注意的点,一个是自我赋值,另一个是异常安全。所幸的是在解决异常安全的同时也可以顺便解决了自我赋值的问题。
class Test {
public:
Test(int x);
Test& operator=(const Test&);
~Test();
private:
int *p;
};
Test::Test(int x) : p(new int[x]()) {}
Test::~Test() {
delete[] p;
p = nullptr;
}
Test& Test::operator=(const Test& rhs) {
delete p;
p = new int(*rhs.p);
return *this;
}
int main() {
Test t1(5);
Test t2(6);
t1 = t1;
}
这里的自我赋值的问题是,operator=函数内的*this(赋值的目的端)和rhs有可能是同一个对象。如果这样,delete就不只是销毁当前对象的p,它也销毁rhs的p。这样就会使p指向与i个被删除的对象。
欲阻止这种错误,传统做法是这样的:
Test& Test::operator=(const Test& rhs) {
if (this == &rhs) return *this;
delete p;
p = new int(*rhs.p);
return *this;
}
这样做之后,自我赋值没有问题了。但是异常安全还没解决。如果new int导致异常(可能分配时内存不足),则p会指向一块被删除的内存,这样的指针式有害的。可以这样解决:
Test& Test::operator=(const Test& rhs) {
int* tmp = p; // 记住原先的p
p = new int(*rhs.p); // 令p指向*p的一个副本
delete tmp; // 删除原先的p
return *this;
}
这样做之后,自我赋值和异常安全的问题都解决了。由于自我赋值判断的问题发生频率比较低,所以加上去影响效率。