C++:关于函数返回的一件小事——是返回值还是返回引用?

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/

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值