自定义拷贝构造函数与自定义拷贝赋值函数的区别
自定义拷贝构造函数和自定义拷贝赋值运算符虽然都用于对象的拷贝,但它们的调用时机和功能目的有本质区别。
核心区别总结
| 特性 | 拷贝构造函数 (Copy Constructor) | 拷贝赋值运算符 (Copy Assignment Operator) |
|---|---|---|
| 目的 | 创建一个新对象,并用一个已存在的同类对象初始化它。 | 给一个已存在的对象赋予新值,这个值来自另一个已存在的同类对象。 |
| 调用时机 | 1. 用=初始化新对象时 (MyClass obj1 = obj2;)2. 函数传参(值传递)时 3. 函数返回值(值返回)时 |
当一个已初始化的对象出现在=左边时 (obj1 = obj2;) |
| 函数签名 | ClassName(const ClassName& other) |
ClassName& operator=(const ClassName& other) |
| 返回值 | 无返回值(构造函数) | 通常返回对当前对象的引用 (*this),以支持链式赋值 (a = b = c) |
代码示例说明
用一个简单的String类来演示,这个类管理一个动态分配的字符数组,是典型的需要自定义拷贝控制函数的场景。
#include <iostream>
#include <cstring>
class String {
private:
char* m_data;
size_t m_size;
public:
// 1. 普通构造函数
String(const char* cstr = "") {
std::cout << "普通构造函数被调用: " << cstr << std::endl;
m_size = strlen(cstr);
m_data = new char[m_size + 1];
strcpy_s(m_data, m_size + 1, cstr); // 使用安全的 strcpy_s
}
// 2. 自定义析构函数
~String() {
std::cout << "析构函数被调用: " << (m_data ? m_data : "nullptr") << std::endl;
delete[] m_data;
}
// 3. 自定义拷贝构造函数
String(const String& other) {
std::cout << "拷贝构造函数被调用,从 " << other.m_data << " 拷贝" << std::endl;
m_size = other.m_size;
m_data = new char[m_size + 1];
strcpy_s(m_data, m_size + 1, other.m_data); // 使用安全的 strcpy_s
}
// 4. 自定义拷贝赋值运算符
String& operator=(const String& other) {
std::cout << "拷贝赋值运算符被调用,将 " << other.m_data << " 赋给 " << m_data << std::endl;
if (this == &other) {
return *this;
}
delete[] m_data;
m_size = other.m_size;
m_data = new char[m_size + 1];
strcpy_s(m_data, m_size + 1, other.m_data); // 使用安全的 strcpy_s
return *this;
}
void print() const {
std::cout << "Data: " << m_data << ", Addr: " << (void*)m_data << std::endl;
}
};
int main() {
std::cout << "----- 场景1:拷贝构造函数的调用 -----" << std::endl;
String str1("Hello"); // 调用普通构造函数
String str2 = str1; // 调用拷贝构造函数!(初始化)
String str3(str1); // 调用拷贝构造函数!(直接初始化)
std::cout << "\nstr1: "; str1.print();
std::cout << "str2: "; str2.print(); // str2的数据地址与str1不同
std::cout << "str3: "; str3.print(); // str3的数据地址与str1不同
std::cout << "\n----- 场景2:拷贝赋值运算符的调用 -----" << std::endl;
String str4("World"); // 调用普通构造函数
std::cout << "赋值前 str4: "; str4.print();
str4 = str1; // 调用拷贝赋值运算符!(赋值)
std::cout << "赋值后 str4: "; str4.print(); // str4的数据地址已改变
std::cout << "\n----- 场景3:函数传参(值传递) -----" << std::endl;
// 假设有一个函数 void foo(String s);
// 调用 foo(str1) 时,形参s会通过拷贝构造函数初始化
std::cout << "\n----- 主函数结束,开始析构 -----" << std::endl;
// 所有对象离开作用域,析构函数被调用
return 0;
}
输出结果分析(可能类似如下):
----- 场景1:拷贝构造函数的调用 -----
普通构造函数被调用: Hello
拷贝构造函数被调用,从 Hello 拷贝
拷贝构造函数被调用,从 Hello 拷贝
str1: Data: Hello, Addr: 000001D646C54310
str2: Data: Hello, Addr: 000001D646C547C0
str3: Data: Hello, Addr: 000001D646C54360
----- 场景2:拷贝赋值运算符的调用 -----
普通构造函数被调用: World
赋值前 str4: Data: World, Addr: 000001D646C54BD0
拷贝赋值运算符被调用,将 Hello 赋给 World
赋值后 str4: Data: Hello, Addr: 000001D646C548B0
----- 场景3:函数传参(值传递) -----
----- 主函数结束,开始析构 -----
析构函数被调用: Hello
析构函数被调用: Hello
析构函数被调用: Hello
析构函数被调用: Hello
关键要点
- 深拷贝与浅拷贝:默认的拷贝行为是浅拷贝(按位复制),对于管理资源的类(如动态内存、文件句柄等)是致命的,会导致重复释放等问题。自定义这两个函数通常是为了实现深拷贝,即不仅复制指针,还复制指针所指向的资源。
- 拷贝赋值运算符的注意事项:
- 自赋值检查 (
if (this == &other)): 防止a = a这样的操作导致先释放资源再访问已释放资源的问题。 - 先释放再分配:必须先安全地释放当前对象拥有的旧资源,然后再分配新资源并拷贝。错误的顺序可能导致内存泄漏。
- 自赋值检查 (
- Rule of Three(三法则):如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,那么它通常也需要全部这三个。这是因为它们都与管理资源相关。
现代C++补充 (Rule of Five):在C++11及以后,由于移动语义的引入,法则扩展为“五法则”,还需要考虑移动构造函数和移动赋值运算符。
一句话概括区别:
拷贝构造函数是“出生证明”,它负责创建一个和原对象一模一样的新对象。
拷贝赋值运算符是“换血手术”,它负责把一个已存在对象的内容彻底替换成另一个对象的内容。
浙公网安备 33010602011771号