条款21 必须返回对象时,别妄想返回其reference

2024-08-28 04:18

本文主要是介绍条款21 必须返回对象时,别妄想返回其reference,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

总结:

    绝不要返回一个local栈对象的指针或引用;绝不要返回一个被分配的堆对象的引用;绝不要返回一个局部对象有可能同时需要多个这样的对象的指针或引用。

    条款4中给出了“在单线程环境中合理返回局部静态对象的引用”

-----------------------------------------------------------------------------------------------------------------------

C++ primer中提到:

Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs) {
Sales_item ret(lhs);
ret += rhs;
return ret;
}
        注意这里重载的加操作返回的是对象,而不是引用。因为算术操作符通常产生一个新值,该值是两个操作数的计算结果,它不同于任何一个操作数,且在一个局部变量中计算,返回对那个变量的引用是一种运行时错误


注意下面自增自减操作符的重载


------------------------------------------------------------------------------------------------------------------------------


       一旦程序员抓住对象传值的效率隐忧,很多人就会一心一意根除传值的罪恶。他们不屈不挠地追求传引用的纯度,但他们全都犯了一个致命的错误传递不存在的对象的引用。考虑一个用以表现有理数的类,包含一个函数计算两个有理数的乘积:

class Rational {
public:Rational(int numerator = 0, int denominator = 1);...
private:int n, d; // 分子与分母friendconst Rational operator*(const Rational& lhs, const Rational& rhs);
};

        这个版本operator* 以传值方式返回它的结果,需要付出对象的构造和析构成本。如果你能用返回一个引用来代替,就不需付出代价。但是,请记住一个引用仅仅是一个别名,一个实际存在的对象的名字。无论何时只要你看到一个引用的声明,应该立刻问自己它是什么东西的别名,因为它必定是某物的别名。以上述operator*为例,如果函数返回一个引用,它必然返回某个既有的而且包含两个对象相乘产物的Rational对象引用。


当然没有什么理由期望这样一个对象在调用operator*之前就存在。也就是说,如果你有

Rational a(1, 2);          // a = 1/2
Rational b(3, 5);          // b = 3/5
Rational c = a * b;        // c should be 3/10
        期望原本就存在一个值为3/10的有理数对象并不合理。如果operator* 返回一个reference指向如此数值,它必须自己创建那个Rational对象


函数创建新对象仅有两种方法:在栈或在堆上。如果定义一个local变量,就是在栈空间创建对象

const Rational& operator*(constRational& lhs, const Rational& rhs)
{Rational result(lhs.n * rhs.n, lhs.d * rhs.d);return result;
}  //糟糕的代码!
        本来目的是避免调用构造函数,而result却必须像任何对象一样由构造函数构造而来。这个函数返回一个 指向result的引用,但是result是一个局部对象,在函数退出时被销毁了。因此这个operator*的版本不会返回指向一个Rational的引用,它返回指向一个过时的Rational,因为它已经被销毁了。任何调用者甚至只是对此函数的返回值做任何一点点运用,就立刻进入了未定义行为的领地。这是事实, 任何返回一个指向局部变量引用(或指针)的函数都是错误的


考虑一下在堆上构造一个对象并返回指向它的引用的可能性。基于堆的对象通过使用new创建:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);return *result;
}  //更糟的写法!
谁该对用new创建出来的对象实施delete?
Rational w, x, y, z;
w = x * y * z; // 与operator*(operator*(x, y), z)相同

        这里,在同一个语句中有两个operator*的调用,因此new被使用了两次,这两次都需要使用 delete来销毁。但是operator*的客户没有合理的办法进行那些调用,因为他们没有合理的办法取得隐藏在通过调用operator*返回的引用后面的指针。这绝对导致资源泄漏。

         无论是在栈还是在堆上的方法,为了从operator*返回的每一个 result,我们都不得不容忍一次构造函数的调用,而我们最初的目标是避免这样的构造函数调用。我们可以继续考虑基于 operator*返回一个指向staticRational对象引用的实现,而这个static Rational对象定义在函数内部:

const Rational& operator*(constRational& lhs, const Rational& rhs)
{static Rational result; // static对象,此函数返回其referenceresult= ... ;           // 将lhs乘以rhs,并将结果置于result内
return result;
}                          //又一堆烂代码!bool operator==(const Rational& lhs,const Rational& rhs);
// 一个针对Rational所写的operator==
Rational a, b, c, d;
...if ((a * b) == (c * d)) {当乘积相等时,做适当的相应动作;}
else {当乘积不等时,做适当的相应动作}
         除了和所有 使用static对象的设计一样可能引起的线程安全(thread-safety)的混乱,上面 不管 a,b,c,d 的值是什么,表达式 ((a*b) == (c*d)) 总是等于 true!如果代码重写为功能完全等价的另一种形式,很容易了解出了什么意外:
if (operator==(operator*(a, b), operator*(c, d)))

       在operator==被调用前,已有两个起作用的operator*调用,每一个都返回指向 operator*内部的staticRational对象的引用。两次operator*调用的确各自改变了staticRational对象值,但由于它们返回的都是reference,因此调用端看到的永远是static Rational对象的“现值”。因此operator==被要求拿operator*的static Rational对象值和operator*的static Rational对象值比较,如果不相等才奇怪。


        一个必须返回新对象的函数的正确方法:让那个函数返回一个新对象。对于Rational的 operator*,这就意味着下面这些代码或在本质上与其等价的代码:

inline const Rational operator*(constRational& lhs, const Rational& rhs)
{return Rational(lhs.n * rhs.n, lhs.d * rhs.d);}

        当然,你可能付出了构造和析构operator*的返回值的成本,但是从长远看,这只是为正确行为付出的很小代价。但万一代价很恐怖,你可以允许编译器施行最优化,用以改善出码的效率却不改变其可观察的行为。因此某些情况下operator*返回值的构造和析构可被安全的消除。如果编译器运用这一事实(它们也往往如此),程序将保持应有行为,而执行起来又比预期的更快。

总结:如果需要在返回一个引用和返回一个对象之间做决定,你的工作就是让那个选择能提供正确的行为。让你的编译器厂商去绞尽脑汁使那个选择成本尽可能地低廉。


这篇关于条款21 必须返回对象时,别妄想返回其reference的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1113705

相关文章

Springboot3统一返回类设计全过程(从问题到实现)

《Springboot3统一返回类设计全过程(从问题到实现)》文章介绍了如何在SpringBoot3中设计一个统一返回类,以实现前后端接口返回格式的一致性,该类包含状态码、描述信息、业务数据和时间戳,... 目录Spring Boot 3 统一返回类设计:从问题到实现一、核心需求:统一返回类要解决什么问题?

SpringBoot返回文件让前端下载的几种方式

《SpringBoot返回文件让前端下载的几种方式》文章介绍了开发中文件下载的两种常见解决方案,并详细描述了通过后端进行下载的原理和步骤,包括一次性读取到内存和分块写入响应输出流两种方法,此外,还提供... 目录01 背景02 一次性读取到内存,通过响应输出流输出到前端02 将文件流通过循环写入到响应输出流

C# WebAPI的几种返回类型方式

《C#WebAPI的几种返回类型方式》本文主要介绍了C#WebAPI的几种返回类型方式,包括直接返回指定类型、返回IActionResult实例和返回ActionResult,文中通过示例代码介绍的... 目录创建 Controller 和 Model 类在 Action 中返回 指定类型在 Action

sqlserver、mysql、oracle、pgsql、sqlite五大关系数据库的对象名称和转义字符

《sqlserver、mysql、oracle、pgsql、sqlite五大关系数据库的对象名称和转义字符》:本文主要介绍sqlserver、mysql、oracle、pgsql、sqlite五大... 目录一、转义符1.1 oracle1.2 sqlserver1.3 PostgreSQL1.4 SQLi

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

JavaScript对象转数组的三种方法实现

《JavaScript对象转数组的三种方法实现》本文介绍了在JavaScript中将对象转换为数组的三种实用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友... 目录方法1:使用Object.keys()和Array.map()方法2:使用Object.entr

使用MapStruct实现Java对象映射的示例代码

《使用MapStruct实现Java对象映射的示例代码》本文主要介绍了使用MapStruct实现Java对象映射的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、什么是 MapStruct?二、实战演练:三步集成 MapStruct第一步:添加 Mave

Java中实现对象的拷贝案例讲解

《Java中实现对象的拷贝案例讲解》Java对象拷贝分为浅拷贝(复制值及引用地址)和深拷贝(递归复制所有引用对象),常用方法包括Object.clone()、序列化及JSON转换,需处理循环引用问题,... 目录对象的拷贝简介浅拷贝和深拷贝浅拷贝深拷贝深拷贝和循环引用总结对象的拷贝简介对象的拷贝,把一个

使用Java读取本地文件并转换为MultipartFile对象的方法

《使用Java读取本地文件并转换为MultipartFile对象的方法》在许多JavaWeb应用中,我们经常会遇到将本地文件上传至服务器或其他系统的需求,在这种场景下,MultipartFile对象非... 目录1. 基本需求2. 自定义 MultipartFile 类3. 实现代码4. 代码解析5. 自定

javaSE类和对象进阶用法举例详解

《javaSE类和对象进阶用法举例详解》JavaSE的面向对象编程是软件开发中的基石,它通过类和对象的概念,实现了代码的模块化、可复用性和灵活性,:本文主要介绍javaSE类和对象进阶用法的相关资... 目录前言一、封装1.访问限定符2.包2.1包的概念2.2导入包2.3自定义包2.4常见的包二、stati