c++关于右值引用的那些事

Posted 冰岩作坊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++关于右值引用的那些事相关的知识,希望对你有一定的参考价值。

c++关于右值引用的那些事

一、基础知识

1.什么是左值和右值

int x=3;
int y=6;
x=y;

左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象,如上面代码,xy都是左值,3、6是右值

int x = 4;
int* p = &x;          //ok, x 是左值

int* p2 = &foobar();  // 错误,不能获取右值的地址

2.左值引用

int y=6;
int &x=y;

其实是类似于x是一个编译器帮我们写好的指针指向y,但这个指针初次赋值后就不可以指向别的内存了

int y=6;
int &x=y;
int z=7;
x=z;

这个时候x,y,z的值分别是什么,显而易见,都是7,而x引用的还是y,后续改变x的值也是x,y会变,z不会变,这就是左值引用

二、右值引用

知道了上面左值引用的概念,右值引用就很好理解了,右值引用只能绑定右值

int &&x = 3;

为什么要有右值引用

右值引用的出现是为了更高效利用临时对象的内存,他延长了临时对象的生命周期(让他在表达式结束还存活)

比如没有右值引用的时候,临时右值所在的内存会随着作用域结束而释放,看下面代码

string foo()return "abc"; }  
string x;  
x = foo();  

这里面进行了多个拷贝操作,首先“abc”要释放所以要拷贝一遍给一个临时值,在拷贝一遍给x,然后x原来的值和“abc”都要被销毁

如果我们直接利用abc本来所在的内存,这种多余的拷贝就可以去掉,但因为“abc”是一个临时变量注定要销毁,在出函数作用域的时候必定要进行一次构造一个临时的返回对象让x接受,所以一定会调用一次构造函数,且后面“=”赋值也会调用一次拷贝重载的=符号

c++11之前传入一个对象构造会调用拷贝构造函数,我们来模拟一下String类中的拷贝构造函数

class String{
  private:
  char *str;
  public:
  String(const String &d){
    str = new char[strlen(other.str)+1];
    strcpy(str, other.str);
  }
}

由于构造函数接受一个本类的对象,所以采用const引用参数的方式既能接受左值也能接受右值

我们如果要解决上面的返回值多余拷贝问题,我们需要一种新的构造函数,对对象内的内存进行“移动”而非拷贝

关键是,对于右值这种临时变量,很大程度上之后不会使用到,所以我们希望在用右值构造一个新的对象时不要拷贝而要移动以节省内存

所谓移动就是利用原来的内存,即让新对象的指针指向旧对象的内存,而非新对象新开辟一片内存然后拷贝旧对象

有同学会说这不就是浅拷贝吗,我写一个浅拷贝函数就好

class String{
  private:
  char *str;
  public:
  
  String(const String &d){
    str = d.str;
    d.str = nullptr;
  }
}

但这个编译是不通过的,因为d是一个常量引用,d.str是不能被改变的,而为了安全的移动内存必须将原本的指针置null

但如果const去掉,那么常规的引用只能匹配左值,无法匹配右值。

这就是右值引用出现的原因,我们需要一种匹配右值的方式去进行移动构造以更好的利用临时变量的内存防止多余拷贝。

有了右值引用,我们上面的浅拷贝(即移动构造函数)可以这样写

class String{
  private:
  char *str;
  public:
  //拷贝构造函数
  String(const String &d){
    str = new char[strlen(other.str)+1];
    strcpy(str, other.str);
  }
  
  //移动构造函数
  String(String &&d){
    str = d.str;
    d.str = nullptr;
  } 
}

这就是右值引用的很大部分作用,此时再运行上面的函数就不会进入拷贝构造函数而是进入移动构造函数,消除了多余拷贝

当然=操作(即赋值)也是可以重载为移动函数的,这里就不细说了跟上面原理一样

右值引用是左值还是右值

如开头基础所说,看他所在的位置,如果一个函数返回右值引用比如。

string&& func(){...}
string&& a = func();

则此时为右值

如果是下面这个,显而易见为左值

int &&x = 3;

三、std::move

如果我们有一个很大的临时左值返回,但是我们希望他跟右值一样节约内存怎么办呢

c++11引入了新的函数std::move用于将传入值变为右值引用

string foo()
  string a;
  return std::move(a);
}  
string x;  
x = foo();  

这个时候a会变为右值并且进入移动构造函数,当a很大的时候这是一个非常提升性能的操作

这里有一个关键是,move后a就不能进行任何修改操作了,因为他是一个右值,如果比如move后又通过移动构造函数构造了一个新的对象,原本的a里面的char*指针为null,这个时候对a再进行修改操作就会出错

move的实现

template<typename _Tp>
inline typename std::remove_reference<_Tp>::type&& move(_Tp&& __t)

    return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); 
}

_Tp是模版推断类型,这里简单说下函数模版参数的推断规则,简单来说就是 _Tp&&可以匹配所有的左值引用和右值引用

当传入string&时,Tp匹配为string&,然后string& &&折叠为string&

当传入string&&时,Tp匹配为string&&,然后string&& &&折叠为string&&

但这样Tp都被翻译为带引用的,而我们要用static_cast<Tp&&>()这里Tp不是函数参数不能引用折叠,所以必须保证Tp就是传入的具体类型(如string)

所以参数传递进来之后,通过std::remove_reference去除Tp匹配到的引用,让模版Tp变为具体的例如string,然后就相当于return static_cast<string&&>(__t); 返回传入值变为的右值引用

四、总结

右值引用的加入让大型的临时变量内存有了用武之地,配合移动构造来实现内存移动以实现巨大的性能优化,move也让临时的左值能用于内存移动,但move后的原本左值使用必须万分小心



以上是关于c++关于右值引用的那些事的主要内容,如果未能解决你的问题,请参考以下文章

❥关于C++之右值引用&移动语义┇移动构造&移动复制

Effective Modern C++ 条款28 理解引用折叠

C++学习日记:关于我决定开始学习C++的那些事

书中“右值”和“右值引用”之间的混淆

5分钟搞懂C++左值引用和右值引用!

为啥“通用引用”与右值引用具有相同的语法?