代码改变世界

Effective C++ 学习笔记(16)

2011-08-04 21:46  Daniel Zheng  阅读(257)  评论(0编辑  收藏  举报

必须返回一个对象时不要试图返回一个引用


看一个表示有理数的类,其中包含一个友元函数,用于两个有理数相乘:

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*(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

  很明显,这个版本的operator*是通过传值返回对象结果,如果不去考虑对象构造和析构时的开销,你就是在逃避作为一个程序员的责任。另外一件很明

显的事实是,除非确实有必要,否则谁都不愿意承担这样一个临时对象的开销。那么,问题就归结于:确实有必要吗?

  答案是,如果能返回一个引用,当然就没有必要。但请记住,引用只是一个名字,一个其它某个已经存在的对象的名字。无论何时看到一个引用的声明,

就要立即问自己:它的另一个名字是什么呢?因为它必然还有另外一个什么名字(见条款M1)。拿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*:

// 写此函数的第一个错误方法
inline const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational result(lhs.n
* rhs.n, lhs.d * rhs.d);
return result;
}

  这个方法应该被否决,因为我们的目标是避免构造函数被调用,但 result必须要象其它对象一样被构造。另外,这个函数还有另外一个更严重的问题,它返回的是一个局部对象的引用,关于这个错误,条款31 进行了深入的讨论。

  那么,在堆上创建一个对象然后返回它的引用呢?基于堆的对象是通过使用new 产生的,所以应该这样写operator*:

// 写此函数的第二个错误方法
inline const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational
*result =
new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}

  首先,你还是得负担构造函数调用的开销,因为 new 分配的内存是通过调用一个适当的构造函数来初始化的(见条款5 和M8)。另外,还有一个问题:谁将负责用delete 来删除掉new 生成的对象呢?

  实际上,这绝对是一个内存泄漏。即使可以说服 operator*的调用者去取函数返回值地址,然后用delete 去删除它(绝对不可能——条款31 展示了这样的代码会是什么样的),但一些复杂的表达式会产生没有名字的临时值,程序员是不可能得到的。例如:

Rational w, x, y, z;
w
= x * y * z;

  两个对 operator*的调用都产生了没有名字的临时值,程序员无法看到,因而无法删除。(再次参见条款31)

  也许,你会想你比一般的熊——或一般的程序员——要聪明;也许,你注意到在堆栈和堆上创建对象的方法避免不了对构造函数的调用;也许,你想起了我们最初的目标是为了避免这种对构造函数的调用;也许,你有个办法可以只用一个构造函数来搞掂一切;也许,你的眼前出现了这样一段代码:operator*返回一个“在函数内部定义的静态Rational 对象”的引用:

// 写此函数的第三个错误方法
inline const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
static Rational result; // 将要作为引用返回的
// 静态对象
lhs 和rhs 相乘,结果放进result;
return result;
}

  这个方法看起来好象有戏,虽然在实际实现上面的伪代码时你会发现,不调用一个Rational 构造函数是不可能给出result 的正确值的,而避免这样的调用正是我们要谈论的主题。就算你实现了上面的伪代码,但,你再聪明也不能最终挽救这个不幸的设计。

  想知道为什么,看看下面这段写得很合理的用户代码:

  

bool operator==(const Rational& lhs, // Rationals 的operator==
const Rational& rhs); //
Rational a, b, c, d;
...
if ((a * b) == (c * d)) {
处理相等的情况;
}
else {
处理不相等的情况;
}

  看出来了吗?((a*b) == (c*d)) 会永远为true,不管a,b,c 和d 是什么值!用等价的函数形式重写上面的相等判断语句就很容易明白发生这一可恶行为的原因了:

  

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

  所以,写一个必须返回一个新对象的函数的正确方法就是让这个函数返回一个新对象。对于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*的返回值会被安全地除去(见条款M20)。当编译器采用了这种优化时(当前大部分编译器这么做),程序和以前一样继续工作,只不过是运行速度比你预计的要快而已。

  以上讨论可以归结为:当需要在返回引用和返回对象间做决定时,你的职责是选择可以完成正确功能的那个。至于怎么让这个选择所产生的代价尽可能的小,那是编译器的生产商去想的事。