当前位置: 首页 > news >正文

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

目录

一、一个容易混淆的问题

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

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

为什么参数是const引用?

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

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

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

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

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

八、三个必踩的坑

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

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

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指针就是那个“暗号”,它指向当前对象自己。下篇揭秘。

http://www.jsqmd.com/news/800030/

相关文章:

  • Spring Boot全栈项目架构解析:从分层设计到容器化部署
  • 生命体AI产品有什么特点
  • 无人机雷达穿透植被监测土壤湿度技术解析
  • 2026新疆靠谱变频器厂家精选:变频器厂家推荐本地生产/售后无忧 - 栗子测评
  • Antigravity技能目录:从信息过载到技能发现的探索引擎
  • 陈,脑切片模具 大鼠脑切片模具 小鼠脑切片模具
  • 腾讯位置服务开发者征文大赛:“独行侠”智能路线官
  • 功能开关与远程配置:现代Web应用安全发布与动态控制实践
  • 防爆风机哪家好?2026高温风机厂家推荐:离心风机/高压风机生产厂家+防腐风机厂家合集 - 栗子测评
  • 别再乱写SDC了!ICC II里Mode、Corner、Scenario约束文件分离的实战技巧与内存优化
  • IrDA OBEX文件传输技术解析与Microchip实现
  • 热电模块技术原理与PCR温度控制应用
  • selection.js:简化DOM文本选区管理的轻量级JavaScript库
  • 轻量级GraphRAG实现:nano-graphrag核心原理与定制指南
  • Viterbi 算法直接用在中文分词上
  • 别再乱调了!大漠模块SetKeypadDelay/SetMouseDelay参数详解与实战避坑(易语言)
  • 第二章-05-目录切换相关命令(cd/pwd)-课后练习
  • Gemini辅助写周报/月报:从零散记录到结构化汇报的提效方法.
  • 3大维度重构游戏体验:DOL汉化美化整合包全指南
  • 2026 Git 高频面试攻坚:从底层原理到企业级救火(进阶实战版)
  • 嵌入式软件架构一:一个能让人放心接手的嵌入式项目,骨架长什么样
  • MinerU 实战训练营:RAG 数据预处理的最后一块拼图
  • 阿里:时序课程解决多轮蒸馏不稳定
  • 手把手调SVPWM:如何根据你的直流母线电压Udc设置正确的调制比不炸管?
  • 从关中到汉中:用Python+DEM数据,分析古代行军路线的地理可行性
  • Awesome List自动化生成:从手工整理到工业化生产的效率革命
  • 健身直播必备:手表心率如何实时显示在手机拍摄画面上?
  • YOLO26引入Dual-ViT自注意力:局部与全局两条主线的完美交汇
  • 基于Agent-Next框架的Polymarket预测市场模拟交易系统构建指南
  • 告别重复劳动:手把手教你用SAP LSMW为MM模块创建第一个数据导入程序