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/