1. 问题的提出:要求函数返回对象时,可以返回引用么?

一旦程序员理解了按值传递有可能存在效率问题之后(Item 20),许多人都成了十字军战士,决心清除所有隐藏的按值传递所引起的开销。对纯净的按引用传递(不需要额外的构造或者析构)的追求丝毫没有懈怠,但他们的始终如一会产生致命的错误:它们开始传递指向并不存在的对象的引用。这可不是好事情。

考虑表示有理数的一个类,它包含将两个有理数相乘的函数(Item 3):

 class Rational {

 public:

 Rational(int numerator = , // see Item 24 for why this

 int denominator = ); // ctor isn’t declared explicit

 ...

 private:

 int n, d; // numerator and denominator

 friend

 const Rational // see Item 3 for why the

 operator*(const Rational& lhs, // return type is const

 const Rational& rhs);

 };

Operator* 的这个版本为按值返回结果,如果你没有为调用这个对象的构造函数和析构函数造成的开销而担心,你就是在逃避你的专业职责。如果这个对象不是必须的,你就不想为这样一个对象的开销去买单。所以问题是:这个对象的生成是必须的么?

2. 问题的分析(一):如返回引用,必须为返回的引用创建一个新的对象

如果你能够返回一个引用那么就不是必须为其买单。但是记住引用只是一个别名,一个已存对象的别名。每当你声明一个引用时,你应该马上问问自己它用来做谁的别名,因为它必须是某些东西的别名。对于operator*来说,如果这个函数返回一个引用,它必须返回一个指向已存在Rational对象的引用,这个对象包含了两个对象的乘积结果。

没有任何理由假设在调用operator*之前这样一个对象已经存在了。也就是说,如果你进行下面的操作:

 Rational a(, ); // a = 1/2

 Rational b(, ); // b = 3/5

 Rational c = a * b; // c should be 3/10

期望已经存在一个值为3/10的有理数看上去是不合理的。如果operator*即将返回一个指向值为3/10的有理数的引用,它必须自己创建出来。

3. 问题的分析(二):创建新对象的三种错误方法

3.1 在栈上创建reference指向的对象

一个函数只可以通过两种方法来创建一个新的对象:在栈上或者在堆上。通过定义一个本地变量来完成栈上的对象创建。使用这个策略,你可以尝试使用下面的方法来实现:operator*:

 const Rational& operator*(const Rational& lhs, // warning! bad code!

 const Rational& rhs)

 {

 Rational result(lhs.n * rhs.n, lhs.d * rhs.d);

 return result;

 }

你会立即否决这种做法,因为你的目标是避免调用构造函数,但是这里的result必须被构造出来。更加严重的问题是:这个函数返回指向result的引用,但result是一个本地对象,当函数退出的时候这个对象就会被销毁。所以这个版本的operator*并没有返回指向Rational的引用,它返回的引用指向从前的Rational对象,现在变成了一个空的,令人讨厌的,已经腐烂的Rational对象的尸体,它已经被销毁了。任何使用这个函数的返回值的调用者都将会马上进入未定义行为的范围。事实是,任何返回指向本地对象的引用的函数都是被破坏掉的函数。(返回指向本地对象的指针的函数也是如此)。

3.2 在堆上创建reference指向的对象

让我们再考虑一下下面这种用法的可能性:在堆上创建一个对象并且返回指向它的引用。堆上的对象通过使用new来创建,所以你可以像下面这样实现一个基于堆的operator*:

 const Rational& operator*(const Rational& lhs, // warning! more bad

 const Rational& rhs) // code!

 {

 Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);

 return *result;

 }

这里你仍然需要为构造函数的调用买单,对new分配的内存进行初始化是通过调用一个合适的构造函数来实现的,但是现在有另外一个问题:谁在这个对象上应用new召唤出来的delete?

即使是一个认真负责的,心怀善意的调用者,对于下面这种合理的使用场景,他们也没有什么方法来避免内存泄漏:

 Rational w, x, y, z;

 w = x * y * z; // same as operator*(operator*(x, y), z)

这里,在同一个语句中调用了两次operator*,因此使用了两次new,这也需要使用两次delete来对new出来的对象进行销毁。没有什么合理的方法来让operator*的客户来进行这些调用,因为对于他们来说没有合理的方法来获取隐藏在从operator*返回回来的引用后面的指针。这么做保证会产生资源泄漏。

3.3 为reference创建 static对象

3.3.1单一static 对象

你可能注意到了,不管是在堆上还是栈上创建从operator*返回的结果,你都必须要调用一个构造函数。可能你能回忆起来我们的初衷是避免这样的构造函数调用。可能你认为你知道一种只需要调用一次构造函数,其余的构造函数被避免调用的方法。下面的这种实现突然出现了,这种方法基于另外一种operator*的实现:令其返回指向static Rational对象的引用,函数实现如下:

 const Rational& operator*(const Rational& lhs, // warning! yet more

 const Rational& rhs) // bad code!

 {

 static Rational result; // static object to which a

 // reference will be returned

 result = ... ; // multiply lhs by rhs and put the

 // product inside result

 return result;

 }

像所有使用静态对象的设计一样,这种方法增加了对于线程安全的梳理工作,但这个缺点是比较明显的。为了看一下更深层次的缺陷,考虑一份完全合理的客户代码:

 bool operator==(const Rational& lhs, // an operator==

 const Rational& rhs); // for Rationals

 Rational a, b, c, d;

 ...

 if ((a * b) == (c * d)) {

 do whatever’s appropriate when the products are equal;

 } else {

 do whatever’s appropriate when they’re not;

 }

你猜怎么着?表达式((a*b) == (c*d))的求值结果总为true,而不管a,b,c,d的值是什么!

将表达式用等价的函数形式进行重写,上面的不可思议的事情就能很容易明白:

 if (operator==(operator*(a, b), operator*(c, d)))

注意当operator==被调用的时候,已经调用了两次operato*,每次调用都会返回指向operator*中的static Raitional对象的引用。因此,operator==会对operator*中的static Rational对象和operator* 中的static Rational对象进行比较。如果不相等就奇怪了。

3.3.2 Static数组

这应该足够使你相信从像operator*一样的函数中返回一个引用是在浪费时间,但是一些人现在开始想了:好,如果一个static不够,可能一个static数组能够达到目的。。。

我不能提供示例代码来让这个设计显得如此高贵,但是我能描述一下为什么这个想法会让你感到羞愧脸红。首先,你必须选择一个合适的n,也就是数组的大小。如果n太小,你可能会耗尽存储函数返回值的空间,这样对于上面的单一静态对象设计来说,我们没有获得任何好处。如果n太大,你的程序的性能会降低,因为即使这个函数仅被使用一次,在第一次被调用之前,数组中的每一个对象都会被构造出来。这会让你付出调用n个构造函数和n个析构函数的代价。如果最优化(optimization)是改善软件性能的一个过程,那么这种事情应该被叫做“最差化”(pessimization)。最后,想象一下你该如何把你所需要的值放入数组的对象中,并且这样做会付出什么代价。最直接的方法是通过赋值来对对象之间的值进行移动,但是赋值的代价是什么呢?对于许多类型来说,赋值等同于调用一个析构函数(释放旧值)和一个构造函数(拷贝新值)。但是你的目标是要避免析构和构造的开销!直面它把,这个方法没有奏效。(使用vector来代替数组也不会对问题有所改善。)

4. 问题结论:从函数中返回新对象的正确方法是——返回对象

实现一个必须返回一个新对象的函数的正确方法是让函数返回新的对象(value不是reference)。对于Rational的opertaor*函数来说,其实现如下面的代码(或者与其等价的代码):

 inline const Rational operator*(const Rational& lhs, const Rational& rhs)

 {

 return Rational(lhs.n * rhs.n, lhs.d * rhs.d);

 }

当然,你会从operator*的返回值中引入构造和析构的开销,但从长远来看,这是为正确的行为付出了一个小的代价。此外,让你毛骨悚然的账单再也不会到来。像许多编程语言一样,C++允许编译器实现者在不改变可视化代码行为的前提下,对代码进行优化,以达到改善生成码性能的目的。在一些情况中,我们发现,operator*返回值的构造和析构可以被安全的消除。当编译器利用了这个事实(编译器经常这么做),你的程序就会以你所期望的方式进行下去,只是比你想要的要快。

将本条款归结如下:在返回一个引用还是返回一个对象之间做决定时,你的工作是选择能够提供正确行为的那个。对于“如何使这个选择有尽可能小的开销”这个问题的解决,让编译器供应商去斗争把。

5. 总结

绝不要返回指向本地栈对象的指针或者引用,指向堆对象的引用,或者在有可能需要多个对象的时候返回指向本地静态对象的指针或者引用。(Item 4)给出了一种设计的一个例子,说明了返回指向本地静态对象的引用是合理的,至少在单线程环境中。)

读书笔记 effective c++ Item 21 当你必须返回一个对象的时候,不要尝试返回引用的更多相关文章

  1. 读书笔记 effective C++ Item 40 明智而谨慎的使用多继承

    1. 多继承的两个阵营 当我们谈论到多继承(MI)的时候,C++委员会被分为两个基本阵营.一个阵营相信如果单继承是好的C++性质,那么多继承肯定会更好.另外一个阵营则争辩道单继承诚然是好的,但多继承太 ...

  2. 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

    1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...

  3. 读书笔记 effective c++ Item 48 了解模板元编程

    1. TMP是什么? 模板元编程(template metaprogramming TMP)是实现基于模板的C++程序的过程,它能够在编译期执行.你可以想一想:一个模板元程序是用C++实现的并且可以在 ...

  4. 读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎

    1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如 ...

  5. 读书笔记 effective c++ Item 28 不要返回指向对象内部数据(internals)的句柄(handles)

    假设你正在操作一个Rectangle类.每个矩形可以通过左上角的点和右下角的点来表示.为了保证一个Rectangle对象尽可能小,你可能决定不把定义矩形范围的点存储在Rectangle类中,而是把它放 ...

  6. 读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

    1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声 ...

  7. 读书笔记 effective c++ Item 1 将c++视为一个语言联邦

    Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有“合适的”规则看上 ...

  8. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

  9. 读书笔记 effective c++ Item 11 在operator=中处理自我赋值

    1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: class Widget { ... }; Widget w; ... w = w; // assignment to sel ...

随机推荐

  1. 【转】安卓Java的虚拟机区别

    Google于2007年底正式发布了Android SDK, 作为 Android系统的重要特性,Dalvik虚拟机也第一次进入了人们的视野.它对内存的高效使用,和在低速CPU上表现出的高性能,确实令 ...

  2. Django form模块使用心得

    最近用Django 写了一个网站,现在来分享一下对Django form 的一些心得. 一,创建一个表单 创建一个Form表单有两种方式: 第一种方式是继承于forms.Form,的一个子类,通过在f ...

  3. ASP.NET异步处理

    前一篇:详解 .NET 异步 在前文中,介绍了.NET下的多种异步的形式,在WEB程序中,天生就是多线程的,因此使用异步应该更为谨慎.本文将着重展开ASP.NET中的异步. [注意]本文中提到的异步指 ...

  4. keystore 介绍

    Keytool 是一个有效的安全钥匙和证书的管理工具. Java 中的 keytool.exe (位于 JDK\Bin 目录下)可以用来创建数字证书,所有的数字证书是以一条一条(采用别名区别)的形式存 ...

  5. Failed to install *.apk on device 'emulator-5554': timeout

    错误提示: Failed to install helloworld.apk on device 'emulator-5554': timeout 或者 the user data image is ...

  6. spark Intellij IDEA开发环境搭建

    (1)创建Scala项目 File->new->Project,如下图 选择Scala 然后next 其中Project SDK指定安装的JDK,Scala SDK指定安装的Scala(这 ...

  7. Unity 5 Stats窗口

    Unity5的 Statistics上的统计信息和Unity4 有一些区别, Statistics窗口,全称叫做 Rendering Statistics Window,即渲染统计窗口(或渲染数据统计 ...

  8. jQuery插件Flot的介绍

    Flot采用Canvas绘制图形(Web总共就有三种常见方式来绘制图形,不了解的同学请看这篇文章),在数据量非常大的时候,你需要考虑浏览器端的性能问题.顺便提一句,D3是采用SVG来绘制图形的,从我自 ...

  9. jQuery删除DOM节点

    jQuery删除DOM节点 <%@ page language="java" import="java.util.*" pageEncoding=&quo ...

  10. android的引用库类

    在eclipse中的项目里,有时需要外来的jar文件.添加后就可以消去程序中的红条条啦~~~~~~~~~可以照下面的说明添加. 方法/步骤   打开eclipse,导入项目   右击 项目 , “Bu ...