在 C++ 里,深浅拷贝不是语法关键字,而是资源管理策略。
默认拷贝对裸指针是浅拷贝;深拷贝要自己写;现代 C++ 优先 Rule of Zero。
1. C++ 中拷贝发生在哪?
C++ 里有两个容易混淆的动作:
Buffer a(10); Buffer b = a; // 拷贝构造:用 a 初始化 b Buffer c(5); c = a; // 拷贝赋值:把 a 赋给已存在的 c拷贝构造:
T(const T& other),创建新对象。拷贝赋值:
T& operator=(const T& other),已有对象被赋值。如果自己不写,编译器会隐式生成默认版本。
默认版本做的事情是:成员逐一拷贝。
对int、double等标量,直接复制值。
对std::string、std::vector等类类型成员,调用它们自己的拷贝构造。
对裸指针成员,只复制指针地址。
所以关键不是“默认拷贝一定是浅拷贝”,而是:
默认拷贝对裸指针/句柄是浅拷贝;对 RAII 类型成员,则依赖该类型自己的拷贝语义。
2. 浅拷贝:只复制指针,不复制资源
看一个危险例子:
#include <cstring> class Shallow { public: explicit Shallow(const char* s) : data_(new char[std::strlen(s) + 1]) { std::strcpy(data_, s); } ~Shallow() { delete[] data_; } // 没有自定义拷贝构造和拷贝赋值 // 编译器生成默认版本:浅拷贝 private: char* data_; };使用:
cpp
int main() { Shallow a("hello"); Shallow b = a; // 默认拷贝构造:b.data_ == a.data_ }问题:
a.data_和b.data_指向同一块堆内存。作用域结束时,
a和b都会delete[] data_。同一块内存被释放两次:double free,程序崩溃或未定义行为。
浅拷贝还可能带来:
悬垂指针;
意外修改:改
b影响a;资源泄漏或重复释放。
3. 深拷贝:分配新资源,复制内容
深拷贝要求:新对象拥有自己的资源,与原对象完全独立。
#include <algorithm> #include <cstddef> #include <cstring> #include <utility> class Buffer { public: explicit Buffer(std::size_t n = 0) : size_(n), data_(n ? new char[n]{} : nullptr) {} explicit Buffer(const char* s) : size_(std::strlen(s)), data_(new char[size_ + 1]) { std::memcpy(data_, s, size_ + 1); } ~Buffer() { delete[] data_; } // 深拷贝构造 Buffer(const Buffer& other) : size_(other.size_), data_(other.size_ ? new char[other.size_] : nullptr) { if (size_ > 0) { std::copy(other.data_, other.data_ + other.size_, data_); } } // 深拷贝赋值:copy-and-swap Buffer& operator=(const Buffer& other) { if (this != &other) { Buffer tmp(other); swap(tmp); } return *this; } // 移动构造:转移资源,不复制 Buffer(Buffer&& other) noexcept : size_(other.size_), data_(other.data_) { other.size_ = 0; other.data_ = nullptr; } // 移动赋值 Buffer& operator=(Buffer&& other) noexcept { if (this != &other) { delete[] data_; size_ = other.size_; data_ = other.data_; other.size_ = 0; other.data_ = nullptr; } return *this; } void swap(Buffer& other) noexcept { std::swap(size_, other.size_); std::swap(data_, other.data_); } std::size_t size() const { return size_; } const char* data() const { return data_; } private: std::size_t size_; char* data_; };现在:
Buffer a("hello"); Buffer b = a; // 深拷贝,b 有自己的内存 Buffer c(3); c = a; // 深拷贝赋值,c 也独立修改b或c不会影响a。
4. 拷贝赋值为什么要 copy-and-swap?
拷贝赋值最容易踩的坑:
自赋值:
a = a;异常安全:分配新内存失败时,不能破坏原对象。
copy-and-swap 写法:
Buffer& operator=(const Buffer& other) { if (this != &other) { Buffer tmp(other); // 先拷贝,失败也不影响 *this swap(tmp); // 交换后,tmp 持有旧资源 } return *this; } // tmp 析构,释放旧资源优点:
自赋值安全;
强异常安全;
拷贝逻辑复用拷贝构造。
5. 移动语义:不是拷贝,是资源转移
深拷贝很贵。如果源对象马上要销毁,就没必要复制资源,直接“偷”过来即可。
Buffer a(100); Buffer b = std::move(a); // 移动构造移动后:
b拿到a原来的指针和大小;a被置空;没有堆内存复制,通常 O(1)。
移动构造和移动赋值应尽量标记noexcept,这样std::vector扩容时才会优先移动而不是拷贝。
注意:
std::move本身不移动,只是把表达式转换成右值引用。
6. Rule of Three / Five / Zero
Rule of Three
如果你需要自定义以下三者中的任意一个:
析构函数;
拷贝构造;
拷贝赋值;
通常三个都需要。
因为这说明类在管理资源。只写析构,不写拷贝,就会掉进浅拷贝陷阱。
Rule of Five
C++11 之后,再加上:
移动构造;
移动赋值。
合称五法则。上面的Buffer就是五法则实现。
Rule of Zero
最推荐:不要自己管理资源。
用std::string、std::vector、std::unique_ptr、std::shared_ptr等 RAII 类型作为成员,让编译器生成正确的拷贝、移动和析构。
class Person { public: Person(std::string name, std::vector<int> scores) : name_(std::move(name)), scores_(std::move(scores)) {} private: std::string name_; std::vector<int> scores_; };这个类不需要写析构、拷贝构造、拷贝赋值、移动构造、移动赋值。
编译器生成的版本会正确调用std::string和std::vector的拷贝/移动。
7. 常见 RAII 类型的拷贝语义
| 类型 | 拷贝行为 | 说明 |
|---|---|---|
std::string | 深拷贝 | 新字符串拥有自己的缓冲区 |
std::vector<T> | 深拷贝 | 新 vector 拥有自己的元素存储 |
std::unique_ptr<T> | 禁止拷贝 | 只能移动,独占所有权 |
std::shared_ptr<T> | 共享所有权 | 拷贝只增加引用计数,不复制对象 |
裸指针T* | 浅拷贝 | 只复制地址,危险 |
| 自定义资源类 | 看实现 | 需要自己决定深/浅/移动 |
示例:
std::vector<int> a = {1, 2, 3}; std::vector<int> b = a; // 深拷贝 b[0] = 100; // a[0] 仍然是 1auto p1 = std::make_shared<Buffer>(10); auto p2 = p1; // 共享同一个 Buffer,不是深拷贝如果你想要shared_ptr的深拷贝,需要手动克隆:
auto p3 = std::make_shared<Buffer>(*p1); // 调用 Buffer 的拷贝构造8. 面试常见问题
Q:默认拷贝构造是浅拷贝吗?
A:默认拷贝构造是成员逐一拷贝。对裸指针成员,它复制地址,是浅拷贝;对std::string、std::vector等成员,会调用它们的拷贝构造,通常是深拷贝。
Q:浅拷贝会导致什么问题?
A:共享资源,可能出现 double free、悬垂指针、意外修改。
Q:深拷贝怎么实现?
A:在拷贝构造中分配新资源并复制内容;在拷贝赋值中用 copy-and-swap 或先释放再复制,并处理自赋值。
Q:拷贝赋值为什么需要自赋值检查?
A:防止先释放自己的资源,再从已释放的资源拷贝。
Q:移动和拷贝的区别?
A:拷贝复制资源;移动转移资源,源对象变成有效但未指定状态。
Q:什么时候用 Rule of Zero?
A:优先使用 RAII 成员,避免手动管理资源,让编译器生成特殊成员函数。
9. 总结
C++ 默认拷贝是成员逐一拷贝。
裸指针成员默认拷贝是浅拷贝,共享资源。
深拷贝需要自己分配资源并复制内容。
管理资源的类通常遵循Rule of Three / Five。
现代 C++ 优先Rule of Zero:用
string、vector、智能指针。移动语义用于转移资源,不是拷贝。
unique_ptr不可拷贝,shared_ptr拷贝是共享所有权,不是深拷贝。
一句话:
裸指针默认浅拷贝,资源类要深拷贝,现代 C++ 尽量零法则。