条款21:必须返回对象时,别妄想返回其reference
Don’t try to return a reference when you must return an object.
本章分为两个部分。
在上一章中我们了解到,pass-by-value有很多效率方面的问题,因此pass-by-reference可能是一种比较好的方法。
但是!盲目的用reference可能会造成这样的错误:
- 开始传递一些references指向其实并不存在的对象!
举个例子,对于一个用以表现有理数(rational number)的class,内含一个函数用来计算一个有理数的乘积:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
//分子numerator和分母denominator
...
private:
int n, d;
friend
const Rational
operator* (const Rational& lhs,
const Rational& rhs);
};
在上面的代码中,这个版本的operator* 是以by value的方法返回其计算结果——一个rational对象。对于这样的返回方法,代价如何?
假如说我们进行修改,使用reference进行传递,就不需要付出代价了。但是:
- 所谓的reference,只是一个名称,代表着某个既有对象。即,它一定是某物的另一个名称。
就像上面的operator* ,如果他返回一个reference,那么后者一定指向某个既有的Rational对象,内含两个Rational对象的乘积。
因此,我们不能期望这样一个内含乘积的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对象”并不合理。如果operator* 要返回一个reference指向这个数值,它就必须自己创建这个Rational对象!
对象的创建
一般来说,函数创建新对象有两种方法:
- 在stack空间创建
- 在heap空间创建
Stack
如果我们定义一个local变量,就是在stack空间创建对象。根据这个策略,尝试写一下operator*:
const Rational& operator* ( const Rational& lhs,
const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //使用了构造函数实现,但是非常糟糕!
return result;
}
对于上面这个方法,是一个比较糟糕的办法!因为我们的目标是避免使用构造函数,而result却必须用构造函数的方法来进行构造。
更严重的是,这个函数返回一个reference指向result,但是result是一个local对象,而local对象在函数退出之前就被销毁了。因此,此时operator* 所指向的Rational,是一个已经被销毁的Rational!于是,此时将会陷入“无定义行为”的困境。
简单总结一句话:
- 任何函数如果返回一个reference指向某个local对象,都会产生必然的错误!
Heap
因此,我们考虑在heap内构造一个对象,并返回reference指向它。
Heap-based对象是由new创建的,因此我们需要写一个heap-based operator* ,形式如下:
const Rational& operator* ( const Rational& lhs,
const Rational& rhs)
{
Rational* result = new Rational(lhs.n * rhs.n, lhs.n * rhs.d); //更为!糟糕的写法!
return *result;
}
在上面的代码中,我们依然需要付出一个“构造函数调用”的代价,因为分配获得的内存将以一个适当的构造函数完成初始化动作。
然而,此时还有一个更为严重问题:
- 谁应该为被new出来的对象实施delete??
即使我们十分谨慎,还是会在合情合理的使用下,造成内存泄漏:
Rational w, x,y,z;
w = x * y * z; //与operator*(operator*(x, y), z)相同
在上面的代码中,同一个语句调用了两次operator*,因此使用了两次new,因此也就需要两次delete。
但是,并没有合理的办法让operator*的使用者进行哪些delete调用,因为没有合理的办法让他们取得operator* 返回的references背后隐藏的那个指针。
- 这势必会造成内存泄漏!