拷贝控制
定义与作用
拷贝控制(Copy Control)指控制对象被拷贝时的行为,包括两个核心操作:
| 操作 | 语法 | 何时调用 |
|---|---|---|
| 拷贝构造函数 | T(const T&) | 用已有对象初始化新对象 |
| 拷贝赋值运算符 | T& operator=(const T&) | 将已有对象赋值给另一个已有对象 |
当类管理动态资源(堆内存、文件句柄、网络连接)时,默认的逐成员拷贝会引发双重释放或资源泄漏,必须自定义拷贝语义。
核心原理
浅拷贝 vs 深拷贝
Rule of Three
如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,那么通常三者都需要自定义。C++11 扩展为 Rule of Five(加上移动构造和移动赋值)。
完整示例
示例一:动态数组类的深拷贝实现
场景说明:实现一个简化的动态数组类,展示深拷贝的完整实现和必要性。
#include <iostream>
#include <algorithm>
#include <stdexcept>
class IntArray {
public:
// 构造函数
explicit IntArray(size_t size = 0)
: size_(size), data_(size ? new int[size]() : nullptr) {
std::cout << "[构造] 分配 " << size_ << " 个元素" << std::endl;
}
// 拷贝构造函数(深拷贝)
IntArray(const IntArray& other)
: size_(other.size_), data_(other.size_ ? new int[other.size_] : nullptr) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "[拷贝构造] 深拷贝 " << size_ << " 个元素" << std::endl;
}
// 拷贝赋值运算符(深拷贝 + 自赋值安全)
IntArray& operator=(const IntArray& other) {
std::cout << "[拷贝赋值] 从 " << other.size_ << " 元素拷贝" << std::endl;
if (this != &other) { // 自赋值检查
// copy-and-swap 惯用法
IntArray temp(other); // 拷贝构造临时对象
std::swap(size_, temp.size_);
std::swap(data_, temp.data_);
} // temp 析构,释放旧资源
return *this;
}
// 析构函数
~IntArray() {
std::cout << "[析构] 释放 " << size_ << " 个元素" << std::endl;
delete[] data_;
}
// 访问元素
int& operator[](size_t i) {
if (i >= size_) throw std::out_of_range("索引越界");
return data_[i];
}
const int& operator[](size_t i) const {
if (i >= size_) throw std::out_of_range("索引越界");
return data_[i];
}
size_t size() const { return size_; }
void print() const {
std::cout << "[";
for (size_t i = 0; i < size_; ++i) {
if (i > 0) std::cout << ", ";
std::cout << data_[i];
}
std::cout << "]" << std::endl;
}
private:
size_t size_;
int* data_;
};
int main() {
std::cout << "===== 拷贝构造 =====" << std::endl;
IntArray a(3);
a[0] = 10; a[1] = 20; a[2] = 30;
std::cout << "a = "; a.print();
// 拷贝构造:独立的内存空间
IntArray b = a; // 等价 IntArray b(a)
std::cout << "b = "; b.print();
// 验证独立性:修改 b 不影响 a
b[0] = 999;
std::cout << "\n修改 b[0] = 999 后:" << std::endl;
std::cout << "a = "; a.print();
std::cout << "b = "; b.print();
std::cout << "\n===== 拷贝赋值 =====" << std::endl;
IntArray c(2);
c[0] = 1; c[1] = 2;
std::cout << "赋值前 c = "; c.print();
c = a; // 拷贝赋值
std::cout << "赋值后 c = "; c.print();
std::cout << "\n===== 自赋值安全 =====" << std::endl;
a = a; // 自赋值,应该安全
std::cout << "自赋值后 a = "; a.print();
return 0;
}
预期输出:
===== 拷贝构造 =====
[构造] 分配 3 个元素
a = [10, 20, 30]
[拷贝构造] 深拷贝 3 个元素
b = [10, 20, 30]
修改 b[0] = 999 后:
a = [10, 20, 30]
b = [999, 20, 30]
===== 拷贝赋值 =====
[构造] 分配 2 个元素
赋值前 c = [1, 2]
[拷贝赋值] 从 3 元素拷贝
[拷贝构造] 深拷贝 3 个元素
[析构] 释放 2 个元素
赋值后 c = [10, 20, 30]
===== 自赋值安全 =====
[拷贝赋值] 从 3 元素拷贝
[拷贝构造] 深拷贝 3 个元素
[析构] 释放 3 个元素
自赋值后 a = [10, 20, 30]
...
逐段分析:
- 拷贝构造分配新内存并复制数据——b 和 a 的数据完全独立
- 深拷贝验证:修改
b[0] = 999后a[0]仍是 10——两对象内存独立 - 拷贝赋值使用 copy-and-swap 惯用法:先构造临时对象(可能抛异常但不影响原对象),再交换指针(永远成功),临时对象析构释放旧资源
- 自赋值安全:
if (this != &other)检查自赋值,但 copy-and-swap 本身也能正确处理自赋值 - 如果没有自定义拷贝控制,默认的逐成员拷贝会导致两个
IntArray共享同一块内存,析构时双重释放 → 崩溃
示例二:禁用拷贝的类
#include <iostream>
class DatabaseConnection {
public:
DatabaseConnection(const std::string& conn_str) {
std::cout << "连接数据库: " << conn_str << std::endl;
}
// 禁用拷贝:数据库连接不应被随意复制
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
~DatabaseConnection() {
std::cout << "断开数据库连接" << std::endl;
}
};
int main() {
DatabaseConnection db1("mysql://localhost:3306/mydb");
// 以下两行都会编译错误:
// DatabaseConnection db2 = db1;
// DatabaseConnection db3(""); db3 = db1;
return 0;
}
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 默认拷贝导致双重释放 | 程序崩溃 | 管理资源的类自定义拷贝控制 |
| 拷贝赋值忘记自赋值检查 | 先释放再使用自身数据 | 使用 copy-and-swap 惯用法 |
| 拷贝构造中浅拷贝指针 | 两对象共享同一内存 | 分配新内存并复制数据 |
| 派生类拷贝构造忘记调用基类拷贝构造 | 基类部分未正确拷贝 | 初始化列表中调用基类拷贝构造 |
| 拷贝赋值未处理异常安全 | 赋值中途抛异常,对象处于中间状态 | copy-and-swap 保证强异常安全 |
常见面试问题
什么是浅拷贝和深拷贝?各自的适用场景?——浅拷贝只复制指针值,两对象共享资源;深拷贝复制指针指向的内容。默认编译器生成的是浅拷贝。管理堆资源的类必须深拷贝。
Rule of Three 是什么?为什么需要?——如果自定义了析构函数、拷贝构造函数或拷贝赋值运算符中的一个,通常三者都需要。因为它们都涉及资源管理,编译器默认生成的行为通常不正确。
copy-and-swap 惯用法的原理和优势?——先用拷贝构造创建临时对象,再
swap资源,临时对象析构释放旧资源。优势:自赋值安全、强异常安全、代码复用。如何禁止拷贝?C++11 前后有什么不同?——C++11 前将拷贝构造和拷贝赋值声明为 private 且不实现;C++11 起用
= delete明确禁止。编译器何时自动生成拷贝构造函数和拷贝赋值运算符?——当用户没有自定义时自动生成(逐成员拷贝)。C++11 中如果自定义了移动操作,拷贝操作被隐式删除。
小结
- 管理动态资源的类必须自定义拷贝控制,实现深拷贝
- Rule of Three:自定义析构/拷贝构造/拷贝赋值中的任一个,通常都需要全部自定义
- copy-and-swap 是拷贝赋值的标准惯用法:自赋值安全 + 强异常安全
- 不打算支持拷贝的类使用
= delete明确禁用 - C++11 起 Rule of Five 替代 Rule of Three(增加移动构造和移动赋值)