拷贝构造与拷贝赋值
更新时间:2026-08-27。本文是
languages/cpp/主题入门层第 20 篇。Circle c2 = c1和c2 = c1看着像,走的却是两条路:前者拷贝构造、后者拷贝赋值。默认实现是逐成员拷贝,遇到指针成员就变浅拷贝——这是 C/C++ 共同的老坑,本篇把它讲透并引出 Rule of Three。
本文要回答的问题
- 拷贝构造和拷贝赋值到底差在哪?
- "浅拷贝"为什么会崩?"深拷贝"怎么救?
- Rule of Three 是什么,为什么说它是军规?
一、两个"拷贝"的区别
Circle c1(5.0);
Circle c2 = c1; // 拷贝构造:用 c1 创建一个新对象 c2
Circle c3;
c3 = c1; // 拷贝赋值:c3 已存在,把 c1 的值灌进去判断口诀:对象创建的那一行出现 =,是拷贝构造;对象已存在,= 才是拷贝赋值。Circle c2 = c1 是在创建 c2,所以走构造;c3 = c1 时 c3 早就存在了,走赋值。
对应的两个函数签名:
class Circle {
public:
Circle(const Circle& other); // 拷贝构造
Circle& operator=(const Circle& other); // 拷贝赋值
};注意拷贝构造的参数是 const Circle&(第 14 篇的 const 引用——拷贝必然不该改源对象,也避免了一次不必要的拷贝)。
二、默认行为:逐成员拷贝
你不写这两个函数,编译器自动生成:把每个成员按值复制一份。对简单类(全是内建类型成员)完全够用:
Circle c2 = c1; // radius_ 拷贝,行为正确但成员里有指针时就危险了。看这个类:
class Buffer {
char* data_;
int size_;
public:
Buffer(int n) : data_(new char[n]), size_(n) {}
~Buffer() { delete[] data_; } // 析构释放
};Buffer a(100); Buffer b = a; 发生什么?默认拷贝把 data_ 这个指针的值复制过去——于是 a.data_ 和 b.data_ 指向同一块堆内存。这叫浅拷贝:

后果:a、b 析构时都 delete[] data_,同一块内存释放两次——双重释放,程序崩溃或内存损坏。
三、深拷贝:自己动手,各占一份
修法是自己实现拷贝构造/赋值,让每个对象复制一份独立的数据:
class Buffer {
char* data_;
int size_;
public:
Buffer(int n) : data_(new char[n]), size_(n) {}
~Buffer() { delete[] data_; }
Buffer(const Buffer& other) // 深拷贝构造
: data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
Buffer& operator=(const Buffer& other) { // 深拷贝赋值
if (this != &other) { // 自赋值保护
delete[] data_; // 先释放旧数据
size_ = other.size_;
data_ = new char[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
return *this;
}
};要点:拷贝构造"新开一块内存、逐字节复制";拷贝赋值还要多两步——先释放自己的旧数据(否则泄漏)、检查自赋值(a = a 不炸)。std::copy 在第 20 篇之后的 STL 篇正式登场,这里先用着。
四、Rule of Three:三件套
如果一个类自己写了析构函数(说明它管着资源),那么拷贝构造、拷贝赋值也大概率要自己写——因为它们仨经常一起出现:
析构函数、拷贝构造、拷贝赋值,三者要么都用默认,要么都自己写。
这就是 Rule of Three。违反的典型症状就是上面的双重释放。现代 C++ 更推荐的其实是不用指针成员、改用 std::string/std::vector/std::unique_ptr——它们自带正确的拷贝/移动语义,让 Rule of Three 大部分时候自动满足。这点在 STL 容器 和 RAII 篇会反复强调。
五、和 C 对比
| 对比项 | C | C++ |
|---|---|---|
| 结构体复制 | b = a 逐成员复制 | 同:默认逐成员复制 |
| 指针成员 | 浅拷贝,同样的坑 | 一样有坑,但能自定义 |
| 拦截时机 | 无,跑起来才崩 | 拷贝构造/赋值可自定义深拷贝 |
| 编译器检查 | 不检查 | 不检查(但 Rule of Three 提醒) |
| 现代解法 | 手动管理 | 用 std::string/vector 自动正确拷贝 |
C 的 struct 赋值和 C++ 的默认拷贝构造本质上都是逐成员复制,浅拷贝的坑是 C/C++ 共享的。差别在于 C++ 给了你深拷贝的"钩子",而 C 只能靠封装(不透明的指针 + 工厂函数)来绕。
六、与本站主线衔接
- 移动语义与右值引用是拷贝的进阶替代:
std::vector扩容时为什么宁可"移动"不"拷贝",见 移动语义与右值引用; std::string等标准库类型自带正确拷贝语义,见 STL 容器;- 对象复制时的性能开销与拷贝省略(RVO),见高手层拷贝省略与 RVO。
一句话总结
拷贝构造(创建时 =)和拷贝赋值(已存在时 =)默认逐成员复制,遇到指针成员就是浅拷贝,析构时双重释放必炸;自己管资源时按 Rule of Three 把析构、拷贝构造、拷贝赋值一起写(深拷贝:新开内存 + 释放旧数据 + 自赋值检查);最省事的办法是别用裸指针成员,交给 std::string/std::vector 自动管理。