之前我们总结了C++类和对象的概念,也提到了构造函数与析构函数,本文我们来了解拷贝构造函数与赋值运算符重载:
一、拷贝构造函数:拷贝构造函数是一种特殊构造函数,使用同一个类的已有对象,去创建新对象。
特点:
1、函数名与类名相同;
2、唯一形参:同类的 const 引用(必须是引用!直接传值会无限递归调用);
3、没有返回值;
4、如果自己不写,编译器会自动提供默认拷贝构造函数。
(一)默认拷贝构造函数(浅拷贝)
逐成员拷贝(值拷贝 / 浅拷贝)对于普通成员变量直接复制;指针成员:只复制变量本身的值(地址),不会新开堆内存。
问题根源:两个对象指针指向同一块堆内存,析构重复释放。
(二)自写拷贝构造函数(深拷贝)
重新开辟堆内存,让新对象拥有独立资源,解决浅拷贝崩溃问题:
(三)什么时候调用拷贝构造函数
拷贝构造函数在以下几种情况下会被调用:
- 用一个对象初始化另一个对象时:
class MyClass { public: MyClass(const MyClass& other) { // 拷贝构造函数实现 } }; int main() { MyClass obj1; MyClass obj2 = obj1; // 调用拷贝构造函数 MyClass obj3(obj1); // 调用拷贝构造函数 return 0; } - 函数参数传递对象时(按值传递):
void processObject(MyClass obj) { // 函数体 } int main() { MyClass original; processObject(original); // 调用拷贝构造函数,创建形参obj return 0; } - 函数返回对象时(按值返回):
MyClass createObject() { MyClass obj; return obj; // 可能会调用拷贝构造函数(取决于编译器优化) } int main() { MyClass newObj = createObject(); // 调用拷贝构造函数 return 0; } - 容器中插入对象时:
#include <vector> int main() { std::vector<MyClass> vec; MyClass obj; vec.push_back(obj); // 调用拷贝构造函数 return 0; }
注意:现代编译器会进行返回值优化(RVO)和命名返回值优化(NRVO),在某些情况下会避免不必要的拷贝构造函数调用,提高程序效率。
二、赋值运算符重载
赋值运算符重载是C++中允许我们自定义类对象之间赋值行为的运算符重载。与拷贝构造函数不同,赋值运算符用于已存在对象之间的赋值操作。
特点:
- 函数名固定为
operator= - 返回类型通常是类的引用(支持连续赋值)
- 参数通常是同类的 const 引用
- 需要处理自赋值情况
- 需要释放原有资源并分配新资源(深拷贝)
(一)默认赋值运算符(浅拷贝)
如果不自定义赋值运算符,编译器会生成默认的赋值运算符,执行逐成员浅拷贝:
class ShallowCopyClass { private: int* data; int size; public: // 构造函数 ShallowCopyClass(int s) : size(s) { data = new int[size]; for (int i = 0; i < size; i++) { data[i] = i; } } // 析构函数 ~ShallowCopyClass() { delete[] data; } // 没有自定义赋值运算符,使用默认浅拷贝 }; int main() { ShallowCopyClass obj1(5); ShallowCopyClass obj2(3); obj2 = obj1; // 默认赋值运算符:浅拷贝! // obj1和obj2的data指针指向同一内存 return 0; // 程序结束时会双重释放,导致崩溃! }问题:与默认拷贝构造函数类似,默认赋值运算符只复制指针值,导致两个对象指向同一块堆内存,析构时重复释放。
(二)自定义赋值运算符(深拷贝)
为了解决浅拷贝问题,我们需要自定义赋值运算符,实现深拷贝:
class DeepCopyClass { private: int* data; int size; public: // 构造函数 DeepCopyClass(int s) : size(s) { data = new int[size]; for (int i = 0; i < size; i++) { data[i] = i; } } // 拷贝构造函数(深拷贝) DeepCopyClass(const DeepCopyClass& other) : size(other.size) { data = new int[size]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } } // 赋值运算符重载(深拷贝) DeepCopyClass& operator=(const DeepCopyClass& other) { // 1. 检查自赋值 if (this == &other) { return *this; } // 2. 释放原有资源 delete[] data; // 3. 分配新资源并复制数据 size = other.size; data = new int[size]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } // 4. 返回当前对象的引用 return *this; } // 析构函数 ~DeepCopyClass() { delete[] data; } // 打印数据(用于测试) void print() const { for (int i = 0; i < size; i++) { std::cout << data[i] << " "; } std::cout << std::endl; } }; int main() { DeepCopyClass obj1(5); DeepCopyClass obj2(3); std::cout << "赋值前obj2: "; obj2.print(); // 输出: 0 1 2 obj2 = obj1; // 调用自定义赋值运算符(深拷贝) std::cout << "赋值后obj2: "; obj2.print(); // 输出: 0 1 2 3 4 // 修改obj1,不会影响obj2 // ...(假设有修改方法) return 0; // 正常析构,不会崩溃 }(三)拷贝构造函数 vs 赋值运算符
| 对比项 | 拷贝构造函数 | 赋值运算符 |
|---|---|---|
| 调用时机 | 创建新对象时 | 已存在对象之间赋值时 |
| 函数签名 | ClassName(const ClassName&) | ClassName& operator=(const ClassName&) |
| 返回类型 | 无(构造函数) | 类的引用(支持连续赋值) |
| 资源管理 | 分配新资源 | 先释放旧资源,再分配新资源 |
| 自处理 | 不需要(新对象) | 需要检查自赋值 |
| 示例 | MyClass obj2 = obj1; | obj2 = obj1; |
(四)现代C++的改进:拷贝并交换惯用法
使用"拷贝并交换"惯用法可以简化赋值运算符的实现,同时提供强异常安全性:
class SmartClass { private: int* data; int size; // 交换函数 friend void swap(SmartClass& first, SmartClass& second) noexcept { using std::swap; swap(first.data, second.data); swap(first.size, second.size); } public: // 构造函数 SmartClass(int s = 0) : size(s), data(size ? new int[size] : nullptr) { for (int i = 0; i < size; i++) { data[i] = i; } } // 拷贝构造函数 SmartClass(const SmartClass& other) : size(other.size), data(size ? new int[size] : nullptr) { for (int i = 0; i < size; i++) { data[i] = other.data[i]; } } // 赋值运算符(拷贝并交换) SmartClass& operator=(SmartClass other) { // 注意:按值传递! swap(*this, other); // 交换当前对象和临时对象 return *this; // other离开作用域时会释放旧资源 } // 移动构造函数(C++11) SmartClass(SmartClass&& other) noexcept : data(nullptr), size(0) { swap(*this, other); } // 移动赋值运算符(C++11) SmartClass& operator=(SmartClass&& other) noexcept { swap(*this, other); return *this; } // 析构函数 ~SmartClass() { delete[] data; } }; int main() { SmartClass obj1(5); SmartClass obj2; obj2 = obj1; // 调用拷贝并交换的赋值运算符 return 0; }优点:
- 代码简洁,避免重复
- 自动处理自赋值(交换是安全的)
- 提供强异常安全性
- 同时支持拷贝和移动语义(C++11)
(五)什么时候需要自定义赋值运算符?
- 类包含动态分配的内存(指针成员)
- 类包含文件句柄、网络连接等资源
- 需要实现引用计数或写时复制
- 需要特殊的赋值逻辑(如数据库记录复制)
总结:赋值运算符重载是C++资源管理的重要组成部分。对于管理资源的类,通常需要同时实现"拷贝构造函数、赋值运算符、析构函数"(Rule of Three),在C++11及以后还需要考虑"移动构造函数、移动赋值运算符"(Rule of Five)。