《Effective C++》《构造/析构/赋值运算——11、在operator=中处理“自我赋值”》

文章目录

  • 1、Terms11:Handle assignment to self in operator=
    • 类中自我赋值问题及如何解决
    • 自我赋值问题解决:
    • 异常处理问题解决
    • 使用“copy and swap”技术来处理自我赋值
  • 2、面试相关
    • 2.1 什么是自我赋值?为什么它是个问题?
    • 2.2 在重载赋值操作符时,如何避免自我赋值问题?
    • 2.3 如果不处理自我赋值,可能会发生什么?
    • 2.4 在赋值操作符中,如何处理深拷贝和浅拷贝?
    • 2.5请提供一个示例,展示如何在赋值操作符中正确处理自我赋值和资源管理。
  • 3、总结
  • 4、参考

1、Terms11:Handle assignment to self in operator=

“自我赋值”发生在对象被赋值给自己时:

class Widget{};Widget w;
w = w;  //自我赋值

这看起来有点蠢,但它合法,所以不要认定客户绝不会这样做。此外赋值动作并不总是那么可被一眼辨识出来,例如:

a[i] = a[j];

如果 a 是一个数组,且索引i和j相等,那么也是一个潜在的自我赋值。

*px = *py;

如果这两个指针px和py指向于同一块内存,那么也是一个潜在的自我赋值。
这些并不明显的自我赋值,是“别名”带来的结果:所谓“别名”就是有一个以上的方法(指涉)某对象。

类中自我赋值问题及如何解决

假设你建立一个类Widget,用来保存一个指针指向一块动态分配的位图(bitmap):

class Bitmap { ... };
class Widget {...
private:Bitmap* pb; // 指针,指向一个从 heap 分配而得的对象
};

下面是operator=的实现代码,表面上看起来合理,但自我赋值出现时并不安全(也不具备异常安全性)

class Bitmap {};
class Widget {
public://赋值运算符Widget&Widget::operator=(const Widget& rhs) // 一份不安全的 operator= 实现版本{delete pb;pb = new Bitmap(*rhs.pb); //是使用rhs's bitmap的副本return *this;}
private:Bitmap* pb;		       //指针,指向一个从heap分配而得的对象
};

这里自我赋值的问题是,operator=函数内的 *this (赋值的目的端)和 rhs 有可能是同一个对象,果真如此 delete 就不只是销毁当前对象的 bitmap,它也销毁 rhs 的 bitmap。在函数末尾,Wigdet——它原本不该被自我赋值动作改变的——发现自己持有一个指针指向一个已被删除的对象!
现在来分析这个运算符如果出现自我赋值而产生的错误:

如果参数rhs传入的就是自身,那么当pb被释放之后,下面再次new的时候又将参数(自己)的pb指针所指的内容传入进去,但是pb的内容已经被释放了,因此再次使用到这个对象的时候就会产生不确定的行为。

自我赋值问题解决:

想要阻止这种错误,做法是在赋值运算符最前面的一个 “认同测试” 达到“自我赋值”的检验目的:
  根据上面“自我赋值”而产生的错误,我们应该在赋值运算符函数的第一步判断传入的对象是否为自己,如果为自己的话做相应的处理

Widget& Widget::operator=(const Widget& rhs)
{//判断是否为“自我赋值”//注意,此处的&为取地址if (this == &rhs)//测试return *this;//其他的与上面介绍的一样delete pb;pb = new Bitmap(*rhs.pb); //以参数为副本调用拷贝构造函数重新创建return *this;
}

异常处理问题解决

上面介绍的operator=虽然解决了“自我赋值”检测,但是不是“异常安全的”。例如在new操作符执行时跑出了异常(内存不足或因为Bitmap类的拷贝构造函数抛出异常),最终Widget对象会只有一个一块已被删除的Bitmap,因此代码是不安全的。
我们对上面代码进行优化:

Widget& Widget::operator=(const Widget& rhs)
{Bitmap *pOrig = pb; 	    //记住原先的pbpb = new Bitmap(*rhs.pb);   //以参数为副本让pb重新创建一个对象delete pOrig;  				//删除原先的pbreturn *this;
}

这段代码可以来处理异常:如果new时抛出了异常,此时我们的pb对象还没有删除
  这段代码还可以来处理“自我赋值”:我们对原bitmap做了一份复制、删除原bitmap,然后将pb再指向于复制的那一份。这个虽然不是处理“自我赋值”最高效的办法,但是行得通。
  关于效率:此处为什么我们不在代码最前面进行“对象是否为自己”的检测了:此处我们的代码已经可以处理自我赋值了,如果还添加那种“自我检测”的代码,会使代码增多并多了一个语句判断,会使执行速度降低。
  
可编译的代码示例:

#include <iostream>  class Bitmap {  
public:  // Bitmap的构造函数  Bitmap() {  std::cout << "Bitmap constructor called\n";  // 初始化Bitmap对象的代码(例如分配内存等)  }  // Bitmap的拷贝构造函数  Bitmap(const Bitmap& other) {  std::cout << "Bitmap copy constructor called\n";  // 拷贝初始化Bitmap对象的代码  }  // Bitmap的析构函数  ~Bitmap() {  std::cout << "Bitmap destructor called\n";  // 清理Bitmap对象的代码(例如释放内存等)  }  // 可能还需要其他的成员函数和数据成员  };  class Widget {  
public:  // 构造函数  Widget() : pb(new Bitmap()) {  std::cout << "Widget constructor called\n";  }  // 析构函数  ~Widget() {  std::cout << "Widget destructor called\n";  delete pb; // 释放pb指向的内存  }  // 拷贝构造函数  Widget(const Widget& other) : pb(new Bitmap(*other.pb)) {  std::cout << "Widget copy constructor called\n";  }  // 赋值运算符  Widget& operator=(const Widget& rhs){//检查自我赋值if(this == &rhs)    return *this;Bitmap *pOrig = pb; 	    //记住原先的pbpb = new Bitmap(*rhs.pb);   //以参数为副本让pb重新创建一个对象delete pOrig;               //删除原先的pbreturn *this;}// 可能还需要其他的成员函数和数据成员  const Bitmap* getPb() const {  return pb;  }  private:  Bitmap* pb; // 指向动态分配的Bitmap对象的指针  
};  int main() {  // 创建两个Widget对象  Widget widget1;  Widget widget2;  // 输出当前widget1和widget2的pb指针指向的Bitmap对象的地址  std::cout << "widget1 pb address: " << widget1.getPb() << std::endl;  std::cout << "widget2 pb address: " << widget2.getPb() << std::endl;  // 使用赋值运算符将widget2赋值给widget1  widget1 = widget2;  // 输出赋值后widget1的pb指针指向的Bitmap对象的地址  // 它应该与widget2的pb指针指向的地址不同(因为创建了新的Bitmap对象)  std::cout << "widget1 pb address after assignment: " << widget1.getPb() << std::endl;  // 释放Widget对象时,它们的析构函数会自动释放Bitmap对象  // 因此,我们不需要在main函数的最后手动删除pb  return 0;  
}

输出结果:

Bitmap constructor called
Widget constructor called
Bitmap constructor called
Widget constructor called
widget1 pb address: 0x55f6914baeb0
widget2 pb address: 0x55f6914bb2e0
Bitmap copy constructor called
Bitmap destructor called
widget1 pb address after assignment: 0x55f6914bb300
Widget destructor called
Bitmap destructor called
Widget destructor called
Bitmap destructor called

使用“copy and swap”技术来处理自我赋值

替换上面的所有办法,我们可以使用“copy and swap”技术来解决“自我赋值”以及“异常处理”
  copy and swap技术和“异常安全性”有密切关系,会在条款29详细讲述
手法1:

class Bitmap {};
class Widget {
public:void swap(Widget& rhs); //将参数rhs与*this进行数据交换,详情见条款29Widget& Widget::operator=(const Widget& rhs){Widget temp(rhs);//以函数参数为参数调用Wiget的拷贝构造函数创建一个对象//不能将rhs直接传入swap,因为这样的话会改变=号后面对象的内容,因此上面需要创建一个临时对象tempswap(temp);      //交换参数所指的对象与*thisreturn *this;}
private:Bitmap* pb;
};

手法2:
  实现这种技术的手法还有一种是以“传值调用”operator=,实现如下:
  这种技术手法实现的功能与上面的一样,但是代码没有那么清晰
  但是这种手法将“拷贝”动作从函数体内移动到了“函数参数构造阶段”,因此效率提高了。

class Bitmap {};
class Widget {
public:void swap(Widget& rhs); //将参数rhs与*this进行数据交换//rhs是被传对象的一份副本,这样的话我们就不用在operator=为参数创建一个临时对象了Widget& Widget::operator=(Widget rhs){swap(rhs); //将传入的副本与*this进行数据替换return *this;}
private:Bitmap* pb;
};

补充说明(copy and swap技术):

在C++中,copy and swap技术是一种用于实现赋值操作符(operator=)的通用且安全的方法。这种技术的主要目的是简化赋值操作的实现,同时避免潜在的错误和异常安全问题,特别是在涉及动态内存分配和复杂资源管理的类中。
copy and swap技术的基本思路如下:
(1)拷贝源对象:首先,在目标对象内部创建一个源对象的副本。这通常通过调用拷贝构造函数或使用其他复制机制来完成。
(2)交换资源:然后,使用std::swap或其他交换机制来交换目标对象与刚刚创建的副本之间的资源。这包括所有动态分配的内存、句柄、指针等。
(3)销毁副本:最后,当副本对象离开作用域时,其析构函数会自动清理原本属于目标对象的资源。
通过这种方法,赋值操作被分解为两个相对简单的步骤:拷贝和交换。这使得代码更加清晰和易于理解,同时也减少了出错的可能性。此外,由于交换操作通常是异常安全的(即不会抛出异常),因此整个赋值过程也变得更加健壮。

总的来说:

  • 类的拷贝赋值操作符可能被声明“以 by value 方式接受实参”;
  • 以传值方式传递东西会生成一份副本;

2、面试相关

在C++中,处理operator=中的“自我赋值”是一个重要的问题,因为它涉及到资源管理和避免不必要的操作。以下是几个与operator=中的“自我赋值”相关的高频面试题目:

2.1 什么是自我赋值?为什么它是个问题?

  • 自我赋值指的是对象尝试将自己赋值给自己,即obj = obj。这通常不是一个预期的操作,而且在没有正确处理的情况下,可能会导致资源泄漏或程序崩溃。

2.2 在重载赋值操作符时,如何避免自我赋值问题?

  • 一个常见的策略是在赋值前检查源对象(即赋值操作符右侧的对象)和目标对象(即赋值操作符左侧的对象)是否相同。这通常通过比较它们的地址来实现。

2.3 如果不处理自我赋值,可能会发生什么?

  • 如果不处理自我赋值,那么在尝试释放和重新分配对象的资源时(如动态分配的内存),可能会遇到问题。例如,释放同一块内存两次会导致未定义行为,或者尝试删除空指针也可能导致问题。

2.4 在赋值操作符中,如何处理深拷贝和浅拷贝?

  • 深拷贝会创建源对象内容的新副本,而浅拷贝则只复制指针或引用。在处理动态分配资源的对象时,通常需要实现深拷贝来避免多个对象共享同一份资源的问题。

2.5请提供一个示例,展示如何在赋值操作符中正确处理自我赋值和资源管理。

  • 示例可能包括检查源对象和目标对象是否相同,只有在它们不同时才执行资源释放和重新分配的操作。同时,可能需要考虑异常安全性,确保在发生异常时资源仍然得到正确管理。

示例代码:

#include <iostream>  
#include <algorithm> // 用于 std::swap  class MyResource {  
public:  MyResource() {  std::cout << "Allocating resource\n";  // 假设这里进行了某些资源的动态分配  }  ~MyResource() {  std::cout << "Deleting resource\n";  // 假设这里释放了之前分配的资源  }  // 假设这里还有其他管理资源的成员函数...  
};  class MyClass {  
private:  MyResource* resource;  public:  MyClass() : resource(new MyResource()) {}  ~MyClass() { delete resource; }  // 拷贝构造函数和拷贝赋值操作符(为了完整性)  MyClass(const MyClass& other) : resource(new MyResource(*other.resource)) {}  // 重载赋值操作符,正确处理自我赋值  MyClass& operator=(const MyClass& other) {  if (this != &other) { // 检查是否为自我赋值  // 释放当前对象的资源  delete resource;  // 分配新的资源  resource = new MyResource(*other.resource);  }  return *this;  }  // ... 其他成员函数 ...  
};  int main() {  MyClass obj1;  MyClass obj2;  // 正常的赋值操作  obj1 = obj2;  // 自我赋值测试  obj1 = obj1; // 应该安全执行,不导致任何问题  return 0;  
}

3、总结

天堂有路你不走,地狱无门你自来。

4、参考

4.1 《Effective C++》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/792586.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vuex的模块化管理

1&#xff1a;定义一个单独的模块。由于mutation的第二个参数只能提交一个对象&#xff0c;所以这里的ThisLog是个json串。 2&#xff1a;在Vuex中的index.js中引入该模块 3&#xff1a;在别的组件中通过...mapState调用模块保存的State的值。 4&#xff1a;用...mapMutations修…

【番外篇2】统计学-方差分析

方差分析 方差分析&#xff08;ANOVA&#xff09;是一种用于比较三个或三个以上组之间平均值是否有显著差异的统计方法。通俗地说&#xff0c;就是用来确定不同组之间的平均值是否有显著差异。 让我们通过一个简单的例子来解释方差分析&#xff1a; 假设你是一位教育工作者&a…

界面控件Kendo UI for jQuery 2024 Q1亮点 - 新的ToggleButton组件

Telerik & Kendo UI 2024 Q1 版本于2024年初发布&#xff0c;在此版本中将AI集成到了UI组件中&#xff0c;在整个产品组合中引入AI Prompt组件以及10多个新的UI控件、支持Angular 17、多个数据可视化功能增强等。 P.S&#xff1a;Kendo UI for jQuery提供了在短时间内构建…

哲♂学家带你用顺序表实现通讯录

实现通讯录能使我们进一步加深对顺序表的理解&#xff0c;接下来就由本哲♂学家带你手把手实现通信录。 其中需要用到顺序表的知识可以点击下面链接了解&#xff1a;http://t.csdnimg.cn/9SjGd话不多说&#xff0c;我们♂开始吧。 一、通讯录头文件声明 由于我们前面已经写过…

四核8g服务器价格多少钱?

2024年腾讯云4核8G服务器租用优惠价格&#xff1a;轻量应用服务器4核8G12M带宽646元15个月&#xff0c;CVM云服务器S5实例优惠价格1437.24元买一年送3个月&#xff0c;腾讯云4核8G服务器活动页面 txybk.com/go/txy 活动链接打开如下图&#xff1a; 腾讯云4核8G服务器优惠价格 轻…

设计模式:代理模式

定义 代理模式(Proxy Pattern)是一种结构型设计模式,它为另一个对象提供一个代理或占位符,以控制对这个对象的访问。使用代理模式可以在不改变对象本身的前提下,增加额外的功能,如访问控制、延迟初始化、日志记录、安全检查等。 应用场景 代理模式常见的应用场景包括:…

gradle 7.0 + 配置

Maven 镜像地址的设置 原来在项目根目录的 build.gradle 中进行设置&#xff0c;但是现在里面只有plugins。 工程的build.gradle的dependencies修改为plugins&#xff0c;替代了引用原来的Gradle版本。 // Top-level build file where you can add configuration options com…

【数据结构】——二叉树的递归实现,看完不再害怕递归

创作不易&#xff0c;感谢三连加支持&#xff1f;&#xff01; 一 递归理解 递归无非就是相信它&#xff0c;只有你相信它&#xff0c;你才能写好递归&#xff01;为什么&#xff1f;请往下看 在进入二叉树的实现之前&#xff0c;我们得先理解一遍递归&#xff0c;可能很多…

Android JNI 调用第三方SO

最近一个项目使用了Go 编译了一个so库&#xff0c;但是这个so里面还需要使用第三方so库pdfium, 首先在Android工程把2个so库都放好 在jni中只能使用dlopen方式&#xff0c;其他的使用函数指针的方式来调用&#xff0c;和windows dll类似&#xff0c;不然虽然编译过了但是会崩溃…

基于H2O AutoML与集成学习策略的房屋售价预测模型研究与实现

项目简述&#xff1a; 本项目采用H2O AutoML工具&#xff0c;针对加州房屋销售价格预测问题进行了深入研究与建模。项目以Kaggle提供的加州房屋 交易数据集为基础&#xff0c;通过数据清洗、特征工程、模型训练与评估等步骤&#xff0c;构建了一种基于集成学习策略的房价预测模…

Flink运行机制相关概念介绍

Flink运行机制相关概念介绍 1. 流式计算和批处理2. 流式计算的状态与容错3. Flink简介及其在业务系统中的位置4. Flink模型5. Flink的架构6. Flink的重要概念7. Flink的状态、状态分区、状态缩放&#xff08;rescale&#xff09;和Key Group8. Flink数据交换9. 时间语义10. 水位…

GrayLog日志平台的基本使用-接入jumpserver

1、jumpserver3.8.0部署 Docker 环境准备 # 安装依赖包 yum install -y yum-utils device-mapper-persistent-data lvm2 # 添加源 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 替换Docker 安装源为清华大学镜像站 sed -i sh…

Spring Web MVC的入门学习(一)

目录 一、什么是 Spring Web MVC 1、MVC 定义 二、学习Spring MVC 1、项目准备 2、建立连接 2.1 RequestMapping 注解的学习 2.2 RequestMapping 使用 3、请求 3.1 传递单个参数 3.2 传递多个参数 3.3 传递对象 3.4 后端参数重命名&#xff08;后端参数映射&#xf…

1111111

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

前端Vue篇之vue3 ref 能处理对象了?

在 Vue 3 中&#xff0c;ref 确实可以用来处理对象了。Vue 3 引入了 Composition API&#xff0c;这是一个新的编写组件的方式&#xff0c;它使得处理响应式数据更加灵活和高效。ref 是 Composition API 中的一个函数&#xff0c;用于创建一个响应式的引用数据。当你使用 ref 来…

配置code-server和texlive实现网页写tex

使用overleaf太卡了&#xff0c;有云服务器或者nas小主机&#xff0c;配置自己的code-servertexlive&#xff0c;来写论文。 之前用服务器配置过自己的overleaf&#xff0c;感觉不是很好用&#xff0c;缺少东西。 一、思路 使用docker先安装一个ubuntu&#xff0c;用dockerfil…

day63 单调栈part02

503. 下一个更大元素 II 中等 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序&#xff0c;这个数字之后的第一个比它更…

【generate】如何维护一套icon组件库,直接输出svg为react component

https://github.com/ant-design/ant-design-web3/pull/761/files 实现了icon-preview(通过jsdoc, 鼠标放在组件上可以看到icon的样式)&#xff0c;因为打包方式、产物以及命名上有一些不同&#xff0c;可能需要稍加改造。 这个同步脚本应该后续也用得上&#xff0c;略加改造同步…

Macbook文件清理软件 Mac电脑清理垃圾文件怎么清理

为了维护Macbook电脑的系统健康&#xff0c;我们需要定期给电脑进行全面清理&#xff0c;清除系统垃圾文件、软件缓存和系统内存。那么好用的Macbook文件清理软件有哪些呢&#xff1f;今天就给大家介绍几款好用的电脑清理软件并介绍Mac电脑清理垃圾文件怎么清理。 一、Macbook…

2024.3.6力扣每日一题——找出数组中的 K-or 值

2024.3.6 题目来源我的题解方法一 枚举遍历方法二 枚举遍历&#xff08;优化&#xff09; 题目来源 力扣每日一题&#xff1b;题序&#xff1a;2917 我的题解 方法一 枚举遍历 使用一个数组存储每一个比特位为1的数量&#xff0c;然后根据该数组得到结果的每一比特位是否为1…