赋值与移动运算符重载
定义与作用
赋值运算符(=)和移动赋值运算符是类资源管理的核心。正确的赋值实现必须处理三个关键问题:
- 自赋值安全:
a = a时不会破坏自身状态 - 异常安全:操作失败时对象仍处于有效状态
- 资源管理:旧资源正确释放,新资源正确接管
MyClass& operator=(const MyClass& other); // 拷贝赋值
MyClass& operator=(MyClass&& other); // 移动赋值(C++11)
核心原理
copy-and-swap 惯用法
赋值运算符的实现层次
完整示例
示例一:动态数组类的完整赋值体系
场景说明:手写动态数组类,展示拷贝赋值、移动赋值和 copy-and-swap。
#include <iostream>
#include <algorithm>
#include <utility>
template <typename T>
class DynamicArray {
public:
DynamicArray() : data_(nullptr), size_(0), capacity_(0) {}
explicit DynamicArray(size_t size)
: data_(new T[size]{})
, size_(size)
, capacity_(size) {}
DynamicArray(const DynamicArray& other)
: data_(new T[other.capacity_])
, size_(other.size_)
, capacity_(other.capacity_) {
std::copy(other.data_, other.data_ + other.size_, data_);
std::cout << " [拷贝构造] size=" << size_ << std::endl;
}
DynamicArray(DynamicArray&& other) noexcept
: data_(other.data_)
, size_(other.size_)
, capacity_(other.capacity_) {
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
std::cout << " [移动构造] size=" << size_ << std::endl;
}
~DynamicArray() {
delete[] data_;
std::cout << " [析构]" << std::endl;
}
// ====== 拷贝赋值(逐个成员实现,需处理自赋值) ======
// 方案 A:传统写法
/*
DynamicArray& operator=(const DynamicArray& other) {
if (this != &other) { // 自赋值检查
T* new_data = new T[other.capacity_];
std::copy(other.data_, other.data_ + other.size_, new_data);
delete[] data_;
data_ = new_data;
size_ = other.size_;
capacity_ = other.capacity_;
}
return *this;
}
*/
// ====== 拷贝赋值(copy-and-swap 惯用法,更优雅) ======
DynamicArray& operator=(const DynamicArray& other) {
DynamicArray temp(other); // 拷贝构造(可能抛异常)
swap(temp); // 交换(不抛异常)
std::cout << " [拷贝赋值]" << std::endl;
return *this;
}
// ====== 移动赋值 ======
DynamicArray& operator=(DynamicArray&& other) noexcept {
if (this != &other) {
delete[] data_; // 释放自己的旧资源
data_ = other.data_; // 接管对方的资源
size_ = other.size_;
capacity_ = other.capacity_;
other.data_ = nullptr; // 对方置空
other.size_ = 0;
other.capacity_ = 0;
}
std::cout << " [移动赋值]" << std::endl;
return *this;
}
// ====== 统一赋值运算符(copy-and-swap + 按值传参) ======
// 同时处理拷贝和移动!按值传参:左值→拷贝,右值→移动
/*
DynamicArray& operator=(DynamicArray other) noexcept {
swap(other);
return *this;
}
*/
void swap(DynamicArray& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
std::swap(capacity_, other.capacity_);
}
void push_back(const T& value) {
if (size_ >= capacity_) {
size_t new_cap = capacity_ == 0 ? 1 : capacity_ * 2;
T* new_data = new T[new_cap];
std::copy(data_, data_ + size_, new_data);
delete[] data_;
data_ = new_data;
capacity_ = new_cap;
}
data_[size_++] = value;
}
size_t size() const { return size_; }
T& operator[](size_t index) { return data_[index]; }
const T& operator[](size_t index) const { return data_[index]; }
void print() const {
std::cout << "[";
for (size_t i = 0; i < size_; ++i) {
if (i > 0) std::cout << ", ";
std::cout << data_[i];
}
std::cout << "] (size=" << size_ << ")" << std::endl;
}
private:
T* data_;
size_t size_;
size_t capacity_;
};
int main() {
std::cout << "===== 构造 =====" << std::endl;
DynamicArray<int> arr1;
arr1.push_back(1);
arr1.push_back(2);
arr1.push_back(3);
std::cout << "arr1: ";
arr1.print();
std::cout << "\n===== 拷贝赋值 =====" << std::endl;
DynamicArray<int> arr2;
arr2 = arr1; // 拷贝赋值
std::cout << "arr2: ";
arr2.print();
std::cout << "\n===== 移动赋值 =====" << std::endl;
DynamicArray<int> arr3;
arr3 = std::move(arr1); // 移动赋值
std::cout << "arr3: ";
arr3.print();
std::cout << "arr1 (移动后): ";
arr1.print(); // 应为空
std::cout << "\n===== 自赋值安全 =====" << std::endl;
arr2 = arr2; // 自我赋值——copy-and-swap 天然安全
std::cout << "arr2: ";
arr2.print();
return 0;
}
预期输出:
===== 构造 =====
arr1: [1, 2, 3] (size=3)
===== 拷贝赋值 =====
[拷贝构造] size=3
[拷贝赋值]
[析构]
arr2: [1, 2, 3] (size=3)
===== 移动赋值 =====
[移动赋值]
arr3: [1, 2, 3] (size=3)
arr1 (移动后): [] (size=0)
===== 自赋值安全 =====
[拷贝构造] size=3
[拷贝赋值]
[析构]
arr2: [1, 2, 3] (size=3)
逐段分析:
- 拷贝赋值(copy-and-swap):创建
other的副本temp→ 与*this交换 →temp析构带走旧资源。即使拷贝构造抛异常,*this仍保持原状态——强异常安全 - 移动赋值:先释放自己的旧资源,再接管对方的资源。移动操作标记
noexcept,让 STL 容器放心使用 - copy-and-swap 对自赋值天然安全:
temp(other)创建独立的副本,swap 自己和自己也没问题 - 注释中的统一赋值运算符:
operator=(DynamicArray other)按值传参,左值时自动拷贝、右值时自动移动——一个函数处理两种赋值。但略有额外的移动构造开销
示例二:银行账户的赋值操作
#include <iostream>
#include <string>
class BankAccount {
public:
BankAccount(std::string owner, double balance)
: owner_(std::move(owner)), balance_(balance) {
std::cout << " 创建账户: " << owner_ << " (¥" << balance_ << ")" << std::endl;
}
// 拷贝构造
BankAccount(const BankAccount& other)
: owner_(other.owner_ + " (副本)") // 账户名不能重复
, balance_(0.0) { // 余额不复制
std::cout << " [拷贝] 创建副本账户: " << owner_ << std::endl;
}
// 移动构造
BankAccount(BankAccount&& other) noexcept
: owner_(std::move(other.owner_))
, balance_(other.balance_) {
other.balance_ = -1; // 标记为无效
std::cout << " [移动] 所有权转移: " << owner_ << std::endl;
}
// 拷贝赋值
BankAccount& operator=(const BankAccount& other) {
if (this != &other) {
std::cout << " 拷贝账户信息(但余额不复制)——" << other.owner_ << std::endl;
owner_ = other.owner_ + " (副本)";
// balance_ 保持不变——每个账户的余额独立
}
return *this;
}
// 移动赋值
BankAccount& operator=(BankAccount&& other) noexcept {
if (this != &other) {
std::cout << " 转移所有权: " << other.owner_ << " → " << owner_ << std::endl;
owner_ = std::move(other.owner_);
balance_ = other.balance_;
other.balance_ = -1;
}
return *this;
}
void deposit(double amount) { balance_ += amount; }
void print() const {
std::cout << " " << owner_ << ": ¥" << balance_ << std::endl;
}
private:
std::string owner_;
double balance_;
};
int main() {
std::cout << "===== 银行账户 =====" << std::endl;
BankAccount alice("张三", 1000.0);
BankAccount bob("李四", 500.0);
std::cout << "\n--- 拷贝赋值 ---" << std::endl;
alice = bob;
alice.deposit(200.0); // alice 的余额独立于 bob
alice.print();
bob.print();
std::cout << "\n--- 移动赋值 ---" << std::endl;
BankAccount temp("临时账户", 999.0);
bob = std::move(temp);
bob.print();
temp.print(); // 余额为 -1(已转移)
std::cout << "\n--- 自赋值检查 ---" << std::endl;
alice = alice; // 安全
alice.print();
return 0;
}
预期输出:
===== 银行账户 =====
创建账户: 张三 (¥1000)
创建账户: 李四 (¥500)
--- 拷贝赋值 ---
拷贝账户信息(但余额不复制)——李四
张三 (副本): ¥1000
李四: ¥500
--- 移动赋值 ---
创建账户: 临时账户 (¥999)
转移所有权: 临时账户 → 李四
临时账户: ¥999
临时账户: ¥-1
--- 自赋值检查 ---
张三 (副本): ¥1000
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 赋值未处理自赋值 | 释放后使用已释放内存 | if (this != &rhs) 或用 copy-and-swap |
| 移动赋值未释放旧资源 | 旧资源泄漏 | 先 delete[] 再接管 |
| 移动赋值后源对象未重置 | 悬垂指针 | 将源对象置空(data=nullptr 等) |
| 移动赋值不是 noexcept | STL 容器回退到拷贝 | 移动赋值必须标记 noexcept |
| copy-and-swap 中 swap 抛异常 | 违背强异常保证 | swap 必须 noexcept |
常见面试问题
copy-and-swap 惯用法是什么?有什么优点?——赋值运算符中:创建参数副本 → swap 交换 → 副本析构。优点:自赋值安全、强异常安全、代码简洁。
移动赋值和拷贝赋值的区别?——拷贝赋值创建参数副本并交换;移动赋值直接接管参数资源(不拷贝),源对象置空。移动赋值必须 noexcept。
传统赋值实现和 copy-and-swap 的区别?——传统写法需手动检查自赋值、先分配再释放(异常时可能泄漏)。copy-and-swap 通过 swap 一次性替换,天然安全。
统一赋值运算符
operator=(T other)的原理?——参数按值传递,左值参数触发拷贝构造,右值参数触发移动构造。函数体内只需 swap。代价是多一次移动构造/拷贝构造。为什么移动赋值/移动构造要标记
noexcept?——STL 容器在扩容时,如果移动操作不是 noexcept,会回退到拷贝操作以保证异常安全。标记 noexcept 让容器放心使用移动语义。
小结
- copy-and-swap 是拷贝赋值的最佳实践:拷贝参数 → swap → 析构旧值
- 移动赋值先释放旧资源,再接管新资源,源对象置空
- 自赋值检查:传统写法手动检查,copy-and-swap 天然安全
- 移动赋值和 swap 必须
noexcept——保证 STL 容器正常使用移动优化 - 默认使用编译器生成的赋值运算符(Rule of Zero)——除非管理裸资源