一、何时必须用传值
一旦把注意力都转向了对象传值方式隐含的效率问题(参见第 20 条)时,对传值方法采取斩草除根的态度,在追求传递引用方式的纯粹性的同时,也犯下了致命的错误:有时候传递的引用所指向的对象并不存在。
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);// 第 24 条中解释了为什么这里的构造函数没有显性声明。
...
private:
int 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的Rational对象并不合理,如果要返回这样一个对象,必须自己创建。
二、陷入错局
上面分析,返回类型如果是引用类型,就必须存在一个Rational对象,这个对象必须自己创建,而创建这样的对象,一般有两种方法:在栈空间上创建、在堆空间上创建;下面分别探讨这两种方式存在的陷阱;
2.1 在栈空间建立
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对象已经毫无关系,因为它已经被销毁了。对于所有的调用者而言,只要稍稍触及这一函数的返回值,都会遭遇到无尽的无法预知的行为。事实上,任何函数,如果返回一个指向局部对象的引用,都将一败涂地。(任何返回指向局部对象的指针的函数,也是一样。)
2.2 在堆空间建立对象
现在我们考虑在推空间建立一个对象,并返回一个引用指向它,堆空间的对象由new创建,那么有如下代码:
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);// 警告!更严重的错误
return *result;
}
这时,还是要付出调用构造函数的代 价,这是因为通过 new 分配的内存要通过调用一个合适的构造函数来初始化,还有一个更严重的问题:谁来释放空间?那么,随之而来的事情就是内存泄漏!
2.3 static对象
栈空间与堆空间都面临着同一个问题:它们都需要为 operator* 的每一个返回值调用一次构造函数。也许你能够回忆起我们最初的目的就是避免像此类构造函数调用。也许你认为你知道某种方法来将此类构造函数调用的次数降低到仅有一次。也许你想到了下面的实现方法:让 operator* 返回的引用指向一个函数的内部的static 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);
Rational a, b, c, d;
...
if ((a * b) == (c * d)) {
当乘积相等时,执行恰当的操作 ;
} else {
当乘积不相等时,执行恰当的操作 ;
}
其中(a * b) == (c * d);
等价于operator==(operator*(a, b), operator*(c, d))
;即在调用 operator== 时,已经先两次对 operator* 调用了,每次调用时都回返回一个指向 operator* 内部的静态 Rational 对象的引用。于是编译器将要求 operator== 去将 operator* 内部的静态 Rational 对象值与operator* 内部的静态 Rational 对象值相比较,这样结果总是相等的,造成逻辑错误(可参照static的特性)。
三、总结
重载乘号操作符容易犯以下几个错误:
1、返回一个局部对象的引用,局部对象已经销毁
2 、在堆上创建一个对象,然后返回它的引用,无法去delete它,造成内存泄漏
3、返回静态对象的引用,可能导致逻辑错误。
牢记:
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
所以,当你看到一个引用的声明时,你应该立刻问一下你自己:它的另一个名字是什么,因为一个引用作指向的内容必定有它自己的名字。于是对于上面的 operator* 而言,如果它返回一个引用,那么它所引用的必须是一个已存在的 Rational 对象,这个对象中包含着需要进行乘法操作的两个对象的乘积。
文章内容来自:Effective C++(侯捷译)