[c++]string赋值运算符重载:从“砸碗“到优雅的“换碗仪式“

[c++]string赋值运算符重载:从“砸碗“到优雅的“换碗仪式“

一、为什么需要赋值运算符重载?

        使用编译器默认生成的赋值运算符重载,而这个对象有申请动态分配的资源时,使用编译器默认生成的赋值运算符重载会带来两个问题。

二、浅拷贝的两个致命问题

1.1、同一个空间析构释放两次

        赋值运算符重载中深拷贝用来解决浅拷贝带来的危害的,如果对象有在堆区申请内存,那么赋值运算符重载浅拷贝会造成同一块内存会被释放两次。

1.2、内存泄漏

        内存泄漏,被赋值的那个对象中的_str指向了赋值对象_str指向的空间,而被赋值的对象_str原先的空间没有指针指向了,造成内存泄漏,因此赋值运算符重载对于有动态分配资源的对象进行赋值操作时,需要深拷贝来解决问题。

        形象比喻:在C++中,当类包含动态分配的资源时,默认的赋值操作(浅拷贝)会带来严重问题。想象一下两个人共用一个碗吃饭,当其中一个人决定换碗时,如果处理不当,可能会把另一个人的饭碗也砸了。这就是我们需要自定义赋值运算符重载的原因。

三、赋值运算符重载传统写法:粗暴的"砸碗"操作

        传统写法是自己完成开空间,拷贝内容到新空间,释放旧空间,然后_str指向新空间,并且更新成员变量,也就是_size和_capacity。

1、"砸碗"比喻解析

场景:A = B(赋值操作)

买新碗new char[]创建新内存盛饭memcpy复制B的数据到新内存砸旧碗delete[] _str释放A的旧内存用新碗_str = temp让A指向新内存

自赋值的危险:A = A

A和B其实是同一个人,共用同一个碗买新碗、盛饭都正常砸旧碗时把唯一的碗砸了!后续操作无法进行
string& operator=(const string& s) { if (this != &s) { // 自赋值检查:确保不是自己给自己赋值 char* temp = new char[s._capacity + 1]; // 1. 买新碗 memcpy(temp, s._str, s._size + 1); // 2. 把旧碗的饭盛到新碗 delete[] _str; // 3. 砸旧碗 _str = temp; // 4. 用新碗 _size = s._size; _capacity = s._capacity; } return *this; }

2、赋值运算符重载传统写法-自赋值问题

        我们来看传统写法中自赋值的问题。假设我们有一个字符串对象,我们将其赋值给自己,即 str = str;。在传统写法中,如果没有自赋值检查,代码会这样执行:

分配新内存:char* temp = new char[s._capacity + 1];复制数据:memcpy(temp, s._str, s._size + 1);释放旧内存:delete[] _str;更新指针:_str = temp;

        现在,如果发生自赋值,那么 s 和 *this 是同一个对象。因此,在第3步释放 _str 时,实际上也释放了 s._str,因为它们指向同一块内存。然后,在第2步中,我们试图从已经被释放的内存中复制数据(s._str 现在是一个悬空指针),这会导致未定义行为(通常程序崩溃)。

所以,自赋值检查 if (this != &s) 是为了避免在自赋值时出现先释放再使用的问题。

string& operator=(const string& s) { // 假设没有 if (this != &s) 检查 // 1. 分配新内存 char* temp = new char[s._capacity + 1]; // 分配新空间 // 2. 复制数据:memcpy(temp, s._str, s._size + 1); // 此时 s._str 和 this->_str 指向同一块内存 // 3. 释放旧内存:delete[] _str; // 这里释放了 _str,但 s._str 也指向同一块内存! // 现在 s._str 变成了悬空指针 // 4. 更新指针:_str = temp; return *this; }

四、赋值运算符重载传统写法:优雅的"换碗仪式"

         现代写法是把自己完成开空间,拷贝内容到新空间,释放旧空间,然后_str指向新空间,并且更新_size和_capacity,这些操作交给拷贝构造去做,拷贝构造出一个临时对象,然后再与这个临时对象交换内容,并且让临时对象生命周期结束的时候调用析构函数来清理自己原本的空间,而自己完成了赋值的深拷贝

场景:A = B(赋值操作)

准备阶段(传值参数):厨师用B的配方重新做一份菜(深拷贝临时对象)新菜和原菜食材独立,味道相同交换阶段(swap操作):A说:"我把我的剩菜给你,你把新菜给我"双方优雅交换碗,不破坏任何食物清理阶段(析构临时对象):服务员收走A的剩菜剩饭(临时对象析构)新菜完好留在A的桌上

自赋值安全:A = A

厨师用A的配方做新菜(创建副本)A与新菜交换服务员收走A的旧菜结果:A的菜品保持不变

阶段一:引入拷贝构造辅助

string& operator=(const string& s) { if (this != &s) { string temp(s); // 拷贝构造创建临时对象 std::swap(_str, temp._str); // 交换资源 std::swap(_size, temp._size); std::swap(_capacity, temp._capacity); } return *this; }

阶段二:封装Swap操作

 void swap(string& temp) { std::swap(_str, temp._str); std::swap(_size, temp._size); std::swap(_capacity, temp._capacity); } string& operator=(const string& s) { if (this != &s) { string temp(s);// 厨师做新菜 swap(temp);// 优雅换碗 } return *this; }

阶段三:终极版Copy-and-Swap

        在这个版本中,我们利用了一个事实:参数s是一个临时对象,它是传入对象的副本。我们直接交换当前对象和s,这样当前对象就拥有了传入对象的副本,而s则拥有了当前对象原来的资源。当函数返回时,s超出作用域,析构函数被调用,释放掉原对象资源。

        自赋值的情况:当发生自赋值时,比如 str = str,传值会调用拷贝构造函数,创建一个与str相同的临时对象,然后交换,临时对象被销毁,最终str还是原来的值。虽然效率低一些,但是正确。最终版本中不需要显式检查自赋值,因为自赋值的情况也能正确工作。

        总结:最终版本的赋值运算符重载代码简洁、安全(异常安全、自赋值安全),是推荐的写法。需要注意的是,这种写法依赖于拷贝构造函数的正确实现和析构函数的正确实现。

 void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } string& operator=(string s)// 传值参数:自动创建副本 { swap(s);// 交换资源 return *this;// 临时对象析构清理旧资源 }

Read more

深度优先搜索(DFS)详解及C++实现

深度优先搜索(DFS)详解及C++实现 一、什么是深度优先搜索(DFS)? 深度优先搜索(Depth-First Search,简称DFS)是一种用于遍历或搜索树或图的算法。其核心思想是:尽可能深地搜索图的分支,当某条分支搜索到尽头无法继续前进时,回溯到上一个节点,再选择另一条未探索的分支继续搜索,直到所有节点都被访问完毕。 可以用一个生动的比喻理解DFS:想象你走进一个迷宫,每次遇到岔路时,随机选择一条路一直走,直到走到死胡同(无法继续前进),然后沿原路返回上一个岔路,选择另一条未走过的路继续探索,直到找到出口或遍历完整个迷宫。 DFS的实现通常依赖栈(Stack)这种数据结构(手动实现时),或者直接利用递归函数调用栈(更简洁,也是最常用的方式)。递归实现的本质是将每次的节点访问和回溯过程交给函数栈来管理,无需手动维护栈结构。 二、DFS的核心特性与适用场景 1. 核心特性 * 不撞南墙不回头:优先深入探索当前分支,而非横向遍历同级节点; * 回溯思想:探索到尽头后,返回上一节点继续探索其他分支,需要记录节点访问状态(避免重复访问); * 空间复杂度:取决于

By Ne0inhk
《算法题讲解指南:优选算法-滑动窗口》--13 水果成篮

《算法题讲解指南:优选算法-滑动窗口》--13 水果成篮

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 13 水果成篮 题目链接: 编辑 题目示例: 解法(滑动窗口): 算法思路: 算法流程: C++代码演示:方法一(使用容器) C++代码演示:方法二(用数组模拟哈希表) 算法总结及流程解析: 结束语 13 水果成篮 题目链接: 题目示例: 解法(滑动窗口): 算法思路:       研究的对象是一段连续的区间,可以使用【滑动窗口】思想来解决问题。       让滑动窗口满足:窗口内水果的种类只有两种。       做法:右端水果进入窗口的时候,

By Ne0inhk
【CVPR2025 DEIM】超详细!手把手训练自己的数据集教学:从源码下载,配置虚拟环境,准备数据集、训练、验证、推理测试 ,实现0到1的完整教学过程。本文在win系统上训练,最强实时目标检测算法!

【CVPR2025 DEIM】超详细!手把手训练自己的数据集教学:从源码下载,配置虚拟环境,准备数据集、训练、验证、推理测试 ,实现0到1的完整教学过程。本文在win系统上训练,最强实时目标检测算法!

🔥DEIM创新改进目录:全新DEIM有效涨点改进目录 | 包含各种最新顶会顶刊:卷积模块、注意力模块、特征融合模块、有效特征聚合提取模块,上采样模块、下采样模块,二次创新模块、独家创新,特殊场景检测等最全大论文及小论文必备创新改进点 🔥全新DEIM创新改进专栏地址:全网独家DEIM创新改进高效涨点+永久更新中(至少500+创新改进🗡剑指小论文、大论文)+小白也能简单高效跑实验+容易发各种级别小论文 本文目录 一、下载CVPR2025 DEIM官方源码  二、创新DEIM项目虚拟环境 第一步创建一个自己的虚拟环境: 第二步进入到自己的虚拟环境: 第三步:安装pytorch,建议不要安装太新版本 第四步:直接复制以下所有命令到控制台“终端里面粘贴回车运行” 三、准备自己的数据集和配置自己数据集步骤 3.1 本文以训练Visdrone2019无人机数据集为例 3.2 将自己数据集放到datasets文件夹里 3.3 配置数据步骤 四、使用DEIM训练自己的数据集 4.1

By Ne0inhk
《算法闯关指南:优选算法--前缀和》--31.连续数组,32.矩阵区域和

《算法闯关指南:优选算法--前缀和》--31.连续数组,32.矩阵区域和

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 31. 连续数组 * 解法(前缀和+哈希表): * 算法思路: * C++算法代码: * 算法总结&&笔记展示: * 32. 矩阵区域和 * 解法: * 算法思路: * C++算法代码: * 算法总结&&笔记展示: * 结尾: 前言: 聚焦算法题实战,系统讲解三大核心板块:优选算法:剖析动态规划、二分法等高效策略,学会寻找“最优解”。 递归与回溯:掌握问题分解与状态回退,攻克组合、排列等难题。 贪心算法:理解“

By Ne0inhk