拷贝赋值运算符 是一个特殊的运算符重载函数,用于将一个已存在对象的值赋予另一个已存在的对象。通俗地说,它规定了当使用 obj2 = obj1; 时,应该如何用 obj1 的内容来覆盖 obj2 的内容。
语法形式
拷贝赋值运算符的典型声明形式如下:
cpp
class MyClass {
public:
// 拷贝赋值运算符
MyClass& operator=(const MyClass& other); // 最常见的形式
};
它有以下几个关键特点:
-
函数名是
operator=。 -
参数通常是对本类类型的常量引用,即
const ClassName&。 -
返回值通常是对当前对象(
*this)的引用。这主要是为了支持链式赋值,例如a = b = c;。 -
它是一个成员函数。
什么时候会被调用?
拷贝赋值运算符在以下场景中被调用:
cpp
MyClass obj1, obj2, obj3; obj1 = obj2; // 调用拷贝赋值运算符 (场景1:直接赋值) obj1 = obj2 = obj3; // 调用拷贝赋值运算符 (场景2:链式赋值)
关键区别:拷贝构造函数 vs. 拷贝赋值运算符
这是最容易混淆的地方,请务必理解:
| 拷贝构造函数 | 拷贝赋值运算符 | |
|---|---|---|
| 目的 | 创建一个新对象,并用另一个对象初始化它。 | 将已存在的对象的值赋给另一个已存在的对象。 |
| 调用时机 | MyClass obj2(obj1);MyClass obj2 = obj1; (初始化!) | obj2 = obj1; (赋值!) |
| 目标对象状态 | 目标对象正在被创建,之前不存在。 | 目标对象已经存在,可能持有需要管理的资源。 |
默认拷贝赋值运算符
如果你没有在自己的类中定义拷贝赋值运算符,编译器会自动为你生成一个。
和默认拷贝构造函数一样,这个默认的拷贝赋值运算符执行的是 “浅拷贝” 或 “按成员赋值”:
-
对于基本数据类型,直接复制其值。
-
对于类成员对象,会调用该成员对象自身的拷贝赋值运算符。
为什么需要自定义拷贝赋值运算符?
当类中含有动态分配的资源(如指针指向堆内存) 时,默认的浅拷贝赋值会带来两个严重问题:
-
内存泄漏:目标对象原有的资源没有被释放。
-
悬空指针/双重释放:两个对象的指针指向了同一块内存。
问题示例:浅拷贝赋值的陷阱
cpp
#include <iostream>
#include <cstring>
class BadString {
private:
char* m_data;
int m_size;
public:
// 普通构造函数
BadString(const char* str = "") {
m_size = strlen(str);
m_data = new char[m_size + 1];
strcpy(m_data, str);
}
// 析构函数
~BadString() {
delete[] m_data;
}
// 注意:这里没有定义拷贝赋值运算符!
// 编译器会生成一个默认的(浅拷贝)版本。
};
int main() {
BadString str1("Hello");
BadString str2("World");
str2 = str1; // 灾难的开始!浅拷贝赋值
// 此时,str2.m_data 原来的内存("World")没有被释放 -> 内存泄漏
// 同时,str1.m_data 和 str2.m_data 指向了同一块内存("Hello")
return 0;
} // 离开作用域时:
// 1. str2 被析构,释放了 "Hello" 的内存。
// 2. str1 被析构,尝试再次释放同一块 "Hello" 的内存 -> 程序崩溃(双重释放)
解决方案:自定义拷贝赋值运算符
一个正确、安全的拷贝赋值运算符通常需要完成以下步骤:
-
自我赋值检查:防止
a = a;这种操作导致资源被意外释放。 -
释放自身原有资源:避免内存泄漏。
-
分配新资源并复制数据:进行深拷贝。
-
返回
*this:以支持链式赋值。
cpp
class GoodString {
private:
char* m_data;
int m_size;
public:
// ... 构造函数、析构函数与之前相同 ...
// 【核心】自定义拷贝赋值运算符
GoodString& operator=(const GoodString& other) {
// 1. 自我赋值检查 (非常重要!)
if (this == &other) {
return *this; // 如果是自己给自己赋值,直接返回
}
// 2. 释放自己原有的内存,避免泄漏
delete[] m_data;
// 3. 分配新内存,并复制数据 (深拷贝)
m_size = other.m_size;
m_data = new char[m_size + 1];
strcpy(m_data, other.m_data);
// 4. 返回当前对象的引用,以支持链式赋值
return *this;
}
};
int main() {
GoodString str1("Hello");
GoodString str2("World");
str2 = str1; // 安全!调用自定义的拷贝赋值运算符
str1 = str1; // 安全!自我赋值检查会处理这种情况
// 现在 str1 和 str2 拥有各自独立的内存块
return 0;
}
拷贝赋值运算符的“拷贝-交换”技法
一种更优雅、更安全且能自动提供异常安全性的实现方式是 “拷贝-交换”技法。它通常需要一个能正常工作的拷贝构造函数和一个交换成员函数。
cpp
#include <utility> // for std::swap
class StringWithSwap {
private:
char* m_data;
int m_size;
public:
// ... 其他成员 ...
// 友元交换函数
friend void swap(StringWithSwap& first, StringWithSwap& second) noexcept {
using std::swap;
swap(first.m_data, second.m_data);
swap(first.m_size, second.m_size);
}
// 拷贝赋值运算符 (使用拷贝-交换技法)
StringWithSwap& operator=(StringWithSwap other) { // 注意!这里是传值,不是传引用
// 参数 `other` 是通过拷贝构造函数创建的副本
swap(*this, other); // 将 *this 的内容与副本交换
return *this;
// 当函数返回时,参数 `other` 被析构,会释放掉 *this 原来的资源
}
};
“拷贝-交换”技法的优点:
-
自动处理自我赋值:因为参数是传值,
a = a;会先创建一个临时副本,再交换,是安全的。 -
强异常安全性:如果拷贝构造失败(内存不足抛出异常),不会影响
*this的原始状态。 -
代码复用:无需手动写释放和分配的代码。

2074

被折叠的 条评论
为什么被折叠?



