14. 拷贝控制:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、Rule of Five
拷贝控制,说白了就是C++管理对象生命周期的核心机制。我刚开始学的时候也觉得这东西有点绕,但后来在项目中吃过亏,才真正体会到它的重要性。
你想想看,一个对象怎么被复制?怎么被赋值?怎么被移动?这些操作背后都有对应的特殊成员函数在默默工作。今天我们就来彻底搞明白它们。
14.1 拷贝构造函数
拷贝构造函数,就是用一个已存在的对象去初始化另一个新对象。它的签名长这样:
class Widget {
public:
Widget(const Widget& other); // 拷贝构造函数
};
什么时候会调用它?最常见的场景有三个:
- 用一个对象初始化另一个对象:
Widget w2 = w1; - 函数传参(按值传递):
void func(Widget w); - 函数返回对象(按值返回):
Widget createWidget();
我在项目中遇到过一个问题:有个同事写了一个类,里面有个指针成员,但没有定义拷贝构造函数。结果两个对象共享了同一块内存,一个析构了,另一个就变成了悬空指针。嗯,这就是经典的浅拷贝问题。
核心要点:如果类中有指针成员、动态分配的资源,一定要自己实现拷贝构造函数,做深拷贝。
14.2 拷贝赋值运算符
拷贝赋值运算符和拷贝构造函数很像,但它是给一个已经存在的对象赋值的:
class Widget {
public:
Widget& operator=(const Widget& other); // 拷贝赋值运算符
};
它和拷贝构造函数的区别在于:拷贝构造是创建新对象,拷贝赋值是修改已有对象。
写拷贝赋值运算符时,有个经典陷阱——自赋值。你想想看,如果写 w = w; 会发生什么?
Widget& Widget::operator=(const Widget& other) {
if (this == &other) return *this; // 自赋值检查
// 释放当前资源
// 拷贝 other 的资源
return *this;
}
我曾经见过一个线上bug,就是因为没做自赋值检查,导致对象把自己释放了,然后又去拷贝已经释放的资源。那场面,啧啧,崩溃得那叫一个干脆。
警告:拷贝赋值运算符一定要处理自赋值!这是基本功,但很多人会忘。
14.3 移动构造函数
C++11引入了移动语义,说白了就是把一个对象的资源「偷」过来,而不是复制一份。移动构造函数的签名:
class Widget {
public:
Widget(Widget&& other) noexcept; // 移动构造函数
};
注意参数是右值引用(&&)。它和拷贝构造的区别在于:移动构造会把 other 的资源指针直接拿过来,然后把 other 置为空。这样避免了深拷贝的开销。
什么时候会触发移动?最常见的是:
- 返回局部对象(编译器会优先尝试移动)
- 用
std::move()显式转换 - 临时对象作为参数
我个人习惯在移动构造函数后面加 noexcept。为什么?因为标准库容器在重新分配内存时,如果移动构造是 noexcept 的,它会优先用移动而不是拷贝,性能能提升不少。
14.4 移动赋值运算符
移动赋值运算符和移动构造函数对应,它是给已有对象赋值的移动版本:
class Widget {
public:
Widget& operator=(Widget&& other) noexcept; // 移动赋值运算符
};
实现思路和拷贝赋值类似,但不需要深拷贝,只需要交换指针:
Widget& Widget::operator=(Widget&& other) noexcept {
if (this == &other) return *this;
delete[] data; // 释放当前资源
data = other.data; // 偷走对方的资源
other.data = nullptr; // 把对方置空
return *this;
}
这里也要做自赋值检查。虽然自移动赋值很少见,但安全第一嘛。
14.5 Rule of Five
Rule of Five 是C++11之后的一个指导原则。它说:如果你需要自定义下面五个函数中的任何一个,那么你很可能需要自定义全部五个:
| 函数 | 作用 |
|---|---|
| 析构函数 | 释放资源 |
| 拷贝构造函数 | 深拷贝创建新对象 |
| 拷贝赋值运算符 | 深拷贝赋值给已有对象 |
| 移动构造函数 | 转移资源创建新对象 |
| 移动赋值运算符 | 转移资源赋值给已有对象 |
为什么是五个?因为如果你手动管理资源(比如 new/delete),编译器生成的默认版本大概率是错的。浅拷贝、资源泄漏、双重释放,这些坑我都踩过。
小技巧:如果类不需要自定义析构函数,那大概率也不需要自定义拷贝和移动操作。遵循「零规则」(Rule of Zero),尽量用智能指针和标准库容器来管理资源。
我曾经接手过一个老项目,里面有个类只定义了拷贝构造函数,没定义拷贝赋值运算符。结果赋值操作走了默认的浅拷贝,两个对象析构时对同一块内存调了两次 delete,程序直接崩了。从那以后,我每次写资源管理类都会默念一遍:析构、拷贝构造、拷贝赋值、移动构造、移动赋值,一个都不能少。
14.6 知识体系图
下面这张图帮你理清拷贝控制的整体脉络:
这张图把五个特殊成员函数的关系和 Rule of Five 的核心逻辑都展示出来了。你写代码的时候,可以把它当作一个检查清单。
最后说一句:拷贝控制是C++里最容易出bug的地方之一,但也是最能体现C++设计哲学的地方。理解了它,你就真正理解了C++的资源管理。