1. 引言
在C++中,拷贝构造和拷贝赋值运算符都是用于对象拷贝的重要成员函数,但它们在调用时机、使用场景和实现细节上有着本质的区别。理解这两者的差异对于编写正确的、高效的C++代码至关重要,尤其是在涉及资源管理(如动态内存、文件句柄等)的类中。本文将详细解析拷贝构造函数和拷贝赋值运算符的区别,并通过代码示例帮助读者深入理解。
2. 基本概念
2.1 拷贝构造函数
拷贝构造函数用于创建一个新对象,并用另一个同类型对象的值来初始化这个新对象。其函数签名通常为:
cpp
ClassName(const ClassName& other);
或者(C++11起):
cpp
ClassName(const ClassName& other) = default; // 显式默认
ClassName(ClassName&& other) = delete; // 禁止移动构造
2.2 拷贝赋值运算符
拷贝赋值运算符用于将一个已存在对象的值赋给另一个已存在的对象。其函数签名通常为:
cpp
ClassName& operator=(const ClassName& other);
或者:
cpp
ClassName& operator=(const ClassName& other) = default;
3. 核心区别对比
| 对比维度 | 拷贝构造函数 | 拷贝赋值运算符 |
|---|---|---|
| 调用时机 | 创建新对象时 | 已存在对象之间赋值时 |
| 对象状态 | 目标对象尚未构造 | 目标对象已构造完成 |
| 函数签名 | ClassName(const ClassName&) |
ClassName& operator=(const ClassName&) |
| 返回值 | 无返回值(构造函数) | 返回ClassName&(支持链式赋值) |
| 默认行为 | 默认执行浅拷贝(成员逐一拷贝) | 默认执行浅拷贝(成员逐一赋值) |
| 自赋值检查 | 不需要(新对象不可能等于源对象) | 需要(obj = obj是合法操作) |
| 资源管理 | 直接分配新资源 | 先释放旧资源,再分配新资源 |
4. 调用场景示例
4.1 拷贝构造函数的调用场景
cpp
class MyClass {
public:
int* data;
// 拷贝构造函数
MyClass(const MyClass& other) {
data = new int(*other.data);
std::cout << "拷贝构造函数被调用" << std::endl;
}
};
int main() {
MyClass obj1;
obj1.data = new int(42);
// 场景1:通过已有对象初始化新对象
MyClass obj2 = obj1; // 拷贝构造函数
// 场景2:函数传参(按值传递)
void func(MyClass param); // 调用func(obj1)时会调用拷贝构造
// 场景3:函数返回值(某些情况下)
MyClass createObj() {
MyClass local;
return local; // 可能调用拷贝构造(取决于编译器优化)
}
// 场景4:初始化列表
MyClass obj3(obj1); // 拷贝构造函数
return 0;
}
4.2 拷贝赋值运算符的调用场景
cpp
class MyClass {
public:
int* data;
// 拷贝赋值运算符
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 自赋值检查
delete data; // 释放旧资源
data = new int(*other.data); // 分配新资源
std::cout << "拷贝赋值运算符被调用" << std::endl;
}
return *this;
}
};
int main() {
MyClass obj1, obj2, obj3;
obj1.data = new int(10);
obj2.data = new int(20);
obj3.data = new int(30);
// 场景1:直接赋值
obj2 = obj1; // 拷贝赋值运算符
// 场景2:链式赋值
obj3 = obj2 = obj1; // 连续调用拷贝赋值运算符
// 场景3:赋值给已有对象
MyClass obj4;
// ... 一些操作后 ...
obj4 = obj1; // 拷贝赋值运算符
return 0;
}
5. 实现要点与注意事项
5.1 拷贝构造函数的实现要点
- 深拷贝必要性:如果类包含指针成员或需要独占的资源,必须实现深拷贝
- 初始化列表:尽量使用成员初始化列表,特别是对于const成员和引用成员
- 异常安全:在可能抛出异常的操作前完成资源分配
- 默认行为:如果不定义拷贝构造函数,编译器会生成一个执行浅拷贝的默认版本
5.2 拷贝赋值运算符的实现要点
- 自赋值检查 :必须检查
this != &other,避免释放自身资源 - 异常安全:使用"copy-and-swap"惯用法可以提高异常安全性
- 返回*this :必须返回
*this的引用以支持链式赋值 - 释放旧资源:在分配新资源前,必须先释放对象原有的资源
- 处理const成员:const成员不能通过赋值修改,需要特殊处理
5.3 完整示例:实现Rule of Three
cpp
class ResourceHolder {
private:
int* resource;
size_t size;
public:
// 构造函数
ResourceHolder(size_t s = 0) : size(s) {
resource = (s > 0) ? new int[s] : nullptr;
}
// 析构函数
~ResourceHolder() {
delete[] resource;
}
// 拷贝构造函数(深拷贝)
ResourceHolder(const ResourceHolder& other)
: size(other.size) {
if (other.resource) {
resource = new int[other.size];
std::copy(other.resource, other.resource + other.size, resource);
} else {
resource = nullptr;
}
std::cout << "拷贝构造函数" << std::endl;
}
// 拷贝赋值运算符
ResourceHolder& operator=(const ResourceHolder& other) {
if (this != &other) { // 1. 自赋值检查
// 2. 分配新资源(异常安全)
int* newResource = nullptr;
if (other.resource) {
newResource = new int[other.size];
std::copy(other.resource, other.resource + other.size, newResource);
}
// 3. 释放旧资源
delete[] resource;
// 4. 接管新资源
resource = newResource;
size = other.size;
std::cout << "拷贝赋值运算符" << std::endl;
}
return *this; // 5. 返回*this
}
// 打印资源信息
void print() const {
std::cout << "资源大小: " << size << std::endl;
}
};
int main() {
ResourceHolder rh1(5); // 构造函数
ResourceHolder rh2 = rh1; // 拷贝构造函数(创建新对象)
ResourceHolder rh3(3);
rh3 = rh1; // 拷贝赋值运算符(已存在对象)
return 0;
}
6. 常见误区与最佳实践
6.1 常见误区
- 混淆调用时机 :误以为
obj2 = obj1总是调用拷贝构造函数(实际是拷贝赋值) - 忘记自赋值检查:在拷贝赋值运算符中未检查自赋值,导致资源被意外释放
- 浅拷贝问题:对包含指针的类使用默认拷贝,导致多个对象共享同一资源
- 异常不安全:在拷贝赋值中先释放旧资源再分配新资源,如果分配失败则对象处于无效状态
6.2 最佳实践
- Rule of Three:如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,那么它很可能需要全部三个
- 使用copy-and-swap:通过创建临时副本并交换来提高异常安全性
- 优先使用=delete :如果不希望对象被拷贝,使用
= delete明确禁止 - 考虑移动语义:C++11后,对于可移动的资源,实现移动构造函数和移动赋值运算符可以提高性能
- 测试自赋值 :总是测试
obj = obj的情况
7. 总结
拷贝构造函数和拷贝赋值运算符是C++中对象拷贝的两种不同机制:
- 拷贝构造函数用于创建新对象时的初始化,目标对象尚未存在
- 拷贝赋值运算符用于已存在对象之间的赋值,需要处理自赋值和旧资源释放
理解这两者的区别对于编写正确的资源管理代码至关重要。在实际开发中,应遵循Rule of Three原则,根据需要实现深拷贝,并使用copy-and-swap等技巧保证异常安全。对于现代C++,还应考虑Rule of Five,将移动语义纳入设计考量。