最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
在上一条条款中,我们了解到引用传递的高效率,但这并不说明值传递是无用的,若一心只想着使用引用传递,可能会犯下一个致命的错误:开始传递一些引用指向并不存在的对象。
设计一个表示有理数的类,内含一个函数用来计算两个有理数的乘积。
class Rational{ //表示有理数的类
public:
Rational(int numerator = 0, int denominator = 1); //条款24说明为什么这个构造函数不声明为explicit
...
private:
int n,d; //分子n与分母d
//条款3说明为什么返回类型是 const
friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};
这里的 opeator*
按值返回一个常量,就必定会在调用处生成本地拷贝,那么我们可不可以通过返回一个引用来避免拷贝带来的高成本呢? 就像这样:
friend const Rational& operator*(const Rational& lhs, const Rational& rhs);
这个想法很美好。但是请记住,所谓引用只是个名称,代表已经存在的某个对象。任何时候看到一个引用声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。如果 opeator*
需要返回引用,那么它必须创建一个对象,然后返回指向此对象的引用。函数创建新对象的方式又两种,在栈(stack)空间或在堆(heap)空间创建。
-
栈空间创建
const Rational& operator*(const Rational& lhs, const Rational& rhs){ Rational result(lhs.n*rhs.n, lhs.d*rhs.d);//创建局部变量 return result; }
你必须拒绝这种做法,因为返回引用是为了避免调用构造函数,而
result
却必须由构造函数构造出来。更严重的是,这个函数返回一个指向局部变量的引用,局部变量的生命周期在函数内,一旦退出函数,局部变量就会被销毁,所以函数实际返回的是指向不存在的对象的引用。 -
堆空间创建
const Rational& operator*(const Rational& lhs, const Rational& rhs){ Rational* result = new Rational(lhs.n*rhs.n, lhs.d*rhs.d); return *result; }
这种方式也不可避免构造函数的调用,因为分配所得的内存将以一个适当的构造函数完成初始化动作。但出现了一个新问题:谁来
delete
被new
出来的对象?Rational w,x,y,z; w = x * y * z; //等价于operator*(operator*(x,y),z)
这里,同一个语句调用了两次
operator*
,因而两次使用new
,也就需要两次delete
。但却没有合理的方法让operator*
使用者进行那些delete
调用,因为他们无法取得operator*
返回的引用背后隐藏的那个指针。这绝对导致内存泄漏。
上述代码不论是在栈中还是在堆中创建对象返回引用,都无法避免构造函数的调用。我们的目标是避免任何构造函数的调用,那么是否可以使用静态变量来达到目标呢?
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的等值运算符
Rational a,b,c,d;
...
if((a * b) == (c * d)){
//当乘积相等时
}else{
//当乘积不等时
}
表达式 (a * b) == (c * d)
的值永远都是 true,不论a,b,c,d的值是什么。该表达式等价于 operator==(operator*(a,b),operator*(c,d))
,从中可看出在 operator==
被调用前,已有两个 operator*
被调用,而它们返回的引用都是指向同一个对象——静态变量result
,自己和自己比较,当然是返回true。
一个“必须返回新对象”的函数的正确写法是:让那个函数返回一个新对象。
inline const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
总的来说,当你必须在“返回一个引用和返回一个新对象”之间抉择时,你的工作就是挑出最正确的那个。其中造成的成本,让编译器为你尽可能降低成本,因为C++编译器自带优化功能,生成的机器代码会在保证不影响可观测范围内结果的前提下提升效率。
Note:
- 绝不要返回指向一个局部变量的指针或引用,或返回引用指向一个 堆分配对象,或返回指针或引用指向一个局部静态对象而有可能同时需要多个这样的对象。(条款4有“在单线程环境下合理返回引用指向一个局部静态对象”的例子)