赋值运算符为什么要避免自赋值?

赋值运算符为什么要避免自赋值?

最近看到的部分代码中,赋值运算符重载总是忘记自赋值的处理,但是自赋值的处理又非常重要,所以我决定写这篇文章。

1. 重要前提

并不是所有的场景下自我赋值都会导致问题。自赋值风险只针对手动资源管理的类/结构体,满足如下条件的类/结构体,自我赋值完全安全:

类成员全部为基础栈类型:int、char、double 等;

使用标准库容器:std::string、std::vector(内部已做安全处理);

使用编译器默认生成的赋值运算符(仅逐成员浅拷贝,无手动释放逻辑)。

虽然这种场景下,自赋值只会自己覆盖自己,数据不变、无崩溃、无资源泄漏。但是也不推荐,毕竟这些成员之间的赋值也是有CPU开销的。

2. 风险原因

前文提到,自赋值风险只针对手动资源管理的类/结构体 。这主要是因为,手动资源管理的类/结构体,其赋值运算符重载的常规逻辑是:先释放旧资源 → 再拷贝新资源,形如如下的代码:

cpp

复制代码

#include

#include

class MyStr {

public:

// 构造函数与析构函数省略

// 传统赋值:先释放旧资源,再拷贝新资源(自赋值时致命)

MyStr& operator=(const MyStr& rhs) {

delete[] data_; // 1. 先释放自身资源

data_ = new char[std::strlen(rhs.data_) + 1]; // 2. 再分配新资源

std::strcpy(data_, rhs.data_); // 3. 拷贝数据(此时 rhs.data_ 已被释放,野指针)

return *this;

}

const char* c_str() const { return data_; }

private:

char* data_; // 管理堆内存

};

正是这样的代码,如果存在MyStr类型的对象发生了自赋值时,赋值函数内 this == &rhs,两个指针指向同一个对象。程序会先执行「释放自身堆内存」的逻辑,提前销毁了自己的数据;后续再读取源对象数据进行拷贝时,原内存已被释放,变成野指针,直接触发程序崩溃。 如上只是以管理内存为例,持有文件句柄、网络套接字等资源的类,也会面临类似问题。

3. 解决方案

3.1 区分自我

通常,最常用的解决方案,是在赋值函数开头增加自我赋值的判断,不允许自我赋值发生。

cpp

复制代码

MyStr& operator=(const MyStr& rhs) {

if (this == &rhs) { // 自赋值拦截

return *this;

}

delete[] data_;

data_ = new char[std::strlen(rhs.data_) + 1];

std::strcpy(data_, rhs.data_);

return *this;

}

3.2 Copy-and-Swap 机制

前文提到自赋值风险是因为先释放旧资源,再拷贝新资源 导致的,只要打破这个顺序,就可以避免风险。Copy-and-Swap 机制,就是利用了这个原理,它先拷贝、再交换、最后释放旧资源 。它从底层架构上天然规避自赋值风险,自带异常安全特性,还无需人工判断。 所谓的先拷贝、再交换、最后释放旧资源,借助临时对象,将所有风险操作(内存分配、数据拷贝)转移到临时对象,原对象全程保持安全,不会被篡改、不会提前释放资源。实现写法如下:

cpp

复制代码

#include

#include

class MyStr {

public:

MyStr(const char* s = "") {

data_ = new char[std::strlen(s) + 1];

std::strcpy(data_, s);

}

// 拷贝构造:用于生成临时对象

MyStr(const MyStr& other) {

data_ = new char[std::strlen(other.data_) + 1];

std::strcpy(data_, other.data_);

}

~MyStr() { delete[] data_; }

// 交换内部资源,noexcept 保证不抛异常

void swap(MyStr& other) noexcept {

std::swap(data_, other.data_);

}

// 范式一:const 引用传参 + 手动临时对象

MyStr& operator=(const MyStr& rhs) {

MyStr(rhs).swap(*this); // 拷贝出临时对象,交换后旧资源随临时对象析构

return *this;

}

// 范式二:值传递自动生成临时对象(与范式一等价,二选一即可)

// MyStr& operator=(MyStr rhs) noexcept {

// swap(rhs);

// return *this;

// }

private:

char* data_; // 管理堆内存

};

如上代码中,范式一主动构建临时对象,范式二借助值传递自动生成临时对象。二者等价,二选一即可。

4. 总结

自我赋值的风险不在于语法违规,而在于传统赋值「先释放、后拷贝」的资源逻辑,会导致自赋值时数据提前销毁、程序崩溃。简单栈对象的自赋值是安全的,但手动管理堆资源的自定义类必须严防自赋值。传统的 if 判断只能解决自赋值崩溃,无法解决异常安全问题,工程中不推荐;Copy-and-Swap 是工业级标准解法,通过临时对象隔离风险,同时实现自赋值安全与异常安全,是资源类赋值运算符的推荐实现。

相关作品

大雁塔在哪里
日博365官网

大雁塔在哪里

📅 08-30 👀 3379
美瞳取不下来怎么办
日博365官网

美瞳取不下来怎么办

📅 12-02 👀 8810
揭秘9月29日出生者的星座,神秘而独特的天秤座
约彩365彩票官方app下载安卓

揭秘9月29日出生者的星座,神秘而独特的天秤座

📅 01-21 👀 750