结论
这篇文章没什么意思,就是告诉大家一个常识,函数不要返回引用类型的临时对象,也不要引用函数返回的临时对象,因为无法预测会发生什么。
但是!我就是想看看这么做的糟糕后果是什么!
定义
先要搞清楚什么是临时对象,这里我参考 《More Effective C++》99页定义。真正的临时对象是在源码中不可见的,是栈上的、没有名字的对象。与函数内定义的临时对象有根本差别。
有两种情况:
第一种:当触发隐式类型转换时
例如:
// 统计ch在str中出现的次数
size_t counterChar(const string& str, char ch);
char cs[] = "123";
cout << counterChar(cs, '1') << endl;
此时,char[] 隐式转换为 string,再引用为str。该隐式转换的string,会在counterChar返回时销毁。
第二种:当函数返回一个对象的时候
例如:
const Number operator+(const Number& lhs, const Number& rhs);
在函数返回时,会产生一个临时对象作为返回值(当然编译器会帮你优化它,所以不一定有一个临时对象生成和销毁)。
实验:
对象的类实现为:
class Cat {
string name;
public:
Cat(string name): name(name)
{
cout << "new cat " << name << endl;
}
Cat(const Cat& cat)
{
cout << "copy " << name << endl;
this->name = cat.name;
}
void sleep() const
{
cout << "sleep " << name << endl;
}
~Cat()
{
cout << "destroy cat " << name << endl;
}
};
很简单,为了看清现象打了一些日志。
情况一(正常情况):函数使用值返回,同样拷贝赋值来保存函数返回值。
Cat foo1()
{
Cat a("a");
Cat b("b");
return b;
}
int main()
{
Cat c = foo1();
c.sleep()
cout << "end mainn";
}
下面来观察运行结果:
new cat a
new cat b
copy
destroy cat b
destroy cat a
sleep b
end main
destroy cat b
简单解释一下:foo1运行到return b时,会执行一次拷贝构造函数。此时,main函数的c对象构造完成。之后会将两个局部对象销毁。
情况二:函数返回值为引用型
Cat& foo1()
{
Cat a("a");
Cat b("b");
return b;
}
在VS的Debug模式中,会先执行函数局部对象的销毁,再返回这个局部对象的引用。之后调用Cat的拷贝构造函数。由于引用的对象已经被销毁。故会报错。
再Release模式中,现象很神奇:
new cat a
new cat b
destroy cat b
destroy cat a
copy
sleep
end main
destroy cat
也就是说,对象c为虽然执行了拷贝构造函数,但并没有复制name成员。导致对象c的name未知。
情况三:引用函数的返回对象
Cat foo1()
{
Cat a("a");
Cat b("b");
return b;
}
int main()
{
const Cat &c = foo1(); // 由于函数返回值为右值,这里必须是const引用
c.sleep()
cout << "end mainn";
}
在Debug模式下,现象与情况一相同,变量c引用的是一个匿名对象。
new cat a
new cat b
copy
destroy cat b
destroy cat a
sleep b
end main
destroy cat b
在Release模式下,很有意思
new cat a
new cat b
destroy cat a
sleep b
end main
destroy cat b
由于编译器的优化,foo函数局部变量b真的被main函数局部变量c所引用了,其内存释放也是在main函数返回时完成的。
综上所述
不要试图使用"引用"来减少构造和析构函数的调用。如果你非要优化的话,建议使用返回值优化(RVO)。
Cat foo2()
{
return Cat("b");
}
如此一来,main函数中的变量c就不会调用拷贝构造了。