在必须返回一个对象时,不要去尝试返回一个引用

一旦程序员把注意力都转向了对象传值方式隐含的效率问题(参见第 20 条)时,许多人都变成了极端的“改革运动者”,他们对传值方法采取斩草除根的态度,在他们不屈不挠追求传递引用方式的纯粹性的同时,他们也犯下了致命的错误:有时候传递的引用所指向的对象并不存在。这决不是一件好事情。

请看下面的示例,其中的 Rational 类用来表示有理数,其中还包括一个函数来计算两个有理数的乘积:

class Rational {
public:
  Rational(int numerator = 0, int denominator = 1);
  // 第 24 条中解释了为什么这里的构造函数没有显性声明。
  ...

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

friend const Rational
         operator*(const Rational& lhs, const Rational& rhs);
  // 第 3 条中解释了为什么返回值是 const 的。
};

这一版本的 operator* 通过传值方式返回一个对象,如果你不去考虑这一对象在构造和析构过程中的开销,那么你就是在逃避你的专业职责。如果你并不是非得要为这样的对象付出代价,那么你大可不必那样做。现在问题就是:你必须付出这一代价吗?

好的,如果此时你可以返回一个引用作为替代品,那么就不需要了。但是请记住,一个引用仅仅是一个名字,它是一个已存在的对象的别名。当你看到一个引用的声明时,你应该立刻问一下你自己:它的另一个名字是什么,因为一个引用作指向的内容必定有它自己的名字。于是对于上面的 operator* 而言,如果它返回一个引用,那么它所引用的必须是一个已存在的 Rational 对象,这个对象中包含着需要进行乘法操作的两个对象的乘积。

如果你期望在调用 operator* 之前这一对象必须存在,那么你就太不理智了。也就是说,如果你这样做了:

Rational a(1, 2);   // a = 1/2
Rational b(3, 5);   // b = 3/5
Rational c = a * b;   // c 的值应该为 3/10

期待存在一个值为 3/10 的有理数的做法看上去显得很不理智。其实并不是这样的,如果 operator* 返回一个指向这类数值的引用,那么它必须要自己创建这个数字。

一个函数只能以两种方式创建新的对象:在栈上或在堆上。定义一个局部变量就是在栈上创建一个新对象。应用这一策略时,你可能会以这种方式编写 operator* :

const Rational& operator*(const Rational& lhs, const Rational& rhs)
    // 警告!错误的代码
{
  Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
  return result;
}

你大可以拒绝这样的实现方法,因为你的目标是防止对构造函数的调用,但是此时 result 会像其它对象一样被初始化。一个更严重的问题是:这个函数会返回一个指向 result 的引用,但是 result 是一个局部对象,而局部对象在函数退出时就会被销毁。那么,这一版本的 operator* ,并不会返回一个指向 Rational 的引用,它返回的引用指向一个“前期 Rational ”,它曾经是 Rational 的对象,一个“空寂的、散发着霉气的、开始腐烂的、曾是一个 Rational 的尸体”,但它现在与 Rational 已经毫无关系,因为它已经被销毁了。对于所有的调用者而言,只要稍稍触及这一函数的返回值,都会遭遇到无尽的无法预知的行为。事实上,任何返回局部对象引用的函数都是灾难性的。(任何返回指向局部对象的指针的函数也是如此。)

现在,让我们考虑下面做法的可行性:在堆上创建一个对象,然后返回一个指向它的引用。由于保存于堆上的对象由 new 来创建,因此你可能会这样编写基于堆的 operator* :

const Rational& operator*(const Rational& lhs, const Rational& rhs)
     // 警告!这里有更多的错误!

{
  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; // 等价于 operator*(operator*(x, y), z)

这里,在一个语句中存在着两次对 operator* 的 调用,于是存在两次 new 操作有待于使用 delete 来清除。但是又没有任何理由要求 operator* 的客户端程序员来进行这一操作,这是因为对 operator* 的调用返回了一个引用,没有理由要求客户端程序员去取得隐藏在这一引用背后的指针。这势必会造成资源泄漏。

但是,也许你注意到了,栈方案与堆方案都面临着同一个问题:它们都需要为 operator* 的每一个返回值调用一次构造函数。也许你能够回忆起我们最初的目的就是避免像此类构造函数调用。也许你认为你知道某种方法来将此类构造函数调用的次数降低到仅有一次。也许你想到了下面的实现方法:让 operator* 返回一个指向一个静态的 Rational 对象的引用,这一静态对象在函数的内部:

const Rational& operator*(const Rational& lhs, const Rational& rhs)
  // 警告!会出现更多更多的错误!
{
  static Rational result;    // 用来作为返回值的静态对象
  result = ... ;   // 将 lhs 与 rhs 相乘, 并将乘积存入 result

return result;
}

与其它引入静态对象的设计方法一样,这种方法很显著的提高了线程的安全性,但是这却带来了更明显的缺陷。下面的客户端代码是无懈可击的,但是上文中的设计会使其暴露出问题:

bool operator==(const Rational& lhs, const Rational& rhs);
    // 为有理数作比较的 operator==

Rational a, b, c, d; 
...
if ((a * b) == (c * d))  {
    当乘积相等时,执行恰当的操作 ;
} else    {
    当乘积不相等时,执行恰当的操作 ;
}

猜猜会发深什么?无论 a 、 b 、 c 或 d 取什么值,表达式 ((a*b) == (c*d)) 的值永远为真。

我们为上面函数中的判断语句更换一个形式,这个问题就更加浅显了:

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

请注意,在调用 operator== 时,已经存在了两次活动的对 operator* 的调用,每次调用时都回返回一个指向 operator* 内部的静态 Rational 对象的引用。于是编译器将要求 operator== 去将 operator* 内部的静态 Rational 对象与自身相比较。如果结果并不总是相等的,这才是让人奇怪的事情。

上面的内容似乎已经足够让你确信:为类似于 operator* 这样的函数返回一个引用确实是在浪费时间,但是有些时候你会想:“好吧,一个静态值不够,那么用一个静态数组总可以了吧 … ”

我无法用实例来捍卫我的观点,但是我可以用非常简明的推理证明这样做会让你多羞愧:首先,你必须确定一个 n 值,也就是数组的大小。如果 n 太小了,函数返回值的存储空间可能会用完,这种情况与刚才否定的单一静态对象的方案一样糟糕。但是如果 n 的值太大,那么你的程序将面临性能问题,这是因为数组中的每个对象都应在函数在第一次调用时被构造。这会使你付出 n 次构造函数和 n 次析构函数的调用,即使我们讨论的函数只被调用一次。如果将“优化”称为改善软件性能的一个步骤,那么我们可以把这一做法称为“劣化”。最后,请考虑一下:你如何将需要的值放入数组中的对象里,在放置的过程中你又付出了多大代价呢?在两个对象之间传值的最直接的方法就是赋值,但是赋值操作又会带来多大开销呢?对于许多类型而言,赋值的开销类似于调用一次析构函数(以销毁旧数值)加上一次构造函数(以复制新数值)。但是要知道,你的原始目标本来是避免构造和析构过程所带来的开销!请面对它:这样做一定不会得到好结果。(别妄想,用 vector 来代替数组也不会改善多少。)

编写必须返回一个新对象的函数,正确的方法就是:让这个函数返回一个新对象。对于 Rational 的 operator* 来说,这就意味着下面的代码是基本符合要求的:

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

显然地,这样做可能会招致对 operator* 的返回值的构造和析构过程的开销,但是从长远角度讲,付出这小小的代价可以获得更大的收益。而且,这一恐怖的清单可能永远不需要你来付账。就像其它编程语言一样,C++允许编译器的具体实现版本通过优化代码来提升性能,同时又不改变其固有的行为,在一些情况下,对 operator* 返回值的构造和析构过程可以被安全的排除。当编译器利用了这一事实时(编译器通常都会这样做),你的程序就可以继续按预期的行为执行,仅仅是更快了一些。

归根结底,当选择是使用引用返回,还是直接返回一个对象时,你的工作就是:做出正确的抉择,使程序拥有正确的行为。然后把优化工作留给编译器制造商,他们会使你的抉择变得尽可能的经济实用。

牢记在心

    • 对于局部的 / 分配于栈上 / 分配于堆上的对象,如果你需要将其中的任意一种作为函数的返回值

在必须返回一个对象时,不要去尝试返回一个引用

时间: 2024-07-29 05:44:16

在必须返回一个对象时,不要去尝试返回一个引用的相关文章

条款23: 必须返回一个对象时不要试图返回一个引用

class rational { public: rational(int numerator = 0, int denominator = 1); ... private: int n, d; // 分子和分母 friend const rational // 参见条款21:为什么 operator*(const rational& lhs, // 返回值是const const rational& rhs) }; inline const rational operator*(cons

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

条款21:必须返回对象时,别妄想返回其reference 引用只是对象的一种别名当使用引用的时候,请确认他的另一个身份在哪? class Rational { public: Rational(int x, int y) : m_x(x), m_y(y){} //返回const是属于类型保护,friend修饰,以后条款详说 friend const Rational operator + (const Rational &lhs, const Rational &rhs) { Ration

原则21:必须返回对象时,别妄想返回其引用

http://www.jianshu.com/p/35f26eea6cb3 Effective C++中第21个原则,因为引用是要指向某已存在的对象的,但如果该对象某一瞬间突然消失了,这个引用被架空了,那就出错了.为了证实这一点作者举了一个有理数相乘的例子.有这个一个有理数类,其中有一个有理数相乘的成员函数,该成员函数返回该有理数类的对象.在此例中该对象是一个本地对象,什么叫本地对象呢?就是一个普通的,局部的对象,它随着作用域的结束而被自动销毁.因为具备这一性质,一旦你把这个函数的返回值赋给某一

C++常见误区1 -- 必须返回一个对象时不要试图返回一个引用

1 #include <iostream> 2 3 class Person 4 { 5 public: 6 Person(){} 7 8 void setAge(int age) { this->m_iAge = age; } 9 10 int getAge() { return this->m_iAge; } 11 12 private: 13 int m_iAge; 14 }; 15 16 Person* CreatePerson() 17 { 18 Person p; 19

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

总结: 绝不要返回一个local栈对象的指针或引用:绝不要返回一个被分配的堆对象的引用:绝不要返回一个局部对象有可能同时需要多个这样的对象的指针或引用. 条款4中给出了“在单线程环境中合理返回局部静态对象的引用” 提出问题 一旦程序员抓住对象传值的效率隐忧,很多人就会一心一意根除传值的罪恶.他们不屈不挠地追求传引用的纯度,但他们全都犯了一个致命的错误:传递不存在的对象的引用.考虑一个用以表现有理数的类,包含一个函数计算两个有理数的乘积: class Rational { public: Rati

[021]必须返回对象时,别妄想返回其reference

引言 在条目20中,我们知道了值传递和引用传递的效率问题,因此在设计程序时,我们可能就尽可能来返回引用而不是值. 可是,可能会犯下面的一些错误:传递一些引用指向其实并不存在的对象. 第一节:返回临时变量的引用 假如我们有以下的例子,先看值传递 1 class A { 2 public: 3 A(int n = 0, int d = 1):n(n),d(d) {} 4 private: 5 int n,d; 6 friend const A operator* (const A& l, const

Effective C++ -----条款21:必须返回对象时,别妄想返回其reference

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象.条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例.

读书笔记_Effective_C++_条款二十一:当必须返回对象时,别妄想返回其reference

在栈空间的临时成员变量在函数生命期结束后无法传出 friend A& operator*(const A& a, const A& b) { A temp; temp.data = a.data*b.data;//a,b的成员变量相乘 return temp; } 既然栈空间不行,试试堆空间 friend A& operator*(const A& a, const A& b) { A *temp=new A; temp.data = a.data*b.da

C++对象模型之编译器如何处理函数返回一个对象

1.与经验不符的输出 我们知道,当发生以下三种情况之一时,对象对应的类的复制构造函数将会被调用: 1)对一个对象做显示的初始化操作时 2)当对象被当作参数传递给某个函数时 3)当函数返回一个类的对象时 所以,当我们设计一个函数(普通或成员函数)时,经验告诉我们,出于效率的考虑,应该尽可能返回一个对象的指针或引用,而不是直接返回一个对象.因为在直接返回一个对象可能会引起对象的复制构造过程,这意味着会发生一定量的内存复制和对象创建的动作,从而降低了程序的效率.这个设计的想法是正确的,但是实际上,当函