文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

左值引用、右值引用、移动语义、完美转发,你知道的不知道的都在这里

2024-12-11 16:34

关注

众所周知C++11新增了右值引用,谈右值引用我们也可以扩展一些相关概念:

程序喵下面会一一介绍:

左值、右值

概念1:

左值:可以放到等号左边的东西叫左值。

右值:不可以放到等号左边的东西就叫右值。

概念2:

左值:可以取地址并且有名字的东西就是左值。

右值:不能取地址的没有名字的东西就是右值。

举例: 

  1. int a = b + c; 

a是左值,有变量名,可以取地址,也可以放到等号左边, 表达式b+c的返回值是右值,没有名字且不能取地址,&(b+c)不能通过编译,而且也不能放到等号左边。 

  1. int a = 4; // a是左值,4作为普通字面量是右值 

左值一般有:

纯右值、将亡值

纯右值和将亡值都属于右值。

纯右值

运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。

举例:

将亡值

将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。

举例: 

  1. class A {  
  2.     xxx;  
  3. };  
  4. A a;  
  5. auto c = std::move(a); // c是将亡值  
  6. auto d = static_cast<A&&>(a); // d是将亡值 

左值引用、右值引用

根据名字大概就可以猜到意思,左值引用就是对左值进行引用的类型,右值引用就是对右值进行引用的类型,他们都是引用,都是对象的一个别名,并不拥有所绑定对象的堆存,所以都必须立即初始化。 

  1. type &name = exp; // 左值引用  
  2. type &&name = exp; // 右值引用 

左值引用

看代码: 

  1. int a = 5 
  2. int &b = a; // b是左值引用  
  3. b = 4 
  4. int &c = 10; // error,10无法取地址,无法进行引用  
  5. const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址 

可以得出结论:对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。

右值引用

如果使用右值引用,那表达式等号右边的值需要时右值,可以使用std::move函数强制把左值转换为右值。 

  1. int a = 4 
  2. int &&b = a; // error, a是左值  
  3. int &&c = std::move(a); // ok 

移动语义

谈移动语义前,我们首先需要了解深拷贝与浅拷贝的概念

深拷贝、浅拷贝

直接拿代码举例: 

  1. class A {  
  2. public:  
  3.     A(int size) : size_(size) {  
  4.         data_ = new int[size];  
  5.     }  
  6.     A(){}  
  7.     A(const A& a) {  
  8.         size_ = a.size_;  
  9.         data_ = a.data_;  
  10.         cout << "copy " << endl 
  11.     }  
  12.     ~A() {  
  13.         delete[] data_;  
  14.     }  
  15.     int *data_;  
  16.     int size_;  
  17. };  
  18. int main() {  
  19.     A a(10);  
  20.     A b = a 
  21.     cout << "b " << b.data_ << endl 
  22.     cout << "a " << a.data_ << endl 
  23.     return 0;  

上面代码中,两个输出的是相同的地址,a和b的data_指针指向了同一块内存,这就是浅拷贝,只是数据的简单赋值,那再析构时data_内存会被释放两次,导致程序出问题,这里正常会出现double free导致程序崩溃的,但是不知道为什么我自己测试程序却没有崩溃,能力有限,没搞明白,无论怎样,这样的程序肯定是有隐患的,如何消除这种隐患呢,可以使用如下深拷贝: 

  1. class A {  
  2. public:  
  3.     A(int size) : size_(size) {  
  4.         data_ = new int[size];  
  5.     }  
  6.     A(){}  
  7.     A(const A& a) {  
  8.         size_ = a.size_;  
  9.         data_ = new int[size_]; 
  10.         cout << "copy " << endl 
  11.     }  
  12.     ~A() {  
  13.         delete[] data_;  
  14.     }  
  15.     int *data_;  
  16.     int size_;  
  17. };  
  18. int main() {  
  19.     A a(10);  
  20.     A b = a 
  21.     cout << "b " << b.data_ << endl 
  22.     cout << "a " << a.data_ << endl 
  23.     return 0;  

深拷贝就是再拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源,而不是简单的赋值。

聊完了深拷贝浅拷贝,可以聊聊移动语义啦:

移动语义,在程序喵看来可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢,是通过移动构造函数。 

  1. class A {  
  2. public:  
  3.     A(int size) : size_(size) {  
  4.         data_ = new int[size];  
  5.     }  
  6.     A(){}  
  7.     A(const A& a) {  
  8.         size_ = a.size_;  
  9.         data_ = new int[size_];  
  10.         cout << "copy " << endl 
  11.     }  
  12.     A(A&& a) {  
  13.         this->data_ = a.data_; 
  14.          a.data_ = nullptr 
  15.         cout << "move " << endl 
  16.     }  
  17.     ~A() {  
  18.         if (data_ != nullptr) {  
  19.          delete[] data_;  
  20.         }  
  21.     }  
  22.     int *data_;  
  23.     int size_; 
  24.  };  
  25. int main() {  
  26.     A a(10);  
  27.     A b = a 
  28.     A c = std::move(a); // 调用移动构造函数  
  29.     return 0;  

如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的STL都实现了移动语义,方便我们使用。例如: 

  1. std::vector<string> vecs;  
  2. ...  
  3. std::vector<string> vecm = std::move(vecs); // 免去很多拷贝 

注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。

完美转发

完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward()。 

  1. void PrintV(int &t) {  
  2.     cout << "lvalue" << endl 
  3.  
  4. void PrintV(int &&t) {  
  5.     cout << "rvalue" << endl 
  6.  
  7. template<typename T>  
  8. void Test(T &&t) {  
  9.     PrintV(t);  
  10.     PrintV(std::forward<T>(t));   
  11.     PrintV(std::move(t));  
  12.  
  13. int main() {  
  14.     Test(1); // lvalue rvalue rvalue  
  15.     int a = 1 
  16.     Test(a); // lvalue lvalue rvalue  
  17.     Test(std::forward<int>(a)); // lvalue rvalue rvalue  
  18.     Test(std::forward<int&>(a)); // lvalue lvalue rvalue  
  19.     Test(std::forward<int&&>(a)); // lvalue rvalue rvalue  
  20.     return 0;  

分析

返回值优化

返回值优化(RVO)是一种C++编译优化技术,当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。

那什么时候编译器会进行返回值优化呢?

看几个例子:

示例1: 

  1. std::vector<int> return_vector(void) {  
  2.     std::vector<int> tmp {1,2,3,4,5};  
  3.     return tmp;  
  4.  
  5. std::vector<int> &&rval_ref = return_vector(); 

不会触发RVO,拷贝构造了一个临时的对象,临时对象的生命周期和rval_ref绑定,等价于下面这段代码: 

  1. const std::vector<int>rval_ref = return_vector(); 

示例2: 

  1. std::vector<int>&& return_vector(void) {  
  2.     std::vector<int> tmp {1,2,3,4,5};  
  3.     return std::move(tmp); 
  4.  
  5. std::vector<int> &&rval_ref = return_vector(); 

这段代码会造成运行时错误,因为rval_ref引用了被析构的tmp。讲道理来说这段代码是错的,但我自己运行过程中却成功了,我没有那么幸运,这里不纠结,继续向下看什么时候会触发RVO。

示例3: 

  1. std::vector<int> return_vector(void) {  
  2.     std::vector<int> tmp {1,2,3,4,5};  
  3.     return std::move(tmp);  
  4.  
  5. std::vector<int> &&rval_ref = return_vector(); 

和示例1类似,std::move一个临时对象是没有必要的,也会忽略掉返回值优化。

最好的代码: 

  1. std::vector<int> return_vector(void) {  
  2.     std::vector<int> tmp {1,2,3,4,5};  
  3.     return tmp;  
  4.  
  5. std::vector<int> rval_ref = return_vector(); 

这段代码会触发RVO,不拷贝也不移动,不生成临时对象。 

 

来源:C语言与C++编程内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯