22 / 80 · C++11 · 约 10 分钟
深浅拷贝与 Rule of Zero / Five
默认拷贝逐成员处理,是否共享底层资源取决于成员类型,而不是统一的深拷贝或浅拷贝标签。优先让标准资源类型管理所有权;只有确需自定义复制语义时,才同时设计拷贝、移动、赋值和析构。
复制成员不等于复制资源
编译器生成的拷贝通常逐个复制基类和成员。复制裸指针只复制地址,不复制所指对象;复制 vector 则调用 vector 的值语义复制,得到独立元素存储;复制 shared_ptr 则有意共享所有权。因此判断深浅拷贝,要沿着成员的所有权语义分析。
若一个类用裸指针独占资源,却只自定义 delete 析构,默认复制会让多个对象误认为自己独占同一资源,最终产生重复释放。并非所有浅拷贝都错误:非拥有的观察指针本来就应指向同一个外部对象,但必须明确它不能延长被观察对象的生命周期。
优先让成员承担所有权
Rule of Zero 指业务类通常无需自己声明拷贝、移动、赋值和析构,让 string、vector、unique_ptr 等成员完成工作。含 vector 的值对象自然可复制;含 unique_ptr 的对象自然不可复制但可移动,这常常正是所需契约。
Rule of Five 则提醒资源类型协调五个特殊成员,不要求每个都手写实现:可以 default,也可以 delete。用户声明析构,包括写 = default,会影响隐式移动的生成;而拥有移动构造的表达式也不意味着底层资源必然真的发生移动。
自定义深拷贝要包含失败语义
示例 DeepBox 需要独占存储且允许独立复制,因此拷贝构造分配新整数,赋值先构造临时副本再交换。如果分配失败,原目标保持不变;自赋值也不破坏数据。移动直接交给 unique_ptr,移动后的空状态通过 value() 明确定义为零。
复制并交换容易推理,但可能放弃复用已有容量,不能当成所有容器赋值的性能模板。真实资源类还应明确复制是否昂贵、移动是否能不抛出、移动后支持哪些操作,避免调用者把“有效状态”误当成“仍保留原值”。
容易答错的地方
- memcpy 不是通用对象复制方案;带资源所有权或虚函数的类型尤其不能用字节复制替代特殊成员。
- 写了析构后不要假定编译器仍自动生成移动操作;必须重新检查五个特殊成员的可用性。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <memory>
#include <utility>
#include <vector>
struct Values {
std::vector<int> items;
};
class DeepBox {
std::unique_ptr<int> value_;
public:
explicit DeepBox(int value) : value_(new int(value)) {}
DeepBox(const DeepBox& other)
: value_(other.value_ ? new int(*other.value_) : nullptr) {}
DeepBox& operator=(const DeepBox& other) {
DeepBox temporary(other);
value_.swap(temporary.value_);
return *this;
}
DeepBox(DeepBox&&) noexcept = default;
DeepBox& operator=(DeepBox&&) noexcept = default;
~DeepBox() = default;
int value() const { return value_ ? *value_ : 0; }
void set(int value) {
if (value_) *value_ = value;
else value_.reset(new int(value));
}
};
int main() {
Values a{{1, 2}};
Values b = a;
b.items[0] = 8;
assert(a.items[0] == 1);
DeepBox original(3);
DeepBox copy = original;
copy.set(7);
assert(original.value() == 3 && copy.value() == 7);
original = original;
DeepBox moved = std::move(copy);
assert(original.value() == 3);
assert(moved.value() == 7 && copy.value() == 0);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-copy.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
如果 DeepBox 的语义改成只能转移、不能复制,最简单的 Rule of Zero 版本是什么?
查看参考答案
保留 unique_ptr<int> 成员、普通构造函数和查询函数,删除示例中手写或显式默认的五个特殊成员声明。unique_ptr 会使隐式拷贝不可用,并支持隐式移动;无需额外声明析构。若业务还要求显式写 delete 来强调接口,则需要同时显式恢复所需的移动操作,这就不再是严格的零规则形式。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。