条款21:必须返回对象时,别妄想返回其reference
此条款,也我刚刚工作时踩过的坑,一个功能总是莫名奇妙的数据丢失,调查的时候就是返回值指针总是在特定逻辑下返回NULL,就是因为我返回的是一个局部变量。
跟随着上一个条款的思路延申,看看引用还有那些使用注意。
一、三种错误方法
用原书例子,这里有一个无理数的类,它包含将两个有理数相乘的函数:
class Rational
{
public:Rational(int numerator = 0, int denominator = 1);
private:int n, d; //分子和分母friend const Rational operator*(const Rational& lhs, const Rational& rhs); //划重点
};
在这里,重载乘号运算符(Operator*) 为值传递方式返回结果。
当看到值传递的时候,看过条款20的人都会开始考虑开销问题,然后就开始想方设法的开始使用引用传递来处理,比如以下实现以下操作:
Rational a(1, 2);
Rational b(3, 5);
Rational c = a * b;
不可避免的问题是,如果operator*即将返回一个有理数的引用,它必须自己创建出来这个有理数。
1、在栈上创建reference指向的对象
大聪明一号,选择通过定义一个本地变量来完成栈上的对象创建,实现方法如下:
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // warning! 糟糕的代码return result;
}
这种方法对么,当然不对,其原因是:
- 我们的目标是避免调用构造函数,但是这里的result必须被构造出来。
- 而且,这个函数所返回指向result的引用,是一个局部对象,当函数退出的时候,这个对象就会随之销毁。
因此,不仅目的没有有效达到,同时,任何使用这个函数的返回值的调用者都将会马上进入未定义行为的范围,请排除这种方法!
2、在堆上创建reference指向的对象
比较聪明的大聪明二号,选择通过在堆内构造一个对象,并返回引用指向它,实现方法如下:
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{Rational *result = new Rational(lhs.n*rhs.n, lhs.d*rhs.d); //更糟糕的写法return *result;//虽然编译器不报错,但是逻辑上是错误的
}
这种方法对么,当然也不对,要考虑以下内容:
- 避免调用构造函数的目的,同样没有有效达成。
- 而且,作为一个成熟的程序员,就要多考虑一步:谁该对着被你new出来的对象实施delete?
比如下面这种合理的使用场景,内存泄漏将不可避免:
Rational w, x, y, z;
w = x*y*z; //相当于operator*(operator*(x,y),z);
请问 operator*(x,y) 有指针接么?答案是 没有 。
3、为reference创建 static对象
更聪明的大聪明三号,选择了静态对象(static);
他的考虑是 静态对象具有 只需要调用一次构造函数,其余的构造函数将避免调用 的特点,利用此可以达成 避免调用构造 函数目标;
因此,他是这么做的:
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{ // warning, 又一堆烂代码static Rational result; //静态局部变量result=...; //将lhs乘以rhs,然后将结果保存在result中return *result; //虽然编译器不报错,但是逻辑上是错误的
}
像所有使用静态对象的设计一样,这种方法增加了对于线程安全的梳理工作,但这个缺点是比较明显的。
为了看一下更深层次的缺陷,考虑下面这些完全合理的客户代码:
bool operator==(const Rational& lhs, const Rational& rhs); // for Rationals
Rational a, b, c, d;
...
if ((a * b) == (c * d)) {//乘积相等,做相应动作
} else {//不相等,做相应动作
}
当运行起来就会发现一个bug,即 表达式 ( (ab) == (cd) ) 的求值结果总为 true 。
这是为什么?
一句话:(a * b) 和 (c * d)都修改了同一个静态对象。
二、正确方法
一个“必须返回新对象”的函数的正确写法是:让函数返回新的对象。对Rational的opertaor*函数来说,其实现如下面的代码(或者与其等价的代码):
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
在返回一个引用还是返回一个对象之间做决定时,你的工作是选择 能够提供正确行为 的那个;
对于“如何使这个选择有尽可能小的开销”这个问题的解决,让编译器供应商去努力把。
三、总结
绝不要返回指针/引用指向一个局部stack对象,或返回一个引用指向heap-allocated对象,或者返回一个引用/指针指向一个静态局部变量。
条款4已经为“在单线程中合理返回引用指向一个静态局部变量”提供了一份设计实例。