拷贝控制
掌握C++三/五法则——拷贝构造/拷贝赋值/析构/移动构造/移动赋值,理解值语义与资源管理的核心机制
学习目标
- 能解释合成拷贝控制成员的行为——什么情况下编译器替你生成、生成的拷贝是浅拷贝还是深拷贝,以及为什么管理堆资源的类不能依赖合成的拷贝
- 能根据类的资源管理需求,正确实现拷贝构造/拷贝赋值/析构函数——知道拷贝赋值必须先释放旧资源、处理自赋值,析构必须释放所有堆资源
- 能回答:下面
HasPtr类的合成拷贝控制成员有什么问题?写出修正后的完整五法则实现。class HasPtr { public: HasPtr(const std::string &s) : ps(new std::string(s)), i(0) {} private: std::string *ps; int i; };
机制总览
拷贝控制:机制路径
- 1
直觉:对象拷贝时,到底发生了什么?
你已经习惯写 auto b = a; 把 a 的值拷贝给 b 。对于 int 、 double 这些简单类型,拷贝就是复制那几个字节——干净利落。但现在你开始设计自己的类了——里面可能有指向堆内存的指针、有文件句柄、有网络连接——这时候 auto b = a; 到底该干什么?
- 2
官方 Chapter 13 的生成规则与值语义契约
五个特殊成员不是彼此独立的开关。用户声明其中一些成员会抑制另一些成员的隐式生成,成员或基类不可执行某操作时,对应合成函数还可能被定义为 deleted。设计类时应先决定它表现得像值、像共享句柄,还是不可复制资源,再决定 = default 、 = delete 或自定义实现。
- 3
合成的拷贝:编译器替你做了什么
如果你不为类定义拷贝构造函数和拷贝赋值运算符,编译器可能隐式声明并合成它们,也可能因成员或其他特殊成员规则将其定义为 deleted。
章级决策实验
拷贝控制:机制与证据
切换《拷贝控制》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 直觉:对象拷贝时,到底发生了什么?
你已经习惯写 auto b = a; 把 a 的值拷贝给 b 。对于 int 、 double 这些简单类型,拷贝就是复制那几个字节——干净利落。但现在你开始设计自己的类了——里面可能有指向堆内存的指针、有文件句柄、有网络连接——这时候 auto b = a; 到底该干什么?
可核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:对象拷贝时,到底发生了什么?」的契约。
学完《拷贝控制》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
拷贝控制:失效与核验
直觉:对象拷贝时,到底发生了什么?
典型失效
若把「直觉:对象拷贝时,到底发生了什么?」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:对象拷贝时,到底发生了什么?」的契约。
官方 Chapter 13 的生成规则与值语义契约
典型失效
若把「官方 Chapter 13 的生成规则与值语义契约」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「官方 Chapter 13 的生成规则与值语义契约」的契约。
合成的拷贝:编译器替你做了什么
典型失效
若把「合成的拷贝:编译器替你做了什么」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「合成的拷贝:编译器替你做了什么」的契约。
直觉:对象拷贝时,到底发生了什么?
你已经习惯写 auto b = a; 把 a 的值拷贝给 b。对于 int、double 这些简单类型,拷贝就是复制那几个字节——干净利落。但现在你开始设计自己的类了——里面可能有指向堆内存的指针、有文件句柄、有网络连接——这时候 auto b = a; 到底该干什么?
想象工厂流水线上有个零件盒——里面装着螺丝、垫片、还有一张"去仓库第3排货架取专用夹具"的取货单。现在你要复制这个盒子——你是只复印取货单(两个人拿着同一张单子去同一个货架打架),还是真的去仓库拿一副新夹具放进新盒子?前者就是默认的「浅拷贝」——只复制指针本身,不复制指针指向的数据。
这一章解决什么问题? 教你怎么定制五个"特殊函数"——对象被拷贝时、被赋值时、被移动时、被销毁时分别该干什么。没有它们,管理堆资源的类一拷就崩——两个对象抢同一块内存,析构时删两次直接炸掉。
官方 Chapter 13 的生成规则与值语义契约
五个特殊成员不是彼此独立的开关。用户声明其中一些成员会抑制另一些成员的隐式生成,成员或基类不可执行某操作时,对应合成函数还可能被定义为 deleted。设计类时应先决定它表现得像值、像共享句柄,还是不可复制资源,再决定 = default、= delete 或自定义实现。
拷贝构造不仅发生在 T b = a:按值传参、按值返回、容器插入与某些初始化也可能调用它,虽然编译器可执行拷贝消除。析构函数不直接销毁成员;它的函数体先运行,随后成员按声明逆序自动析构。合成析构并非“空操作”,它仍会正确析构 string、vector 和智能指针成员。
C++11 的关键生成关系包括:声明移动构造或移动赋值会使合成拷贝操作被定义为 deleted;只有当类没有用户声明的拷贝构造、拷贝赋值、移动操作和析构函数时,编译器才会隐式声明移动操作。用户声明析构函数常会让本可移动的类退回拷贝路径,因此资源类应显式审查全部五项。
零法则优先于五法则:若资源能由 string、vector、unique_ptr 或专用 RAII 句柄成员管理,外层类通常无需声明任何拷贝控制成员。只有当类本身承担资源政策、注册关系或特殊值语义时,才自定义这些入口。
合成的拷贝:编译器替你做了什么
如果你不为类定义拷贝构造函数和拷贝赋值运算符,编译器可能隐式声明并合成它们,也可能因成员或其他特殊成员规则将其定义为 deleted。↡编译器按基类和非静态成员逐个执行对应拷贝操作而生成的特殊成员。标量成员复制其值,类成员调用自身拷贝操作;若某个子操作不可用,合成函数可能被删除。按成员语义工作:
- 类类型成员(如
string、vector)调用其自身的拷贝构造函数——string的拷贝会正确分配新内存并复制字符串内容,所以这一部分是深拷贝 - 内置类型成员(如
int、double)直接复制其值——纯值拷贝,没有问题 - 指针成员(如
int*)只复制指针的地址值——不复制指针指向的堆数据
↡只复制指针本身的值,不复制指针指向的堆内存内容。结果:两个对象里的指针指向同一块堆内存——修改一个影响另一个,析构时双重释放崩溃。合成拷贝对裸指针成员所做的就是浅拷贝。只对指针本身的地址做拷贝——比如 ps 值是 0x1000,拷贝后就得到另一个也指向 0x1000 的指针。两个人看着同一张取货单去同一个货架搬东西——你不炸谁炸。
看上面对比图。左半边展示的就是合成的拷贝——两个 HasPtr 对象的 ps 都指向堆上同一个 string。你改了一个的 *ps,另一个的也变了。更致命的是析构:两个对象离开作用域时都对自己的 ps 执行 delete——同一块内存被释放两次,这叫 ↡对同一块已释放的堆内存再次执行 delete 导致的严重错误——属于未定义行为(undefined behavior),程序可能立即崩溃、静默破坏数据、或在一段随机时间后崩溃。是浅拷贝最典型的后果。,程序直接炸。
看右侧那张图——正确的做法是为新对象单独分配一块堆内存,把源对象 *ps 的内容复制进去。两个对象各管各的堆内存,改一个不影响另一个,析构时各删各的——这叫深拷贝。
深拷贝的正确姿势:拷贝构造 + 拷贝赋值
要解决上面左图的问题,你需要自己写拷贝构造函数和拷贝赋值运算符。它们是五个特殊成员函数中的前两个。
↡用同类型对象初始化新对象的构造函数。第一参数必须是自身类型的引用,通常写成 const 引用以接受 const 对象和临时量;其他参数若存在必须有默认实参。通常接收 const ClassName&——用已有对象初始化一个刚诞生的新对象:
HasPtr(const HasPtr &orig)
: ps(new std::string(*orig.ps)), i(orig.i) {}
// ↑ 先 new 分配新内存,再拷贝 *orig.ps 内容关键点:构造时 this 对象刚分配还没内容——只需要分配+拷贝,不需要释放任何东西(本来就没有)。
↡把一个已有对象的值赋给另一个已有对象的运算符。实现必须兼顾自赋值和异常安全,常先取得新状态再释放旧状态,或使用 copy-and-swap;通常返回自身引用。就不同了——a = b 调用时 a 已经存在,可能正持有一块堆内存:
HasPtr& operator=(const HasPtr &rhs) {
auto *newp = new std::string(*rhs.ps); // ① 先分配新内存(防止异常时丢失旧数据)
delete ps; // ② 释放旧内存
ps = newp; // ③ 接管新内存
i = rhs.i;
return *this; // ④ 返回自身引用——支持链式赋值
}注意顺序——如果先 delete ps 再 new,万一 new 抛异常,ps 已经丢了旧数据,对象就坏了。先 new 再 delete 是异常安全的正确写法。
还有一个必须处理的边界情况:自赋值。当调用方写 a = a; 时,上面的代码仍然成立——newp 分配成功后再 delete 旧数据,没有问题。但如果有人错误地写成先 delete 再判断 this != &rhs 再 new——自赋值时你先把 ps 释放了,然后用 *rhs.ps 去拷贝……而 rhs 就是自己,*rhs.ps 在 delete 后已经是野指针了。
析构函数:对象死的时候该做什么
一个对象的生命走到尽头——离开作用域、被显式 delete、或智能指针的引用计数归零——此时必须有人把它的所有堆资源还回去。这个角色就是 ↡对象销毁前自动调用的特殊成员函数。形如 ~ClassName(),无参数无返回类型。负责释放对象持有的所有资源(堆内存、文件句柄等)。先执行函数体,再按成员声明逆序销毁各成员。:
~HasPtr() { delete ps; }
// 对象即将销毁 → 把 ps 指向的 string 还回堆 → 各成员逆序析构什么时候必须自己写析构函数?一句话:当你的类管理了需要手动释放的资源时。具体信号是类里有裸指针成员指向动态分配的内存——如果你 new 了,就必须在析构里 delete。如上面的 HasPtr——ps 指向一个 new 出来的 string,析构函数不 delete ps 就是内存泄漏。
如果成员都是 string、vector、智能指针或其他 RAII 类型,通常不需要自定义析构。编译器合成的析构会按规则自动销毁基类和成员,它不是“空析构”,只是函数体无需你编写清理代码。
析构函数体执行完毕后,成员的销毁按声明顺序的逆序进行——后声明的先销毁。而析构函数本身不能有参数、不能重载——一个类只有一个析构函数。
三/五法则:为什么牵一发而动全身
前面讲的三个函数——拷贝构造、拷贝赋值、析构——构成了 ↡C++ 的经典准则:如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,那么它几乎一定需要自定义全部三个。C++11 扩展为五法则——再加上移动构造函数和移动赋值运算符。。看上面 HasPtr 的例子:你定义了析构(因为有 delete ps)→ 就必须定义拷贝构造和拷贝赋值(避免浅拷贝导致双重释放)。
C++11 引入移动语义后,↡设计资源类时应把析构、拷贝构造、拷贝赋值、移动构造和移动赋值作为一组审查的准则。并非每项都要自定义实现,可选择 default、delete 或用 RAII 成员遵循零法则。 提醒你同时审查移动构造与移动赋值,而不是要求机械地手写五份代码。
看上图的关系——析构函数是基础:它决定了"资源应该怎么还"。拷贝控制(构造+赋值)决定了"怎么安全复制一份"。移动控制(构造+赋值)决定了"怎么高效转移所有权,不复制数据"。
实际决策口诀:
- 如果不需要任何资源管理 → 全都不写,让编译器合成(= 零法则)
- 优先把资源放进 RAII 成员,外层类继续遵循零法则
- 若类直接定义资源政策 → 审查五项,按语义实现、
= default或= delete,不要先写析构再倒推设计
移动语义:不拷贝数据,只转移所有权
拷贝要做深拷贝——分配新内存并逐字节复制。如果源对象马上就要被销毁(比如一个临时对象、函数返回的局部变量),这完全是浪费——明明直接把数据"搬过来"就行。
C++11 引入 ↡允许类型为右值或 xvalue 提供转移式构造和赋值的语言机制。具体移动可以接管资源,也可以逐成员移动;复杂度与源对象的后置状态由类型契约决定。。对这个 HasPtr,移动可以接管裸指针并把源指针置空,因此是 O(1):
// 移动构造函数:接管 rhs 的资源,把它置空
HasPtr(HasPtr &&rhs) noexcept
: ps(rhs.ps), i(rhs.i) {
rhs.ps = nullptr; // 源对象安全了——析构时 delete nullptr 是空操作
}↡写作 T&& 的引用类型,可绑定到临时量或经转换得到的 xvalue。它表达可选择移动重载的值类别,不保证对象即将销毁,也不自动执行资源转移。 T&& 是移动重载的类型基础;是否真正转移资源取决于被调用函数的实现。
↡把表达式无条件转换为对应的 xvalue,使重载决议可以选择移动操作的标准库工具。它本身不移动、不检查安全性,也不规定源对象的后续状态。 只是类型转换;真正的状态变化由随后调用的移动构造、移动赋值或其他右值重载决定。
移动赋值运算符和拷贝赋值的结构类似——先释放自己当前持有的资源,再接管源的资源:
HasPtr& operator=(HasPtr &&rhs) noexcept {
if (this != &rhs) { // 防止自赋值(虽然 a = std::move(a) 很罕见)
delete ps; // 释放当前资源
ps = rhs.ps; // 接管 rhs 的资源
i = rhs.i;
rhs.ps = nullptr; // rhs 安全了
}
return *this;
}移动后源对象处于什么状态?"有效但未指定"——你可以销毁它(析构安全),可以重新给它赋值,但不能假设它里面还有什么。上面的代码把 rhs.ps 设成了 nullptr——析构时 delete nullptr 合法且无事发生,这是最典型的做法。
noexcept 为什么重要?vector 重分配要维持异常保证,通常通过 move_if_noexcept 风格选择:移动构造可证明不抛时优先移动;若移动可能抛且类型可拷贝,可能改用拷贝;若类型不可拷贝,只能使用可能抛的移动并接受相应保证限制。只有实现确实不会抛时才能标 noexcept,不能为性能谎报。
= delete:明确说「这个操作不允许」
有些类的语义里,拷贝根本不成立。比如 unique_ptr——同一时刻只有一个所有者,拷贝就是违反它存在的意义。C++11 提供了 = delete 语法来在编译期杜绝这种行为:
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁止拷贝构造
NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};与 = default(请求编译器合成默认版本)相反,= delete 明确告诉编译器"这个函数不存在"。任何试图拷贝 NonCopyable 的代码都会在编译期直接报错——不会默默出 bug。
即使不写 = delete,某些场景下编译器也会静默删除合成的拷贝控制成员:比如类里有一个 const 成员——const 不能被赋值,所以合成的拷贝赋值被隐式 = delete。又比如类里有一个 unique_ptr 成员——unique_ptr 不能拷贝,于是整个类的合成拷贝构造和拷贝赋值都被删除。
实际应用口诀:① 需要独占资源 → 只保留移动,删除拷贝;② 需要共享资源 → 保留拷贝,移动自动生成或也手动写;③ 对象语义上不可拷贝(如数据库连接、互斥锁)→ 直接 = delete 拷贝两个。
动手:一步步看资源所有权的转移
下面用一个 Stepper 演示一个 HasPtr 对象被 std::move 后,它的 ps 指针如何从指向堆数据变成了 nullptr。每一步下方配一张 MoveSemanticsDiagram 让你直观看到内存里发生了什么。
猜一猜:执行
auto dest = std::move(source);之后,source.ps还是原来那个值吗?用 Stepper 逐步验证你的猜想。
① 移动前:源对象持有堆资源
源对象 source 正常存在——ps 指向堆上一个 string("hello"),i 值为 0。目标对象 dest 尚不存在(如果是移动构造)或已存在并持有自己的旧资源(如果是移动赋值)。此时源对象一切正常,可以正常读写。图中源指针用紫色箭号指向堆资源,说明所有权归源对象。
完整生命周期:一口看完从生到死的六个阶段
结合前面所有概念——把拷贝、移动、析构串在一起,看一个管理堆资源的对象从构造到销毁的完整旅程:
上图按时间顺序展示了六个阶段。注意几个关键转折:拷贝构造创建新堆内存(深拷贝),拷贝赋值先 delete 旧内存再创建新的(自赋值也安全),移动构造直接把指针"偷"过来(源变空),析构做最后清理(delete 所有堆资源)。
代码逐段拆解:HasPtr 五法则完整实现
以下是前面 HasPtr 的完整五法则实现——每一个特殊成员函数都手动写了,每一项设计决策都有解释。
类定义骨架 + 构造函数
#include <string>
#include <iostream>
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
public:
// 默认构造
HasPtr(const std::string &s = std::string())
: ps(new std::string(s)), i(0) {}
// 拷贝构造
HasPtr(const HasPtr &orig)
: ps(new std::string(*orig.ps)), i(orig.i) {}析构决定"怎么释放"、拷贝构造决定"怎么安全复制"——这两个是五法则的基础。构造的 ps 总是通过 new 分配新的堆字符串——无论是默认构造还是拷贝构造。i 是普通 int,直接复制即可。注意拷贝构造没有释放任何东西——this 刚分配,还没有旧资源。
拷贝赋值 + 析构
// 拷贝赋值
HasPtr& operator=(const HasPtr &rhs) {
auto *newp = new std::string(*rhs.ps); // 先分配新——防止异常丢失旧数据
delete ps; // 再释放旧
ps = newp; // 接管新
i = rhs.i;
return *this;
}
// 析构
~HasPtr() { delete ps; }拷贝赋值三步策略:先分配新 → 再释放旧 → 接管新。这是异常安全的标准做法。自赋值时 newp 是旧数据的副本,之后 delete ps 释放旧数据,不存在"删了自己又读自己"的问题。返回 *this 引用支持 a = b = c。
析构函数简单直接——delete ps。ps 可能为 nullptr(如果对象被移动过),delete nullptr 合法且什么都不做。析构完成后各成员按声明逆序析构——这里 i 是 int,析构它什么也不做。
移动构造 + 移动赋值
// 移动构造
HasPtr(HasPtr &&rhs) noexcept
: ps(rhs.ps), i(rhs.i) {
rhs.ps = nullptr;
}
// 移动赋值
HasPtr& operator=(HasPtr &&rhs) noexcept {
if (this != &rhs) {
delete ps;
ps = rhs.ps;
i = rhs.i;
rhs.ps = nullptr;
}
return *this;
}移动构造直接接管源对象的指针——ps(rhs.ps)——没有任何 new,这是 O(1) 时间操作。紧接着 rhs.ps = nullptr 把源对象置安全。标了 noexcept——vector 扩容时会优先用移动。
移动赋值先处理自赋值检测(this != &rhs 可防止 a = std::move(a) 这样的诡异操作),再 delete 当前资源 → 接管 rhs 的指针 → 把 rhs 置空。
swap 辅助函数
private:
std::string *ps;
int i;
};
// 自定义 swap——高效交换两个对象的资源
inline void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
swap(lhs.ps, rhs.ps); // 只交换指针——不拷贝 string
swap(lhs.i, rhs.i);
}自定义 swap 只交换两个对象的指针与整数,不触碰字符串内容,并可被采用 using std::swap; swap(a,b); 的泛型代码通过 ADL 找到。C++11 的通用 std::swap 通常使用一次移动构造和两次移动赋值,不是固定的深拷贝;为资源类提供专用 swap 仍能直接表达不抛、常数时间交换契约。
容易踩的坑
小结
- 合成的拷贝逐成员拷贝——指针成员只复制地址(浅拷贝),导致共享堆数据和双重释放。管理堆资源的类必须自定义深拷贝
- 五法则:自定义析构/拷贝/移动中任一个,大概率全部五个都要管——它们是一整套资源管理决策
- 拷贝赋值三步走「先分配新 → 再释放旧 → 接管新」——异常安全且自赋值也正确
- HasPtr 的移动是 O(1) 指针转移;移动复杂度与源后置状态由类型决定,真实不抛的移动标
noexcept有助于容器选择移动路径 = delete在编译期禁止不该发生的操作——比运行时崩溃好一万倍。= default请求编译器合成默认版本——用在想保留默认行为但编译器不再自动生成的场景
练习
问题 1(改代码型) 下面的 StringHolder 类管理一个动态分配的 C 风格字符串——找出拷贝控制相关的问题,写出修正后的完整五法则实现(包含移动语义)。
#include <cstring>
class StringHolder {
public:
StringHolder(const char *s) : sz(std::strlen(s)) {
data = new char[sz + 1];
std::strcpy(data, s);
}
~StringHolder() { delete[] data; }
private:
char *data;
std::size_t sz;
};问题 2(独立实现题) 设计并实现一个 ResourceManager 类:
- 它管理一个
int*动态分配的整数,初始为nullptr - 默认构造:
ptr = nullptr allocate(int value):分配new int(value)并设置给ptr(如果已分配则先释放旧的)- 完整五法则实现(拷贝/移动/析构)
int getValue() const:返回*ptr,如果ptr == nullptr抛异常std::runtime_error- 在 main 中测试:创建
rm1、给它分配值 42、用移动构造创建rm2、验证rm2.getValue() == 42且rm1的ptr为 nullptr
问题 3(问答型) Message 类包含一个 string 和一个指向 Folder 的指针。如果 Folder 不归 Message 所有(Message 不负责删它),Message 需要自定义析构函数吗?拷贝构造呢?为什么?五法则里哪些需要写?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 合成的拷贝控制成员(synthesized copy control)
编译器按基类和非静态成员逐个执行对应操作而生成的特殊成员。标量复制值,类成员调用自身拷贝;若某个子操作不可用,合成函数可能被定义为 deleted。拥有裸资源的类不能未经语义审查就依赖合成版本。
- 浅拷贝(shallow copy)
只拷贝指针本身的值(地址),不拷贝指针指向的堆数据。结果:两个对象的指针指向同一块堆内存——修改互相影响,析构导致双重释放。合成的拷贝控制成员对指针就是浅拷贝。类比:只复印了取货单上的货架号,两个人拿着同一张单去同一个货架拿东西。
- 双重释放(double free)
对同一块堆内存执行两次 delete 的严重错误。属于未定义行为——可能立即崩溃、静默损坏内存、或延迟崩溃。浅拷贝最常见的后果——两个对象析构时各自 delete 同一个指针。修法:深拷贝——每个对象管自己的堆数据。
- 拷贝构造函数(copy constructor)
用已有对象初始化新对象的构造函数——形如
ClassName(const ClassName&)。新对象刚诞生、没有旧资源——只需分配新堆内存并复制数据。必须传引用(否则拷贝参数本身又触发拷贝构造——无限递归)。- 拷贝赋值运算符(copy assignment operator)
把已有对象值赋给另一个已有对象的运算符。实现需兼顾自赋值与异常安全,常先取得新状态再提交,或使用 copy-and-swap;通常返回自身引用以支持链式赋值。
- 析构函数(destructor)
对象销毁时调用的特殊函数,无参数、无返回类型且不能重载。函数体运行后,成员按声明逆序自动销毁。若资源由 RAII 成员管理,合成析构通常足够;直接承担资源政策时才需要自定义清理。
- 三法则(Rule of Three)
C++ 经典准则:自定义了析构、拷贝构造或拷贝赋值中任意一个,几乎一定需要全部三个。因为这三个都是"资源管理"的不同侧面——从不同入口做同一件事。反例:如果你不需要析构(没有资源),大概率拷贝也只要合成的版本。
- 五法则(Rule of Five)
设计资源类时把析构、两项拷贝和两项移动作为一组审查的准则。每项可实现、default 或 delete,并非机械要求手写五个;能由 RAII 成员承担资源时优先采用零法则。
- 移动语义(move semantics)
允许类型为右值或 xvalue 提供转移式构造与赋值的机制。移动可接管资源,也可逐成员移动;复杂度和源对象后置状态由类型契约决定,不普遍保证 O(1) 或零分配。
- 右值引用(rvalue reference)
写作
T&&,可绑定临时量或经转换得到的 xvalue。它使重载决议能选择移动入口,但不保证对象即将销毁,也不自动转移资源。- std::move
把表达式无条件转换为对应 xvalue 的标准库工具。本身不移动、不检查安全性;只有后续调用了会修改源的右值重载,源状态才按该类型契约发生变化。