返回值和右值引用的传递问题

本文探讨了C++中move语义的应用,包括move构造函数和move赋值运算符的作用。通过对不同函数返回值方式的实验,揭示了编译器优化及move语义在对象传递过程中的实际效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

测试类

  测试类结构如下:

[cpp]  view plain  copy
  1. class Test2  
  2. {  
  3. public:  
  4.     Test2() {}  
  5.     Test2(const char* str);  
  6.     Test2(const Test2& o);  
  7.     Test2(Test2&& o);  
  8.     virtual ~Test2();  
  9.     Test2& operator=(const Test2& o);  
  10.     Test2& operator=(Test2&& o);  
  11.     void swap(Test2& o);  
  12.     const char* cstr() const { return _blocks ? _blocks : ""; }  
  13. protected:  
  14.     char* _blocks;  // 保存字符串的缓冲区  
  15. };  

  可以看到,这个类中包含了C++11标准中规定的若干元素:

  • 默认构造函数(可缺省);
  • 参数构造函数(可缺省);
  • 析构函数;
  • copy构造函数;
  • move构造函数(转移构造函数);
  • copy赋值运算符;
  • move赋值运算符(转移赋值运算);
  • 对象交换函数;
  其中,关键的几个函数实现如下:
[cpp]  view plain  copy
  1. /** 
  2.  * 参数构造器 
  3.  * @param [in] str 字符串值 
  4.  */  
  5. Test2::Test2(const char* str) :  
  6.     _blocks(NULL)  
  7. {  
  8.     if (str)  
  9.         _blocks = ::strdup(str);  
  10. }  
  11.   
  12. /** 
  13.  * 拷贝构造函数 
  14.  * @param [in] o 同类型的另一个对象引用 
  15.  */  
  16. Test2::Test2(const Test2& o) :  
  17.     _blocks(NULL)  
  18. {  
  19.     if (o._blocks)  
  20.         _blocks = ::strdup(o._blocks);  
  21. }  
  22.   
  23. /** 
  24.  * Move构造函数 
  25.  * @param [in] o 同类型的另一个对象右值引用 
  26.  */  
  27. Test2::Test2(Test2&& o) :  
  28.     _blocks(NULL)  
  29. {  
  30.     swap(o);  
  31. }  
  32.   
  33. /** 
  34.  * 析构函数 
  35.  */  
  36. Test2::~Test2()  
  37. {  
  38.     if (_blocks)  
  39.         ::free(_blocks);  
  40.     _blocks = NULL;  
  41. }  
  42.   
  43. /** 
  44.  * 赋值运算符重载 
  45.  * @param [in] o 同类型的另一个对象引用 
  46.  * @return 当前类型的另一个引用 
  47.  */  
  48. Test2& Test2::operator=(const Test2& o)  
  49. {  
  50.     if (this != &o)  
  51.         Test2(o, int()).swap(*this);  
  52.     return *this;  
  53. }  
  54.   
  55. /** 
  56.  * 右值引用赋值运算符重载 
  57.  * @param [in] o 同类型的另一个对象右值引用 
  58.  * @return 当前类型的另一个引用 
  59.  */  
  60. Test2& Test2::operator=(Test2&& o)  
  61. {  
  62.     if (this != &o)  
  63.     {  
  64.         swap(o);  
  65.         o.~Test2();  
  66.     }  
  67.     return *this;  
  68. }  
  69.   
  70. /** 
  71.  * 交换两个对象 
  72.  * @param [in] o 同类型的另一个对象 
  73.  */  
  74. void Test2::swap(Test2& o)  
  75. {  
  76.     std::swap(_blocks, o._blocks);  
  77. }  
  突然想了解一下具有move构造函数和move赋值运算的类,在对象传递时会发生什么情况,所以写了下面的几个函数进行测试。


第一个函数,返回函数内部产生的局部变量:

[cpp]  view plain  copy
  1. /** 
  2.  * 测试返回内部具备变量 
  3.  * @return 返回临时生成的对象 
  4.  */  
  5. Test2 return_object()  
  6. {  
  7.     Test2 res = "test";  
  8.     return res;  
  9. }  
通过如下代码测试
[cpp]  view plain  copy
  1. Test2 t1 = return_object();  
  2. t1 = return_object();  
结论:
  1. 第一行代码中,只在调用函数内部执行了一次参数构造函数(构造局部对象),没有发生copy构造函数(或者move构造函数)的调用,即可以认为在函数内部实例化的局部对象就是返回值变量t1,这应该是编译器优化的结果;
  2. 第二行代码执行时,变量t1已经被初始化,所以赋值运算是必然会发生的,此时除过在调用函数内部执行了一次参数构造函数(构造局部对象)外,还执行了一次move赋值运算,可见编译器认为函数的返回值是右值。由于有了move赋值运算符,所以没有调用copy赋值运算符,相当于将函数内部的局部对象(右值)转移到了t1变量(左值)中,完成了右到左的转化(减少了一次构造和析构);

第二个函数,返回函数内部产生的局部变量的引用:

[cpp]  view plain  copy
  1. /** 
  2.  * 测试返回局部变量的引用 
  3.  * @return 返回临时生成的对象的引用 
  4.  */  
  5. Test2& return_reference()  
  6. {  
  7.     Test2 res = "test";  
  8.     return res;  
  9. }  
  这个函数一看就是 错误的,返回局部变量的引用或指针都是不允许的,因为在函数返回前,局部变量就会被析构,导致返回的引用是 无效引用(已经游离),为了测试的完整性,用如下代码测试:
[cpp]  view plain  copy
  1. Test2 t2 = return_reference();  
  2. t2 = return_reference();  
结论:
  1. 第一行代码中,在调用函数内部执行了参数构造函数构造了局部对象,之后又执行了copy构造函数,其含义是将返回的局部对象引用,通过copy构造函数来构造变量t2对象,但结果是变量t2不一定可以构造成功,即使构造成功了其值也不正确,显然在调用copy构造函数的时候,局部对象已经析构,copy的值无效;
  2. 第二行代码中,在调用函数内部执行了参数构造函数构造了局部对象,之后又执行了copy赋值函数,结果和第一行代码类似;

第三个函数,返回函数内部产生局部变量的右值引用:

[cpp]  view plain  copy
  1. /** 
  2.  * 测试返回局部变量的右值引用 
  3.  * @return 返回临时生成的对象的右值引用 
  4.  */  
  5. Test2 return_right_reference()  
  6. {  
  7.     Test2 res = "test";  
  8.     return std::move(res);<span style="white-space:pre;">   </span>// move函数在这里的作用是将res的引用类型转换为右值引用类型  
  9. }  
  以如下代码进行测试:
[cpp]  view plain  copy
  1. Test2 t3 = return_right_reference();  
  2. t3 = return_right_reference();  
结论:
  1. 第一行代码中,除了调用参数构造函数构造局部对象外,还调用了一次move构造函数,这是由于返回值变成了局部对象的右值引用,和变量t3类型不同,所以又额外的调用了一次move构造函数对变量t3进行初始化;
  2. 第二行代码中,情况就比较复杂了。照例通过参数构造函数构造了局部对象,但返回的是其右值引用,所以又调用了一次move构造函数,通过该右值引用产生了一个临时的Test2对象(右值对象),最后通过一个move赋值运算将临时的Test2对象转移给变量t3;

第四个函数,对第三个函数进行修改:

[cpp]  view plain  copy
  1. /** 
  2.  * 测试返回局部变量的右值引用 
  3.  * @return 返回临时生成的对象的右值引用 
  4.  */  
  5. Test2&& return_right_reference2()  
  6. {  
  7.     Test2 res = "test";  
  8.     return std::move(res);  // move函数在这里的作用是将res的引用类型转换为右值引用类型  
  9. }  
结论:
  这段代码执行的结果和“第二个函数”一样,返回局部变量的引用(不管是左值还是右值)都不会有正确结果。


总结:

  最后发现,最朴素的写法反而是执行效率最高的写法(“第一个函数”),这种写法充分的利用了编译器在构造对象时进行的优化以及move赋值运算带来的优势,避免了对象在传递过程中产生的临时对象以及引发的构造和析构; 这也体现了move赋值运算存在的必要性
   无论如何,都不能在函数内部返回临时变量的指针或引用,无论该引用是左值引用还是右值引用。C++11也从来没有认为变量的控制权被转移后析构就不再发生了。所以要在函数内部产生一个对象并返回,正确的做法是:1)将对象建立在堆内存上并返回地址;2)返回局部对象,并通过copy复制运算符在函数外复制该局部对象的副本;3)返回局部对象(是一个右值),并通过move复制运算符将返回的局部对象转移到另一个对象中;
   move函数不能乱用,C++在一些场合下,隐含着右值的概念(比如函数返回值就是右值),此时将值进行类型转换都会导致额外的不必要开销(例如将返回值必须是“右值”,如果将其转为“右值引用”,编译器仍要生成代码将其转回“右值”的对象,等于做了一堆无用功)。

  上面这些结论在C++文档里说的很明白,但以前也从没有专门思考过,这次做一个测试,发现了一些没有发现的问题,特别是move赋值运算在传递返回值时的作用和move函数在返回时的无效性。所以有些东西光看文档是不够的,还得亲手试一下。
### C++ `auto` 关键字与引用的使用场景区别 #### 一、`auto` 关键字概述 `auto` 是一种类型推导机制,在 C++11 中被引入,允许编译器自动推导变量的类型。这使得开发者无需显式声明复杂的类型名称,从而简化代码并提升可读性。 例如: ```cpp auto i = 42; // 推导为 int auto d = 3.14; // 推导为 double auto s = "hello"; // 推导为 const char* ``` 在涉及复杂类型的场景中,`auto` 尤其有用,比如迭代 STL 容器中的元素时[^5]。 #### 二、引用概述 引用是一种特殊的引用类型,表示临时对象或即将销毁的对象。它的主要用途是支持 **移动语义** **完美转发**,从而避免深拷贝带来的开销。 引用通过 `&&` 表示,例如: ```cpp int &&rv = 42; // rv 是一个引用 ``` 引用的核心作用在于捕获那些不再需要保留的资源,并将其所有权转移给新的对象,这种行为称为 **移动语义**[^1]。 #### 三、`auto` 与引用的区别 | 特性 | `auto` | 引用 (`T&&`) | |---------------------|-------------------------------------|---------------------------------------| | 主要功能 | 类型推导 | 支持移动语义完美转发 | | 是否改变对象生命周期 | 不影响 | 延长临时对象的生命期 | | 使用场景 | 复杂类型推导 | 需要优化性能或传递临时对象 | ##### 示例:`auto` 的类型推导 ```cpp std::vector<int> createVector() { return std::vector<int>{1, 2, 3}; } // 使用 auto 自动推导返回类型 auto vec = createVector(); // vec 被推导为 std::vector<int> ``` ##### 示例:引用的应用 ```cpp std::vector<std::string> getStringVec() { std::vector<std::string> v{"one", "two", "three"}; return v; } void useStringVec(std::vector<std::string>&& vec) { // 参数是一个引用 // 移动 vec 到另一个 vector 对象 std::vector<std::string> movedVec = std::move(vec); } int main() { useStringVec(getStringVec()); // 返回值作为传入 } ``` #### 四、结合使用的场景 虽然 `auto` 引用的功能不同,但在某些情况下可以结合起来使用: 1. **推导引用的类型** 如果希望让编译器自动推导引用的具体类型,可以通过 `decltype(auto)` 实现。 ```cpp decltype(auto) rvalueRef = std::vector<int>{}; // rvalueRef 被推导为 std::vector<int>&& ``` 2. **配合模板编程** 在泛型编程中,`auto` 引用经常一起用于实现高效的数据传输。 ```cpp template<typename T> void moveData(T&& value) { process(std::forward<T>(value)); // 使用引用进行完美转发 } int main() { auto data = getData(); moveData(data); // 数据可能被移动而非复制 } ``` #### 五、总结 - `auto` 提供了一种便捷的方式来自动生成变量类型,适用于任何需要省略冗长类型声明的地方。 - 引用则专注于优化性能,特别是在处理大型数据结构或动态分配内存时,能够显著减少不必要的拷贝操作[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值