1.前言
一旦程员意识到pass-by-value的效率问题,往往就会陷入一种误区,一心一意根除pass-by-value带来的种种问题。在坚定追求pass-by-reference的路上,肯定会进入一个误区,即:开始传递一些references指向不存在的对象。这可不是件好事。
2.举例说明
考虑一个用以表现有理数的class,内含一个函数用来计算两个有理数的乘积:
class Rational{
public:
Rational(int numerator=0,int denominator=1);
....
private:
int n,d;
const Rational operator*(const Rational& lhs,const Rational& rhs);
};
这个版本的operator* 是以by value方式返回其计算结果(一个对象),如果改而传递reference,就不需要考虑构造函数和析构函数带来的开销。但是所谓reference只是个名称,代表某个即有对象。
任何时候看到一个reference声明式,都应该立刻问自己,它的别名是啥。因为它一定是某对象的另一个名称,如果它返回一个reference,后者一定指向某个既有的Rational对象,内含两个Rational对象的乘积。
首先,我们不能期望这样一个Rational对象在调用operator*之前就存在,也就是说,如果你有:
Rational a(1,2);//a=1/2
Rational a(3,5);//b=3/5
Rational c=a*b;//c=3/10
期望“原本就存在一个其值为3/10的Rational对象”并不合理。如果operator*要返回一个reference指向这样一个数值,它必须自己创建那个Rational对象。
3.函数创建新对象的途径
函数创建新对象的途径有二:在stack空间或者heap空间创建。如果定义一个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对象在函数退出之前已经被销毁了。因此这个版本的operayor*并未返回reference指向某个Rational,它返回的reference已经被销毁了。
因为事情的真相就是任何函数如果返回一个reference指向某个local对象,都将一败涂地。
假设让我们考虑在heap内构造一个对象,并返回reference指向它。Heap_based对象由new创建,所以你得写一个heap-base operator* 如下:
const Rational& operator*(const Rational& lhs,const Rational& rhs)
{
Rational* result=new Rational(lhs.n*rhs.n,lhs.d*rhs.d)l//警告,更糟糕的写法
return *result;
}
这里还是必须付出一个"构造函数调用"代价,因为分配所得到的内存将以一个适当的构造函数完成初始化。但现在面临着另外一个问题:谁该对这被你New出来的对象实施delete?
即使调用者诚实谨慎,并且具备良好的意识,他们还是不太能够在这样合情合理的用法下阻止内存泄漏:
Rational w,x,y,z;
w=x*y*z;//与operator*(operator*(x,y),z)相同
这里,同一个语句内调用了两次operator* ,因为两次使用new,也就需要两次delete。但却没有合理的办法让operator*使用者进行delete调用因为没有合理的办法取得operator*返回的reference背后隐藏的那个指针。这绝对导致资源泄漏。
总而言之,或许我们注意到了,上述不论on-the-stack或on-the-heap做法,都因为对operator*返回的结果调用构造函数而受到惩罚。
而我们最初的目标是避免构造函数的调用。或许你认为你知道有一种方法可以避免任何构造函数的调用,然后心里出现这样的实现代码,该方法基于“让operator*返回的reference指向一个被定义于函数内部的static Ratioanl对象”:
const Rational& operator*(const Rational& lhs,const Rational& rhs)
{
static Rational result;static对象,此函数将返回其reference
result=...;将lhs乘以rhs,并将结果置于result
return result;
}
就像所有用上static对象设计的一样,这一个也同样造成我们对多线程安全性的疑虑。不过这还只是比较浅显的弱点,如果想看更深层次的瑕疵,参考以下代码:
bool operator==(const Rational& lhs,const Rational& rhjs);//针对Rational写的operator=
Rational a,b,c,d;
....
if((a*b)==(c*d))
{
当乘积相等,相关操作;
}else
{
当乘积不等,做相应的动作;
}
程序运行的结果显示((a*b)==(c*d))总是被核算为true,不论a,b,c和d的值是什么.
这里需要注意:在operator==被调用之前,已经有两个operator*调用式起作用,每一个都返回reference指向operator*内部定义的static Ratioanl对象。因此operator==被要求将“operator*内的static Rational对象值”拿来和“operator*内的static Rational对象值做比较”,如果结果值不相等才奇怪。(两次operator*调用的确改变了static Rational对象值,但由于它们返回的是reference,因此调用端看到的永远是static Rational对象值的”现值“)。
一个”必须返回新对象“函数的正确写法是:就让那个函数返回一个新对象。对Rational的operator*而言,意味着以下写法:
inline const Rational operatorr* (const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.n*rhs.n,lhs.d*rhs,d);
}
当然,我们需要承受operator*返回值的构造成本和析构成本,然而长远来看,那只是为了获得正确行为而付出的小小代价。