【c++面向对象编程】第5篇:类与对象(四):赋值运算符重载

目录

一、一个容易混淆的问题

二、赋值运算符的基本写法

为什么返回值是SafeArray&(引用)?

为什么参数是const引用?

三、自赋值检查:为什么重要?

四、完整例子:带赋值运算符的SafeArray

[五、拷贝构造 vs 赋值运算符:对比总结](#五、拷贝构造 vs 赋值运算符:对比总结)

六、另一种实现技巧:copy-and-swap

七、三法则升级为五法则(C++11)

八、三个必踩的坑

[1. 忘了写赋值运算符,用了默认的浅拷贝](#1. 忘了写赋值运算符,用了默认的浅拷贝)

[2. 赋值运算符没有返回引用](#2. 赋值运算符没有返回引用)

[3. 自赋值检查写错了](#3. 自赋值检查写错了)

九、这一篇的收获


一、一个容易混淆的问题

看下面这段代码,猜猜哪行调用了拷贝构造,哪行调用了赋值?

cpp

复制代码
SafeArray a(5);
SafeArray b(3);   // b已经存在
SafeArray c = a;  // 第1行
b = a;            // 第2行
  • 第1行c正在被创建,用a初始化它 → 拷贝构造函数

  • 第2行b已经存在,把a的值赋给它 → 赋值运算符

区别很明确:

  • 左边对象还没出生 → 拷贝构造

  • 左边对象已经活着 → 赋值运算符

这就是为什么两个都要实现------它们服务于不同的场景。


二、赋值运算符的基本写法

赋值运算符的函数名是operator=,它的基本框架如下:

cpp

复制代码
class SafeArray {
public:
    // 赋值运算符重载
    SafeArray& operator=(const SafeArray& other) {
        // 1. 自赋值检查
        if (this == &other) {
            return *this;
        }
        
        // 2. 释放当前资源
        delete[] data;
        
        // 3. 分配新资源并复制内容
        size = other.size;
        data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }
        
        // 4. 返回自身引用
        return *this;
    }
    
private:
    int* data;
    int size;
};

为什么返回值是SafeArray&(引用)?

因为C++支持连续赋值:

cpp

复制代码
a = b = c;   // 等价于 a.operator=(b.operator=(c))

b = c应该返回b本身,然后作为参数传给a的赋值运算符。如果返回的不是引用,而是一个副本,就会多一次不必要的拷贝,而且行为不符合预期。

cpp

复制代码
// 错误的写法
SafeArray operator=(const SafeArray& other);  // 返回值不是引用

a = b = c;  // 会有额外拷贝,效率低且可能出错

为什么参数是const引用?

  • const:我们不应该修改右边的对象

  • 引用:避免拷贝(否则会递归调用拷贝构造)


三、自赋值检查:为什么重要?

先看一个没有自赋值检查的例子:

cpp

复制代码
SafeArray& operator=(const SafeArray& other) {
    delete[] data;                    // 释放自己
    data = new int[other.size];       // 重新分配
    // ...
}

如果写成a = a(自己给自己赋值):

  1. delete[] data --- 释放了a的内存

  2. 然后new int[other.size] --- 但other就是a本身,它的data已经被释放了

  3. 访问other.data未定义行为(大概率崩溃)

加上自赋值检查后:

cpp

复制代码
if (this == &other) {
    return *this;   // 啥也不干,直接返回
}

注意:比较的是地址 ,不是内容。this是当前对象的地址,&other是参数对象的地址。指向同一个对象时,地址相同。


四、完整例子:带赋值运算符的SafeArray

cpp

复制代码
#include <iostream>
#include <cstring>
using namespace std;

class SafeArray {
private:
    int* data;
    int size;
    
public:
    // 普通构造函数
    SafeArray(int n = 0) : size(n) {
        data = (n > 0) ? new int[n] : nullptr;
        for (int i = 0; i < size; i++) {
            data[i] = 0;
        }
        cout << "构造:size=" << size << endl;
    }
    
    // 拷贝构造函数
    SafeArray(const SafeArray& other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }
        cout << "拷贝构造:深拷贝了" << size << "个int" << endl;
    }
    
    // 赋值运算符重载
    SafeArray& operator=(const SafeArray& other) {
        cout << "赋值运算符被调用" << endl;
        
        // 1. 自赋值检查
        if (this == &other) {
            cout << "自赋值检查:啥也不干" << endl;
            return *this;
        }
        
        // 2. 释放当前资源
        delete[] data;
        
        // 3. 分配新资源并复制
        size = other.size;
        if (size > 0) {
            data = new int[size];
            for (int i = 0; i < size; i++) {
                data[i] = other.data[i];
            }
        } else {
            data = nullptr;
        }
        
        return *this;
    }
    
    // 析构函数
    ~SafeArray() {
        delete[] data;
        cout << "析构:释放了" << size << "个int" << endl;
    }
    
    void set(int idx, int val) {
        if (idx >= 0 && idx < size) data[idx] = val;
    }
    
    int get(int idx) const {
        if (idx >= 0 && idx < size) return data[idx];
        return -1;
    }
    
    void print() const {
        cout << "[";
        for (int i = 0; i < size; i++) {
            cout << data[i] << (i < size-1 ? ", " : "");
        }
        cout << "]" << endl;
    }
};

int main() {
    SafeArray a(5);
    for (int i = 0; i < 5; i++) a.set(i, i * 10);
    a.print();  // [0, 10, 20, 30, 40]
    
    SafeArray b(3);
    b.set(0, 100);
    b.print();  // [100, 0, 0]
    
    cout << "\n--- 执行 b = a ---" << endl;
    b = a;      // 赋值运算符
    b.print();  // [0, 10, 20, 30, 40]
    
    cout << "\n--- 执行 a = a (自赋值) ---" << endl;
    a = a;      // 自赋值,应该安全地什么都不做
    
    cout << "\n--- 程序结束 ---" << endl;
    return 0;
}

运行结果:

text

复制代码
构造:size=5
[0, 10, 20, 30, 40]
构造:size=3
[100, 0, 0]

--- 执行 b = a ---
赋值运算符被调用
[0, 10, 20, 30, 40]

--- 执行 a = a (自赋值) ---
赋值运算符被调用
自赋值检查:啥也不干

--- 程序结束 ---
析构:释放了5个int
析构:释放了5个int

五、拷贝构造 vs 赋值运算符:对比总结

对比维度 拷贝构造函数 赋值运算符
函数名 类名(const 类名&) operator=(const 类名&)
调用时机 创建新对象时 对象已存在,重新赋值
释放旧资源 不需要(对象是全新的) 必须释放当前资源
自赋值风险 没有风险(新对象无旧资源) 有风险,必须检查
返回值 无返回值 返回自身引用

一个记忆技巧

  • 拷贝构造:建新屋,搬家具(以前没有房子)

  • 赋值:换家具,先清空(房子已经有了)


六、另一种实现技巧:copy-and-swap

上面的实现有个潜在问题:如果new int[size]失败(内存不足抛异常),对象已经delete[] data了,数据丢失,陷入无效状态。

更健壮的写法是"先构造副本,再交换":

cpp

复制代码
SafeArray& operator=(const SafeArray& other) {
    if (this != &other) {
        SafeArray temp(other);      // 先拷贝一份(调用拷贝构造)
        swap(temp);                 // 交换资源
    }
    return *this;
}

void swap(SafeArray& other) {
    std::swap(data, other.data);
    std::swap(size, other.size);
}

这种写法的好处:

  • 强异常安全:如果分配内存失败,原对象保持不变

  • 代码复用:拷贝逻辑直接用拷贝构造

  • 自动处理自赋值 :自赋值时temp是自身的副本,交换后等于没变


七、三法则升级为五法则(C++11)

之前提到三法则 (析构、拷贝构造、拷贝赋值)。C++11引入了移动语义,扩展成了五法则

函数 作用
析构函数 释放资源
拷贝构造函数 深拷贝
拷贝赋值运算符 深赋值
移动构造函数 转移资源(下一篇讲)
移动赋值运算符 转移资源(下一篇讲)

对于管理资源的类,这五个函数通常需要一起考虑。


八、三个必踩的坑

1. 忘了写赋值运算符,用了默认的浅拷贝

cpp

复制代码
class Bad {
    int* p;
public:
    Bad() { p = new int(5); }
    ~Bad() { delete p; }
    // 没有写赋值运算符 → 浅拷贝
};

Bad a, b;
b = a;   // b.p = a.p → 两个指针指向同一块内存
// 析构时重复释放 → 崩溃

2. 赋值运算符没有返回引用

cpp

复制代码
void operator=(const SafeArray& other)  // ❌ 返回值void

后果:

  • 连续赋值a = b = c编译失败

  • 语义不符合C++惯例

3. 自赋值检查写错了

cpp

复制代码
if (*this == other)  // ❌ 比较内容,不是地址

如果两个对象内容相同但地址不同,会错误地跳过赋值,导致该复制的内容没复制。

正确写法:

cpp

复制代码
if (this == &other)  // ✅ 比较地址

九、这一篇的收获

你现在应该能够:

  • 区分拷贝构造和赋值运算符的调用时机

  • 写出标准形式的operator=

  • 理解为什么需要自赋值检查

  • 知道赋值运算符必须返回*this的引用

  • 了解copy-and-swap技术能提供异常安全

💡 小作业:为上一讲的StringWrapper类添加赋值运算符。注意处理自赋值,确保深拷贝正确。然后测试这三种情况:

  1. s1 = s2 (正常赋值)

  2. s1 = s1 (自赋值)

  3. s1 = s2 = s3 (连续赋值)


下一篇预告 :第6篇《this指针:对象如何知道自己在调用谁?》------成员函数里访问成员变量时,编译器怎么知道是哪个对象的变量?this指针就是那个"暗号",它指向当前对象自己。下篇揭秘。

相关推荐
Moment1 小时前
从 beginWork 到 completeWork,Fiber 树是怎么“盖”出来的❓❓❓
前端·javascript·面试
Java面试题总结1 小时前
.NET 8 Web开发入门(三):解构引擎——依赖注入(DI)与中间件管道
前端·中间件·.net
样例过了就是过了1 小时前
LeetCode热题100 颜色分类
c++·算法·leetcode
ZPC82101 小时前
C++ 跨平台 UDP 收发测试程序
c++·算法·机器人
不会写DN1 小时前
为什么需要 @types/react? 解决“无法找到模块 react 的声明文件”报错
前端·react.js·前端框架
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第45题】【JVM篇】第5题:JVM中,对象何时会进入老年代?
java·开发语言·jvm·后端·面试
右耳朵猫AI1 小时前
React技术周刊 2026年第14周
前端·react.js·前端框架
hanbr1 小时前
C++ 类型转换与异常处理全解析
开发语言·c++
ym_xixi1 小时前
《类和对象》—— 构造函数与析构函数总结
前端·c++·算法