在 C++ 里,深浅拷贝不是语法关键字,而是资源管理策略 。
默认拷贝对裸指针是浅拷贝;深拷贝要自己写;现代 C++ 优先 Rule of Zero。
1. C++ 中拷贝发生在哪?
C++ 里有两个容易混淆的动作:
cpp
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. 浅拷贝:只复制指针,不复制资源
看一个危险例子:
cpp
#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
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. 深拷贝:分配新资源,复制内容
深拷贝要求:新对象拥有自己的资源,与原对象完全独立。
cpp
#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_;
};
现在:
cpp
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 写法:
cpp
Buffer& operator=(const Buffer& other) {
if (this != &other) {
Buffer tmp(other); // 先拷贝,失败也不影响 *this
swap(tmp); // 交换后,tmp 持有旧资源
}
return *this;
} // tmp 析构,释放旧资源
优点:
-
自赋值安全;
-
强异常安全;
-
拷贝逻辑复用拷贝构造。
5. 移动语义:不是拷贝,是资源转移
深拷贝很贵。如果源对象马上要销毁,就没必要复制资源,直接"偷"过来即可。
cpp
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 类型作为成员,让编译器生成正确的拷贝、移动和析构。
cpp
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* |
浅拷贝 | 只复制地址,危险 |
| 自定义资源类 | 看实现 | 需要自己决定深/浅/移动 |
示例:
cpp
std::vector<int> a = {1, 2, 3};
std::vector<int> b = a; // 深拷贝
b[0] = 100;
// a[0] 仍然是 1
cpp
auto p1 = std::make_shared<Buffer>(10);
auto p2 = p1; // 共享同一个 Buffer,不是深拷贝
如果你想要 shared_ptr 的深拷贝,需要手动克隆:
cpp
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++ 尽量零法则。