Article21:必须返回对象时,别妄想返回其Reference
- 1. operator* 以by value 方式返回一个结果
- 2. operator* 以 by Reference 方式返回一个结果
- 3 定义static Rational 对象
- 总结
本章主要介绍:函数返回值两种类型:值类型返回和引用返回通过实例来说明,这两种返回类型的差别。
在条款20中,已经领悟了 pass-by-value 的效率牵连层,很多人一心想要根除 传值的罪恶,坚定的追求 pass-by-reference,但是
这也许会犯下一个致命错误:开始传递一些 reference 指向其实并不存在的对象,这可不是件好事。
💚💚 例子: 考虑一个用以表示有理数(Rational numbers)的class ,内容包含一个函数计算两个有理数的乘积。
#include<iostream>
using namespace std;
// g++ -std=c++11 main_3.18.cpp -fno-elide-constructors
class Rational {
public:
Rational()
{
cout<< "default Rational construct......."<<this<< endl;
}
// 分子 numerator,分母:denominator
Rational(int numerator =0,int denominator =1)
{
cout<< "Rational construct......."<<this<< endl;
n = numerator;
d= denominator;
}
~Rational() {
std::cout << "destruct this object!: " << this<< std::endl;
}
Rational(const Rational& rat){
std::cout << "Rational copy construct!: " << this << std::endl;
}
Rational(Rational&& rat){
std::cout << "Rational rvalue construct!: " << this << std::endl;
}
private:
int n,d;
friend const Rational operator* (const Rational& lhs,const Rational& rhs){
std::cout << "Rational operator*: "<< std::endl;
}
};
int main()
{
Rational a(1,2);
Rational b(3,5);
Rational c = a*b;
return 0;
}
// 打印结果
Rational construct.......0x61fe00
Rational construct.......0x61fdf8
Rational operator*:
Rational copy construct!: 0x61fdf0
destruct this object!: 0x61fe08
destruct this object!: 0x61fdf0
destruct this object!: 0x61fdf8
destruct this object!: 0x61fe00
1. operator* 以by value 方式返回一个结果
friend const Rational operator* (const Rational& lhs,const Rational& rhs);
当以 by value 的方式返回其 计算结果(一个对象)时,如果你完全不考虑该对象的 构造和析构成本,那么你就是逃避了专业责任,若非必要,这种代价是不必要付出的。
💚💚 可以看到 这里 const Rational opertor*() 计算结果返回时:有一个构造函数和一个 拷贝构造函数的成本支出。
如何节约这种成本的支出了? 试试,通过 以 by reference 的方式返回 ?
2. operator* 以 by Reference 方式返回一个结果
- 如果可以改而传递 reference,就不需要付出代价,但是你也应该记住,所谓 Reference 只是一个名称,代表某个既有对象,在任何时候看到一个 reference 声明式,你应该立刻问问自己:它的另一个名称是什么? 因为它一定是某物的另一个名称。
- 以上述 operator* 为例,如果它返回一个 refernece ,那么后者一定指向某个既有的 Rational 对象,这个对象 内含两个 Rational 对象的乘积。
- 那么函数创建这个对象的途径有两个:在 stack 空间或在 Heap堆空间,在这里你应该考虑定义一个 局部变量(在 stack 上创建)。
💚💚(一) 如果我们这样创建 :考虑在 栈上构造一个对象
const Rational& operaot*(const Rational& lhs, const Rational& rhs)
{
Rational result (lhs.n * lhs.n, rhs.n* rhs.n);
return result;
}
- 一方面,我们本来就是要减少构造函数的支出成本,而 result 却必须像任何对象一样由构造函数构造起来
- 更严重的是:函数返回一个 reference 指向 result ,但是 这个 result 是一个 local 变量,local 对象在函数退出前被销毁,因此这个 operator* 并未返回 reference 指向的 Rational ,而是指向一个 旧的,从前的 Rational 。这将导致 “无定义行为”的恶地。
💚💚(二):我们考虑在堆上构建一个对象
friend const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational* result = new Rational(*(lhs.n * lhs.n), *(rhs.d* rhs.d));
return *result;
}
💚 对于基础不牢的同学,可能会犯如下错误。
- 从上面的打印可以看出:在堆上构造一个Rational对象 ,还是有构造函数和拷贝构造函数以及析构函数的开支花销。
- 更为严重的是:你在堆上new 出来的对象,谁应该对此对象实施 delete ? (即没有合理的办法让他们取得 operator* 返回reference背后隐藏的那个指针,这肯定会导致资源泄漏)。
3 定义static Rational 对象
上述两个小节中,不论是在 on-the-stack上或 on-the-heap的做法,都因为对 operator* 返回的结果调用构造函数而受到惩罚,你可能现在想到了一种方法,避免构造函数被调用。
💚💚 让operator* 返回的reference 指向一个被定于函数内部的 static Rational 对象。
friend const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational* result;
result = new Rational(lhs.n * lhs.n,rhs.d*rhs.d);
return *result;
}
// 打印结果
Rational construct.......0x61fe08
Rational construct.......0x61fe00
Rational construct.......0xe927c0
Rational copy construct!: 0x61fdf8
destruct this object!: 0x61fdf8
destruct this object!: 0x61fe00
destruct this object!: 0x61fe08
💚💚 那么上面返回 static Rational 对象这种方式有什么问题了?
- 多线程安全性的疑虑,这是个显而易见的弱点
- 由于 static 对象储存在 全局区,调用者看到永远是 static Rational对象的现值。这个虽然满足目前场景,但是如果你需要比较两个等式结果的话,就会出现问题如 :
bool operator==(const Rational& lhs,const Ration& rhs);
Rational a,b,c,d;
if ((a*b) == (c*d))
{
}
// 表达式永远是 true , 因为
3. 在 operator== 被调用前,operator* 两个表达式每一个都是: 返回 Reference 指向 operaotr* 内部定义的 static Rational
4. 因此 operator== 实际上是要求将:operator* 内的 static Rational 对象值 和operator* 内的 static Rational 对象值进行比较,这个肯定是相等的。
总结
- 在需要返回 “一个对象的时候”。你就返回一个新对象,如上述例子,对Rational 的 operator* 而言,就应该返回一个对象
inline const Rational operaotr*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n*lhs.n, rhs.d*rhs.d);
}
- 虽然返回一个对象,可能带来构造和析构的成本,但是C++是允许编译器实施最优化,用以改善代码的效率,因此某些情况下operator* 返回值的构造和析构可被安全的消除。