Effective C++ 2e Item31_cpp 2e31-程序员宅基地

技术标签: c++  Meyers98  工作  delete  class  object  

条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用

本条款听起来很复杂,其实不然。它只是一个很简单的道理,真的,相信我。

先看第一种情况:返回一个局部对象的引用。它的问题在于,局部对象 ----- 顾名思义 ---- 仅仅是局部的。也就是说,局部对象是在被定义时创建,在离开生命空间时被销毁的。所谓生命空间,是指它们所在的函数体。当函数返回时,程序的控制离开了这个空间,所以函数内部所有的局部对象被自动销毁。因此,如果返回局部对象的引用,那个局部对象其实已经在函数调用者使用它之前被销毁了。

当想提高程序的效率而使函数的结果通过引用而不是值返回时,这个问题就会出现。下面的例子和条款23中的一样,其目的在于详细说明什么时候该返回引用,什么时候不该:

class Rational {          // 一个有理数类
public:
  Rational(int numerator = 0, int denominator = 1);
  ~Rational();

  ...

private:
  int n, d;               // 分子和分母

// 注意operator* (不正确地)返回了一个引用
friend const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs);
};

// operator*不正确的实现
inline const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs)
{
  Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
  return result;
}

这里,局部对象result在刚进入operator*函数体时就被创建。但是,所有的局部对象在离开它们所在的空间时都要被自动销毁。具体到这个例子来说,result是在执行return语句后离开它所在的空间的。所以,如果这样写:

Rational two = 2;

Rational four = two * two;         // 同operator*(two, two)


函数调用时将发生如下事件:

1. 局部对象result被创建。
2. 初始化一个引用,使之成为result的另一个名字;这个引用先放在另一边,留做operator*的返回值。
3. 局部对象result被销毁,它在堆栈所占的空间可被本程序其它部分或其他程序使用。
4. 用步骤2中的引用初始化对象four。

一切都很正常,直到第4步才产生了错误,借用高科技界的话来说,产生了"一个巨大的错误"。因为,第2步被初始化的引用在第3步结束时指向的不再是一个有效的对象,所以对象four的初始化结果完全是不可确定的。

教训很明显:别返回一个局部对象的引用。

"那好,"你可能会说,"问题不就在于要使用的对象离开它所在的空间太早吗?我能解决。不要使用局部对象,可以用new来解决这个问题。"象下面这样:

// operator*的另一个不正确的实现
inline const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs)
{
  // create a new object on the heap
  Rational *result =
    new Rational(lhs.n * rhs.n, lhs.d * rhs.d);

  // return it
  return *result;
}

这个方法的确避免了上面例子中的问题,但却引发了新的难题。大家都知道,为了在程序中避免内存泄漏,就必须确保对每个用new产生的指针调用delete,但是,这里的问题是,对于这个函数中使用的new,谁来进行对应的delete调用呢?

显然,operator*的调用者应该负责调用delete。真的显然吗?遗憾的是,即使你白纸黑字将它写成规定,也无法解决问题。之所以做出这么悲观的判断,是基于两条理由:

第一,大家都知道,程序员这类人是很马虎的。这不是指你马虎或我马虎,而是指,没有哪个程序员不和某个有这类习性的人打交道。想让这样的程序员记住无论何时调用operator*后必须得到结果的指针然后调用delete,这样的几率有多大呢?也是说,他们必须这样使用operator*:

const Rational& four = two * two;      // 得到废弃的指针;
                                       // 将它存在一个引用中
...

delete &four;                          // 得到指针并删除

这样的几率将会小得不能再小。记住,只要有哪怕一个operator*的调用者忘了这条规则,就会造成内存泄漏。

返回废弃的指针还有另外一个更严重的问题,即使是最尽责的程序员也难以避免。因为常常有这种情况,operator*的结果只是临时用于中间值,它的存在只是为了计算一个更大的表达式。例如:

Rational one(1), two(2), three(3), four(4);
Rational product;

product = one * two * three * four;

product的计算表达式需要三个单独的operator*调用,以相应的函数形式重写这个表达式会看得更清楚:

product = operator*(operator*(operator*(one, two), three), four);

是的,每个operator*调用所返回的对象都要被删除,但在这里无法调用delete,因为没有哪个返回对象被保存下来。

解决这一难题的唯一方案是叫用户这样写代码:

const Rational& temp1 = one * two;
const Rational& temp2 = temp1 * three;
const Rational& temp3 = temp2 * four;

delete &temp1;
delete &temp2;
delete &temp3;

果真如此的话,你所能期待的最好结果是人们将不再理睬你。更现实一点,你将会在指责声中度日,或者可能会被判处10年苦力去写威化饼干机或烤面包机的微代码。

所以要记住你的教训:写一个返回废弃指针的函数无异于坐等内存泄漏的来临。

另外,假如你认为自己想出了什么办法可以避免"返回局部对象的引用"所带来的不确定行为,以及"返回堆(heap)上分配的对象的引用"所带来的内存泄漏,那么,请转到条款23,看看为什么返回局部静态(static)对象的引用也会工作不正常。看了之后,也许会帮助你避免头痛医脚所带来的麻烦。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lostmouse/article/details/8495

智能推荐

133道Java面试题及答案(面试必看)-程序员宅基地

文章浏览阅读1.7w次,点赞6次,收藏118次。Java 面试随着时间的改变而改变。在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越来越高级,面试官问的问题也更深入。 在我初入职场的时候,类似于 Vector 与 Array 的区别、HashMap 与 Hashtable 的区别是最流行的问题,只需要记住它们,就能在面试中获得更好的机会,但这种情形已经不复存在。如今,你...

tensorflow2系类知识-3 :CNN_cnn_class-程序员宅基地

文章浏览阅读240次。CNN基础知识卷积神经网络(Convolutional Neural Network, CNN)是一种结构类似于人类或动物的 视觉系统 的人工神经网络,包含一个或多个卷积层(Convolutional Layer)、池化层(Pooling Layer)和全连接层(Fully-connected Layer)。示例代码# -*- coding:utf-8 -*-# /usr/bin/p..._cnn_class

用栈来判断括号匹配问题_6-4 括号匹配 分数 15 作者 张瑞霞 单位 桂林电子科技大学 本题要求通过栈来判断-程序员宅基地

文章浏览阅读2.9k次,点赞4次,收藏21次。用栈实现:输入一行符号,以#结束,判断其中的括号是否匹配。括号包括:{ } 、 [ ] 、 ( )、 < >如果匹配,输出 right如果不匹配,给出错误提示。包括: (1)对称符号都匹配,输出 “ right “ (2)处理到某个符号时不匹配了,输出 " The $ character '*' is wrong." ,其中$是出错符号的序号,*是出错..._6-4 括号匹配 分数 15 作者 张瑞霞 单位 桂林电子科技大学 本题要求通过栈来判断

【浙大版《Python 程序设计》题目集(解)】第4章-28 矩阵转置(10分)_3*3转置矩阵行列转置python-程序员宅基地

该题目是要求将一个3×3矩阵进行转置,即行和列互换。输入格式为一行9个小于100的整数,输出格式为3行3列的二维数组,每个数据输出占4列。例子给出了一个输入样例和输出样例。使用for循环和格式化输出即可实现。

【机器学习】基于卷积神经网络 CNN 的猫狗分类问题_cnn猫狗分类-程序员宅基地

文章浏览阅读1.2w次,点赞3次,收藏65次。卷积神经网络(Convolutional Neural Networks, CNN)是一类包含卷积计算且具有深度结构的前馈神经网络(Feedforward Neural Networks),是深度学习(deep learning)的代表算法之一。顾名思义,就是将卷积与前馈神经网络结合,所衍生出来的一种深度学习算法。卷积神经网络CNN的结构图使用卷积神经网络(CNN)实现猫狗分类是一种有效的方法,它能够自动从图像中学习特征并进行分类,提高准确性。_cnn猫狗分类

tf.compat.v1.placeholder-程序员宅基地

文章浏览阅读2.9k次。Inserts a placeholder for a tensor that will be always fed.tf.compat.v1.placeholder( dtype, shape=None, name=None)Important: This tensor will produce an error if evaluated. Its valu...

随便推点

亚信科技笔试题_亚信科技笔试题目-程序员宅基地

文章浏览阅读522次。例如:给定 N=6 以及 A[0]=20 A[1]=10 A[2]=30 A[3]=30 A[4]=40 A[5]=10 此函数应返回 10 或者 30。如果字符串A和字符串B含有相同的字母,但是顺序可能不一样,那么A被称为是B的anagram。对于数组中下标为 k 的元素,A[k] 包含了列表中下一个节点的下标,或者是 -1 ——表示列表到此结束,没有下一个节点了。从给出的非空整数数组 A(下标从 0 开始算)中,找到那个藏在其中的列表的长度,将长度值返回。5. Sleep()和wait()的区别?..._亚信科技笔试题目

java/php/net/python家庭安防系统【2024年毕设】-程序员宅基地

文章浏览阅读757次。系统架构图属于系统设计阶段,系统架构图只是这个阶段一个产物,系统的总体架构决定了整个系统的模式,是系统的基础。springboot基于android的天干地支文化科普和动画系统。springboot基于微信小程序的企业资源预约系统的设计与实现。springboot基于springboot的母婴服务管理系统。springboot基于Android的天气预报及推荐系统。springboot基于springboot的外卖系统。ssm基于SSM的儿童疫苗接种管理系统的设计与实现。

CLSR:Contrastive Learning Based Graph Convolution Network forSocial Recommendation_cross-view temporal graph contrastive learning for-程序员宅基地

文章浏览阅读686次,点赞20次,收藏11次。当前的社交推荐模型通常关注建模多图结构,并从这些多图中汇集信息以学习用户的偏好。然而,这些方法往往使用复杂的模型和冗余的参数来获得轻微的性能改进。因此,本文提出了一种集成社交图和交互图信息的对比学习方法,通过对用户嵌入进行融合来获取更精细的用户表示。同时,引入了对比学习框架,通过数据增强构建对比学习的正负样本。总体而言,这篇论文介绍了一种基于对比学习的图卷积网络方法,用于改进社交推荐系统的性能。通过融合社交图和交互图信息,并利用对比学习框架进行数据增强,该方法能够更好地学习用户的偏好。_cross-view temporal graph contrastive learning for session-based recommendat

Quartus使用步骤及联合Modelsim仿真教程_quartus仿真-程序员宅基地

文章浏览阅读5.7k次,点赞19次,收藏69次。Quartus使用记录及与modelsim联合仿真_quartus仿真

编写函数判断闰年_本关任务:编写一个函数,能判断年份n是否是闰年,并调用该函数判断输入的年份是否为-程序员宅基地

文章浏览阅读3.6k次,点赞2次,收藏3次。编写函数 int fun(int n)判断n是否是闰年,定义main函数输入年份,调用fun函数判断,在main函数中输出yes或no。请只提供子函数的编写。【样例输入】2000【样例输出】yesint fun(int n)_本关任务:编写一个函数,能判断年份n是否是闰年,并调用该函数判断输入的年份是否为

Java异常之throws和throw理解_throws必须捕获或者throw吗-程序员宅基地

文章浏览阅读1.2k次。1、Throws 如果在当前方法不知道该如何处理该异常时,则可以使用throws对异常进行抛出给调用者处理或者交给JVM。调用者调用此方法,要么抛出要么try catch处理,到了JVM这里,就是打印出异常堆栈,并终止运行。换句话说,用这个有两种情况。 1>我throws抛出异常,如果是检查异常,那么调用者必须捕获或再次抛出 2>我th..._throws必须捕获或者throw吗