前言
我们在类中重载赋值运算符时会出现自我赋值和异常安全的问题,下面就来逐步解决这两个问题。
自我赋值
引例
自我赋值发生在对象被赋值给自己时:
class Person { ... };
Person p;
...
p = p;
这看起来有点蠢,但毫无疑问它是合法的,我们也不要认定这种事情决不会发生。
此外,赋值动作并不总是那么可被一眼辨识出来,例如:
arr[i] = arr[j] // 潜在的自我赋值
*pa = *pj // 指针指向引起的自我赋值
int p = 0; // 引用引起的自我赋值
int& rp = p;
...
p = rp;
如果我们写出下面这样的代码:
class Teacher { ... };
class Student {
...
private:
Teacher* t;
};
Student& Student::operator=(const Student& rt) {
delete t;
t = new Teacher(*(rt.t));
return *this;
}
这里的自我赋值问题是,opreator=
函数的 *this(赋值目的端)和 rt 有可能是同一个对象。如果这样 delete 就不只是销毁当前对象的 Teacher,它也销毁 rt 的 Teacher。在函数最后,Student 本不会因为自我赋值而改变,现在却持有一个指针指向一个已被删除的对象。
解决方法
传统的解决方法就是在函数最前面加一个检测达到自我赋值的检验目的:
Student& Student::operator=(const Student& rt) {
if (this->t != rt.t) {
delete t;
t = new Teacher(*(rt.t));
}
return *this;
}
这样做确实具有自我赋值安全性,但并不具备异常安全性。
异常安全
上述的代码存在异常方面的麻烦,更明确的说,如果 new Teacher
导致异常(分配时内存不足或因为 Teacher 的拷贝构造函数抛出异常),Student 最终都会持有一个指针指向一块被删除的 Teacher。你无法安全地删除它们,甚至无法安全地读取它们。
解决方法
我们可以采取先申请空间,再删除原有空间的做法。这样,即使申请空间时抛出异常,原有指针也不会受到影响。
Student& Student::operator=(const Student& rt) {
// 执行检测可以清晰地显示我们考虑了自我赋值这一情况
if (this->t != rt.t) {
Teacher* tmp = t; // 记住原先的 t
t = new Teacher(*(rt.t)); // 令 t 指向新空间
delete tmp; // 删除原先的 t
}
return *this;
}
// 这个版本可以去掉函数最前面的检测,即使没有检测也可以完成自我赋值
// 理论上因为检测本身就需要成本且发生频率并不高
// 并且减少了一个新的控制流分支,执行速度会提高
Student& Student::operator=(const Student& rt) {
Teacher* tmp = t; // 记住原先的 t
t = new Teacher(*(rt.t)); // 令 t 指向新空间
delete tmp; // 删除原先的 t
return *this;
}
copy and swap技术
我们也可以利用拷贝构造来完成这一函数:
class Student {
// 交换 this 和 rs 的数据
void swap(Student& rs) {
// 交换两个 Student object 的 Teacher object 的指针指向
std::swap(this, rs);
}
};
Student& Student::operator=(const Student& rs) {
Student tmp(rs); // 为 rs 数据制作一份拷贝
swap(tmp); // 将 *this 数据和 tmp 的数据交换
// tmp 会在出了 operator= 作用域后自动销毁
return *this;
}
// 若赋值重载采用的是传值传参
Student& Student::operator=(const Student rs) {
swap(rs); // 将 *this 数据和 rs 的数据交换
return *this;
}