C语言中的指针的错误使用,是很多内存错误的根源。C++中引入了_引用(reference)_,来化解指针的毒。但是引用并不是一剂完全的解毒药,还有好多毒还不能解。下面这个毒就是个例子:

int& f() {
int a = 1;
return a;
}

上面的代码,函数​​f()​​​中返回了自身局部变量​​a​​​的引用。这是相当危险的行为,当​​f()​​​返回时,相应的栈上的内存消解了,局部变量也随之而亡。所以,使用​​f()​​的返回值是未定义的行为,轻则出现计算错误(获取到的值非预期值),重则出现程序错误(segmental fault)。

什么是未定义的行为?简单地说,C++把自己处理不了的情况都叫做未定义行为。未定义的行为越多,也就是在抽象完备性上存在漏洞,就会转化为编程时候的坑。一个靠谱的编译器,会为上述行为提出一个告警:

main.cc:4:10: warning: reference to stack memory associated with local variable 'a' returned [-Wreturn-stack-address]
return a;
^
1 warning generated.

所以,我们可以得出一个简单的结论,返回函数局部变量的引用,是不对的。(即使是C++11新增的右值引用(rvalue reference)也是不行的);对于局部变量,我们要勇敢地返回它的值,即便它是一个很臃肿的局部变量……

返回值优化

如果我们返回的局部变量很臃肿,是一个很大的结构体,该怎么办?总的来说,有两个办法:

  • 编译器帮你优化
  • 自己手动优化

先假设我们有一个很大的类,叫做Foo:

struct Foo {
int i;
// 假设这里有许许多多你看不见的属性
}

接着我们修改我们的f(),使他返回Foo:

Foo f() {
Foo a;
a.i = 101;
return a;
}

由于​​Foo​​​很大,当我们使用​​f()​​​的返回值,比如​​Foo foo = f();​​​的时候,按照常理,​​f()​​​的返回值会被赋予​​foo​​​,这个过程可能会发生拷贝构造、析构函数的调用,导致性能下降。这时候靠谱的编译器会自动进行返回值优化,避免这个拷贝。这个聪明的动作叫做​​Return Value Optimization​​。这就是所谓的编译器自动优化。那总有些情况,编译器是无法优化的,只好靠手动优化了。

1、使用const引用
我们可以把foo变成函数返回值的引用,比如:const Foo& foo = f();。由于是引用,所以就避免了拷贝。这里有几点需要注意。

引用必须是const来修饰。这是因为函数的返回值属于右值,也就是(rvalue)。普通的引用是左值引用,也就是lvalue reference。左值引用不能指向右值引用,只有const的左值引用才能用来指向右值。

本来f()的返回值是一个临时的变量,在它调用结束后,就应该销毁了。可是通过像这样const Foo& foo = f();,把临时的返回值赋值给一个const左值引用,f()返回值并不会立即销毁。这等于是在const引用的作用域内,延长了f()返回值的存活时间。

我们看一个示例:

#include <iostream>                         |#include <iostream>
|
using namespace std; |using namespace std;
|
class Point { |class Point {
public: | public:
Point(){ | Point(){
cout << "Point construcate" << endl; | cout << "Point construcate" << endl;
} | }
~Point(){ | ~Point(){
cout << "Point destrucate" << endl; | cout << "Point destrucate" << endl;
} | }
Point(const Point& p) { | Point(const Point& p) {
x = p.x; | x = p.x;
y = p.y; | y = p.y;
cout << "copy construcate" << endl; | cout << "copy construcate" << endl;
} | }
|
private: | private:
int x,y; | int x,y;
}; |};
|
|
Point test() { |Point test() {
Point p; | Point p;
return p; | return p;
} |}
int main(){ |int main(){
Point pp = test(); | const Point& pp = test();
return 0; | return 0;
} |}

我们为了去掉编译期自动优化,在编译的时候加上:g++ test_6.cpp -fno-elide-constructors

1)左边的输出:

Point construcate
copy construcate
Point destrucate
copy construcate
Point destrucate
Point destrucate

分析:

第一行是test()方法中Point p创建对象是的构造函数输出;第二行、第三行是test()方法返回值时调用的拷贝构造函数、析构函数;第四行、第五行是main方法中Point pp = test();通过一直对象给另外一个对象赋值时,调用的拷贝构造函数、析构函数;第六行是main方法结束析构pp对象的输出。

2)左边采用默认编译期优化:

编译时去掉-fno-elide-constructors,输出:

Point construcate
Point destrucate

不优化时在函数返回、函数赋值两处都会调用拷贝构造函数、析构函数;根据输出可以看到,编译器优化会把函数返回、函数赋值这两处的拷贝构造函数、析构函数给优化掉。

3)右边的输出:

Point construcate
copy construcate
Point destrucate
Point destrucate

分析:

第一行是test()方法中Point p创建对象是的构造函数输出;第二行、第三行是test()方法返回值时调用的拷贝构造函数、析构函数;第四行是main方法结束析构pp对象的输出。这里我们明显可以看出来:少了一次拷贝构造函数、析构函数的调用。

注:拷贝构造函数的参数如果不加const会报错:

test_6.cpp: In function ‘int main():
test_6.cpp:29:19: error: no matching function for call to ‘Point::Point(Point)
Point pp = test();
^
test_6.cpp:29:19: note: candidates are:
test_6.cpp:13:5: note: Point::Point(Point&)
Point(Point& p) {
^
test_6.cpp:13:5: note: no known conversion for argument 1 from ‘Point’ to ‘Point&
test_6.cpp:7:5: note: Point::Point()
Point(){

2、使用右值引用
C++11新增了一个引用类型,那就是右值引用(rvalue reference)。那什么是右值引用?这个似乎解释起来有点困难,顾名思义,右值引用是专门指向右值的引用(有点废话)。那什么是右值?等号左边的是左值,那等号右边的是右值吧?好像也不对,因为一个变量也可以出现在等号右边,赋值给另外一个变量。好吧,到底什么是右值?更准确的说,不能放在等号左边的,就是右值。就像1234这种字面值,或者前面提到的函数f()的返回值,这些都是不能放到等号左边的。

注:C++11引入的右值引用,是为了作为补充,和既有的左值引用有所区别。另外右值引用是C++11的特性,所以编译的时候要加上-std=c++11呢。

右值引用的写法是&&,所以可以把const Foo& foo = f();改写成右值引用形式:const Foo&& foo = f();。这样做看起来好像没有多大差别!那再改一下,把const去掉:Foo&& foo = f();。这就是右值的好处,不加const就可以直接指向右值,而且可以对右值进行更改,比如:foo.i = 122;。

我们看一个示例:

#include <iostream>

using namespace std;

class Point {
public:
Point(){
cout << "Point construcate" << endl;
}
~Point(){
cout << "Point destrucate" << endl;
}
Point(const Point& p) {
x = p.x;
y = p.y;
cout << "copy construcate" << endl;
}

private:
int x,y;
};


Point test() {
Point p;
return p;
}
int main(){
Point&& pp = test();
return 0;
}

编译:g++ test_8.cpp -fno-elide-constructors -std=c++11

输出:

Point construcate
copy construcate
Point destrucate
Point destrucate

根据输出我们可以看到,和使用const引用一样,都少了一次拷贝构造、析构函数的调用。

注意:右值引用本身是一个左值,所以左值引用可以指向一个右值引用。

参考:​​https://zh4ui.net/post/2018-08-07-cplusplus-return-value-or-reference/​