C++中的赋值运算符重载

在 C++ 中,赋值运算符(=) 是最常用的运算符之一,默认情况下编译器会为类生成一个「浅拷贝赋值运算符」,但当类包含指针、动态内存、文件句柄等资源时,浅拷贝会导致「双重释放」「内存泄漏」等问题,因此需要手动重载赋值运算符,实现深拷贝。

赋值运算符重载是类的核心重载场景,且有严格的语法和语义要求,以下是详细讲解:

一、赋值运算符重载的核心特性

1、必须重载为成员函数:赋值运算符(=)不能重载为全局函数(编译器强制要求),因为左操作数必须是当前类对象;

2、默认浅拷贝的问题:编译器生成的默认赋值运算符仅拷贝成员变量的值(浅拷贝),若成员是指针,会导致两个对象指向同一块内存;

3、返回值为自身引用:支持链式赋值(如 a = b = c);

4、需处理自赋值:避免 a = a 时误释放自身资源;

5、遵循 "释放旧资源 → 分配新资源 → 拷贝数据" 的深拷贝逻辑。

二、重载语法(成员函数)

  1. 基础语法
cpp 复制代码
// 类内声明
类名& operator=(const 类名& other);

// 类外定义
类名& 类名::operator=(const 类名& other) {
    // 1. 处理自赋值(避免释放自身资源)
    if (this == &other) {
        return *this;
    }

    // 2. 释放当前对象的旧资源(避免内存泄漏)
    // 例如:delete[] 指针成员;

    // 3. 分配新资源并拷贝数据(深拷贝)
    // 例如:指针成员 = new 类型[other.长度];
    //      拷贝 other 的数据到新内存

    // 4. 返回自身引用(支持链式赋值)
    return *this;
}
  1. 关键说明
    const 类名& other:用 const 保证不修改源对象,用引用避免拷贝,提升效率;
    自赋值判断:this == &other 是必加逻辑,否则自赋值时会先释放自身资源,导致后续拷贝失败;
    返回 类名&:必须返回自身引用(*this),否则链式赋值(a = b = c)会失效。
    三、核心场景:深拷贝赋值(解决浅拷贝问题)
    以包含动态字符串的 MyString 类为例,对比「默认浅拷贝」和「手动深拷贝赋值」的区别:
    1、问题示例:默认浅拷贝的坑
cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* str; // 动态分配的字符串指针
public:
    // 构造函数
    MyString(const char* s = "") {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }

    // 析构函数(释放动态内存)
    ~MyString() {
        delete[] str; // 浅拷贝时,两个对象会释放同一块内存 → 崩溃
    }

    // 打印字符串
    void print() const {
        cout << str << endl;
    }
};

int main() {
    MyString s1("Hello");
    MyString s2;
    s2 = s1; // 默认浅拷贝:s2.str 和 s1.str 指向同一块内存

    s1.print(); // 正常
    s2.print(); // 正常
    return 0; // 析构时:s2先释放内存,s1再释放 → 双重释放,程序崩溃
}
  1. 解决:重载赋值运算符(深拷贝)
cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* str;
    int len; // 记录字符串长度,避免重复调用strlen
public:
    // 构造函数
    MyString(const char* s = "") {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    // 析构函数
    ~MyString() {
        delete[] str;
    }

    // 拷贝构造(配合赋值重载,完整深拷贝)
    MyString(const MyString& other) {
        len = other.len;
        str = new char[len + 1];
        strcpy(str, other.str);
    }

    // 重载赋值运算符(深拷贝)
    MyString& operator=(const MyString& other) {
        // 1. 处理自赋值
        if (this == &other) {
            return *this;
        }

        // 2. 释放当前对象的旧资源
        delete[] str;

        // 3. 深拷贝:分配新内存 + 拷贝数据
        len = other.len;
        str = new char[len + 1];
        strcpy(str, other.str);

        // 4. 返回自身引用
        return *this;
    }

    void print() const {
        cout << str << endl;
    }
};

int main() {
    MyString s1("Hello"), s2("World");
    s2 = s1; // 调用重载的赋值运算符,深拷贝

    s1.print(); // Hello
    s2.print(); // Hello

    MyString s3;
    s3 = s2 = s1; // 链式赋值(支持)
    s3.print(); // Hello

    return 0; // 析构正常,无双重释放
}

四、进阶优化:拷贝并交换(Copy-and-Swap) Idiom

这是赋值运算符重载的经典优化写法,将「深拷贝」和「异常安全」结合,代码更简洁、健壮:

  1. 核心思路
    先写一个 swap 成员函数(交换两个对象的资源);
    赋值运算符接收「值传递」的参数(自动触发拷贝构造),再交换当前对象和临时对象的资源;
    临时对象析构时自动释放旧资源,无需手动处理。
  2. 示例实现
cpp 复制代码
#include <iostream>
#include <cstring>
#include <algorithm> // swap函数(可选)
using namespace std;

class MyString {
private:
    char* str;
    int len;

    // 私有swap函数:交换两个对象的资源
    void swap(MyString& other) {
        // 交换指针和长度(无动态内存操作,高效)
        std::swap(this->str, other.str);
        std::swap(this->len, other.len);
    }

public:
    MyString(const char* s = "") {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    // 拷贝构造
    MyString(const MyString& other) {
        len = other.len;
        str = new char[len + 1];
        strcpy(str, other.str);
    }

    // 析构函数
    ~MyString() {
        delete[] str;
    }

    // 拷贝并交换版赋值运算符
    MyString& operator=(MyString other) { // 值传递,自动拷贝
        this->swap(other); // 交换当前对象和临时对象的资源
        return *this; // 临时对象析构时释放旧资源
    }

    void print() const {
        cout << str << endl;
    }
};

int main() {
    MyString s1("Copy-and-Swap"), s2;
    s2 = s1;
    s2.print(); // Copy-and-Swap
    return 0;
}
  1. 优势
    自动处理自赋值:值传递的临时对象是独立的,交换不会影响自身;
    异常安全:拷贝构造若抛出异常(如内存分配失败),当前对象的资源不会被破坏;
    代码简洁:无需手动释放旧资源、判断自赋值,逻辑更清晰。
    五、复合赋值运算符重载(+=、-= 等)
    复合赋值运算符(如 +=)的重载逻辑与 = 类似,但语义是「修改自身」,无需处理深拷贝的 "释放旧资源"(仅需追加数据):
cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* str;
    int len;
public:
    MyString(const char* s = "") {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    ~MyString() {
        delete[] str;
    }

    // 重载 +=(字符串拼接)
    MyString& operator+=(const MyString& other) {
        // 1. 计算新长度
        int newLen = len + other.len;
        // 2. 分配新内存
        char* newStr = new char[newLen + 1];
        // 3. 拷贝原有数据 + 追加新数据
        strcpy(newStr, str);
        strcat(newStr, other.str);
        // 4. 释放旧内存
        delete[] str;
        // 5. 更新成员
        str = newStr;
        len = newLen;
        // 6. 返回自身引用
        return *this;
    }

    void print() const {
        cout << str << endl;
    }
};

int main() {
    MyString s1("Hello"), s2(" World!");
    s1 += s2; // 调用重载的 +=
    s1.print(); // Hello World!
    return 0;
}

六、常见误区与注意事项

1、忘记处理自赋值:

若省略 if (this == &other),自赋值时会先释放自身资源,导致后续拷贝数据时访问已释放的内存,程序崩溃。

2、返回值错误:

若返回 void,无法支持链式赋值(a = b = c 报错);

若返回值(MyString),会生成临时对象,效率低且违反语义(赋值应修改原对象)。

3、浅拷贝未重载:

类包含指针 / 动态内存时,必须重载赋值运算符(和拷贝构造),否则会导致双重释放、内存泄漏。

4、重载为全局函数:

赋值运算符不能重载为全局函数,编译器会直接报错(左操作数必须是类对象,且只能是成员函数)。

5、const 修饰错误:

赋值运算符的参数可加 const(保证不修改源对象),但函数本身不能加 const(因为要修改当前对象)。

七、禁用赋值运算符(可选)

若想禁止类对象的赋值操作(如单例类、包含不可拷贝资源的类),可将赋值运算符声明为 private 且不实现,或 C++11 后用 delete 修饰:

cpp 复制代码
class NoAssign {
private:
    // C++98:声明为private,不实现
    NoAssign& operator=(const NoAssign&);
public:
    NoAssign() {}
};

// C++11 更简洁的方式
class NoAssignC11 {
public:
    NoAssignC11() {}
    // 禁用赋值运算符
    NoAssignC11& operator=(const NoAssignC11&) = delete;
    // 可选:禁用拷贝构造
    NoAssignC11(const NoAssignC11&) = delete;
};

int main() {
    NoAssign a, b;
    // a = b; // 编译错误(private)

    NoAssignC11 c, d;
    // c = d; // 编译错误(delete)
    return 0;
}

总结

相关推荐
superman超哥2 小时前
Rust 基本数据类型:类型安全的底层探索
开发语言·rust·rust基本数据类型·rust底层探索·类型安全
Liu-Eleven2 小时前
Qt/C++开发嵌入式项目日志库选型
开发语言·c++·qt
A24207349302 小时前
深入浅出JS事件:从基础原理到实战进阶全解析
开发语言·前端·javascript
qq_433554542 小时前
C++区间DP
c++·算法·动态规划
烧冻鸡翅QAQ2 小时前
从0开始的游戏编程——开发前的编程语言准备(JAVAScript)
开发语言·javascript·游戏
saber_andlibert2 小时前
【C++转GO】文件操作+协程和管道
开发语言·c++·golang
Halo_tjn2 小时前
Java IO流实现文件操作知识点
java·开发语言·windows·算法
历程里程碑2 小时前
滑动窗口解法:无重复字符最长子串
数据结构·c++·算法·leetcode·职场和发展·eclipse·哈希算法
Geoffwo2 小时前
归一化简单案例
算法·语言模型