【C++11】右值引用
创始人
2024-05-13 10:31:43
0

右值引用是C++11中才被提出来的新概念,而以前的版本中也有引用,但是是指的左值引用。归根结底,左右值引用都是给对象取别名。


 1.区分左值和右值

 提起左值和右值很多小伙伴可能第一时间会有点小蒙圈,敲了好长时间代码了,对于这个概念可能有点蒙圈,其实模糊的说可以以=号为分界线,左边的叫左值,右边的叫右值。

1.1左右值特点 :

左值:是一个表示数据的表达式,如变量名或解引用的指针等

  1. 可以放在等号的左右边
  2. 左值可以修改
  3. 左值可以取地址
int main()
{int a = 10;int b = a;b = 10;const int c = 5;int* p = new int(0);//以上的a,b,c,*p都是左值cout << b << endl;cout << *p << endl;return 0;}

右值:也是一个表示数据的表达式,如字母常量、表达式的返回值、函数的返回值(不能是左值引用返回)等。

  1. 右值不可以取地址
  2. 右值不可以直接修改
  3. 右值只能放在等号右边
  4. 右值往往是没有名称的
int main()
{int x,y=10;
//以下是常见的三种右值x+y       // 表达式返回值func(x,y)  //函数返回值5   //常量
}
  • 之所以右值无法被取地址是因为右值的本身是一个常量值或者是临时变量,这些常量值和临时变量并没有被储存起来,所以就没有他们的地址。

右值又被细分为纯右值和将亡值:

  • 纯右值: 就是指等号右边的常数,上式中的5
  • 将亡值:其实就是中间变量的过渡,过渡之后就消亡,可以细分两种:
  1. 函数的临时返回值:例如 int a = func(3); func(3)的返回值是右值,副本拷贝给a,然后消失。
  2. 表达式 像(x+y),其中(x+y)是右值。

 1.2左值引用和右值引用

左值引用

左值引用就是对左值的引用,给左值取别名,通过“&”来声明。

int main()
{int a = 10;int b = a;b = 10;const int c = 5;int* p = new int(0);//以上的a,b,c,*p都是左值int& ra=a;const int& rc = c;int*& rp=p;int& cpp=*p;
}

以上是几种常见的左值引用。

右值引用: 

右值引用就是对右值的引用,给右值取别名,通过“&&”来声明。

int main()
{double x = 4.1, y = 4.2;//以下几个都是常见的右值x + y;func(x, y);5//以下几个都是对右值的右值引用int&& rr1 = 5;double&& rr2 = x + y;double rr3 = func(x, y);return 0;
}
  • 右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,如果不想让被引用的右值被修改,可以用const修饰右值引用。
int main()
{double x = 4.1, y = 4.2;int&& rr1 = 5;const double&& rr2 = x + y;//修改右值rr1 = 10;rr2 = 20;return 0;
}

 左值引用能引用右值吗?或者右值引用能引用左值吗?

绝大多数情况下是不能的,因为左值是可以修改的而右值是不能修改的,所以涉及到权限放大问题,一般来说两者都不成立。但是要想左值引用来引用右值可以用const。

所以const左值引用既可以引用左值也可应引用右值。

同理,右值引用在绝大多数情况下不能引用左值,但是能引用move()中的左值。move()是C++11新增的函数,在后面我们会介绍。

 左右值引用总结:

  • 左值引用只能引用左值不能引用右值,但是能引用const修饰的右值。
  • 右值引用只能引用右值不能引用左值,但是能引用move后的左值。 

2.右值引用的提出

2.1引用的价值 

说起引用的价值不得不提起:提高效率,减少拷贝。 

 2.2左值引用能解决哪些问题?

  • 做参数:a、减少拷贝,提高效率,b、做输出型参数(这个左值引用几乎可以解决所有的问题)  。
  • 做返回值: a、减少拷贝,提高效率,b、引用返回,可以修改返回对象。(这个左值引用能解决大部分问题,但是有一些还无法解决,所以就提出了右值引用。
  • 如果函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板。

2.3 减少拷贝构造

我们以to_string函数为例,这个函数的返回对象是一个局部变量。此时就不能再使用引用返回了,只能用传值返回一次一次的传了。但是现在的编译器都会进行优化处理,所以一般进行一次拷贝构造而实际进行的是两次拷贝构造。

namespace cl
{string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}string str;while (value > 0){int x = value % 10;value /= 10;str += (x + '0');}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}int main()
{int x = 10;string ret = tmp::to_string(-3456);
}

之所以不能用左值引用做返回值,就是因为to_string函数最后会被析构,这里在用传值引用就会出大问题。 

 有的编译器进行优化,就不再产生临时变量而是直接进行一次拷贝构造。

 但是并不是所有的代码都可以优化或者是有的情况必须要进行两次拷贝构造时就要用到右值引用了。

为了解决上述的问题c++11就提出了新的内容。


2.4右值引用和移动语句 

左值引用是直接加&来使用的,但是右值引用不是直接加&&使用的,而是有它自己的规则。

移动构造

为了更好的解决这个问题,又给移动语句新定义了两元大将:移动拷贝移动赋值

  • 我们以模拟实现的string来说明。
  • #define _CRT_SECURE_NO_WARNINGS 1
    #include
    #include
    #include
    using namespace std;namespace cl
    {class string{public:typedef char* iterator;iterator begin(){return _str; //返回字符串中第一个字符的地址}iterator end(){return _str + _size; //返回字符串中最后一个字符的后一个字符的地址}//构造函数string(const char* str = ""){_size = strlen(str); //初始时,字符串大小设置为字符串长度_capacity = _size; //初始时,字符串容量设置为字符串长度_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')strcpy(_str, str); //将C字符串拷贝到已开好的空间}//交换两个对象的数据void swap(string& s){//调用库里的swap::swap(_str, s._str); //交换两个对象的C字符串::swap(_size, s._size); //交换两个对象的大小::swap(_capacity, s._capacity); //交换两个对象的容量}//拷贝构造函数(现代写法)string(const string& s):_str(nullptr), _size(0), _capacity(0){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); //交换这两个对象}//赋值运算符重载(现代写法)string& operator=(const string& s){cout << "string& operator=(const string& s) -- 深拷贝" << endl;string tmp(s); //用s拷贝构造出对象tmpswap(tmp); //交换这两个对象return *this; //返回左值(支持连续赋值)}//析构函数~string(){delete[] _str;  //释放_str指向的空间_str = nullptr; //及时置空,防止非法访问_size = 0;      //大小置0_capacity = 0;  //容量置0}//[]运算符重载char& operator[](size_t i){assert(i < _size); //检测下标的合法性return _str[i]; //返回对应字符}//改变容量,大小不变void reserve(size_t n){if (n > _capacity) //当n大于对象当前容量时才需执行操作{char* tmp = new char[n + 1]; //多开一个空间用于存放'\0'strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')delete[] _str; //释放对象原本的空间_str = tmp; //将新开辟的空间交给_str_capacity = n; //容量跟着改变}}//尾插字符void push_back(char ch){if (_size == _capacity) //判断是否需要增容{reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍}_str[_size] = ch; //将字符尾插到字符串_str[_size + 1] = '\0'; //字符串后面放上'\0'_size++; //字符串的大小加一}//+=运算符重载string& operator+=(char ch){push_back(ch); //尾插字符串return *this; //返回左值(支持连续+=)}//返回C类型的字符串const char* c_str()const{return _str;}private:char* _str;size_t _size;size_t _capacity;};}

移动构造:是一个右值引用而构造函数是const修饰的左值引用,而移动构造的本质就是将参数右值的资源进行剽窃从而占为己有而避免深拷贝,而这个参数右值(将亡值)也将在不久后消失,所以也可以算是物尽其用了。

  • 这里我们在说一下右值到类型:
  1. 内置类型右值:纯右值
  2. 自定义类型右值:将亡值-----即将被消耗掉的值
namespace cl
{class string{public://移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动构造--资源转移" << endl;swap(s);}private:char* _str;size_t _size;size_t _capacity;};
}

就单单看移动构造和拷贝构造的书写格式,拷贝构造把const去掉,把&换成&&j就ok了。

 对于左值来说,会调用拷贝构造,对于遇见的右值去调用移动构造,但是此时我们用move还会发生一些场景。

s1的所以东西全部都转移个s3了包括它本身的地址,这个就叫资源转移,这就叫专业。因为用到move函数时把s1当将亡值了,你都快没了,我继承你的家产,娶你的老婆没啥毛病吧,哈哈。 

移动构造和拷贝构造的区别:

  • 在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数。
  • 增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则)。
  • string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小。

给string类增加移动构造后,对于返回局部string对象的这类函数,在返回string对象时就会调用移动构造进行资源的移动,而不会再调用拷贝构造函数进行深拷贝了。

在上面已经说过了,编译器为了提高效率,会进行一系列的优化,例如将调用的两次深拷贝减少到一次。但是有点编译器缺不会优化。在C++11提出移动构造以后,移动构造也会从2次被优化为1次。

但如果我们不是用函数的返回值来构造一个对象,而是用一个之前已经定义出来的对象来接收函数的返回值,这时编译器就无法进行优化了。

这时当函数返回局部对象时,会先用这个局部对象拷贝构造出一个临时对象,然后再调用赋值运算符重载函数将这个临时对象赋值给接收函数返回值的对象。

  • 编译器并没有对这种情况进行优化,因此在C++11标准出来之前,对于深拷贝的类来说这里就会存在两次深拷贝,因为深拷贝的类的赋值运算符重载函数也需要以深拷贝的方式实现。
  • 但在深拷贝的类中引入C++11的移动构造后,这里仍然需要再调用一次赋值运算符重载函数进行深拷贝,因此深拷贝的类不仅需要实现移动构造,还需要实现移动赋值

移动赋值 

 移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值。

	//移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}//移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}

移动赋值和原有operator=函数的区别:

  • 在没有增加移动赋值之前,由于原有operator=函数采用的是const左值引用接收参数,因此无论赋值时传入的是左值还是右值,都会调用原有的operator=函数。
  • 增加移动赋值之后,由于移动赋值采用的是右值引用接收参数,因此如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)。
  • string原有的operator=函数做的是深拷贝,而移动赋值函数中只需要调用swap函数进行资源的转移,因此调用移动赋值的代价比调用原有operator=的代价小。

 在实现移动赋值函数之前,该代码的运行结果理论上应该是调用一次拷贝构造,再调用一次原有的operator=函数,但由于原有operator=函数实现时复用了拷贝构造函数,因此代码运行后的输出结果会多打印一次拷贝构造函数的调用,这是原有operator=函数内部调用的。

3.完美转发 

3.1万能引用&&

之所以叫万能引用就是&&代表的不再是右值引用而是既能用左值引用也能用右值引用的“万能充”

template
void PerfectForward(T&& t)//万能引用
{//……
}

右值引用和万能引用的区别就是,右值引用需要是确定的类型,而万能引用是根据传入实参的类型进行推导,如果传入的实参是一个左值,那么这里的形参t就是左值引用,如果传入的实参是一个右值,那么这里的形参t就是右值引用。

万能引用也叫做引用折叠,如果说传进去的是个左值,那&&折叠成一个&,如果说传进去的是一个右值,那还是&&。

但是如果说原本的右值下一步变成左值后再被传参到万能引用中那他被当做左值传进去还是右值传进去还是右值传进去呢?

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template
void PerfectForward(T&& t)
{Fun(t);
}
int main()
{PerfectForward(10);//右值int a;PerfectForward(a);//左值PerfectForward(std::move(a));//右值const int b = 8;PerfectForward(b);//const左值PerfectForward(std::move(b));//const右值return 0;
}

PerfectForward(0函数的参数类型是一个万能引用,而我们在PerfectForward函数中调用func函数就是希望调用PerfectForward函数时传入左值、右值、const左值、const右值,能够匹配到对应版本的Func函数。

为毛全部都是左值引用啊? 

实际调用PerfectForward函数时传入左值和右值,最终都匹配到了左值引用版本的Func函数,调用PerfectForward函数时传入const左值和const右值,最终都匹配到了const左值引用版本的Func函数。
根本原因就是:右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。
也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发。

3.2完美转发

完美转发的保持属性 

要想在参数传递过程中保持其原有的属性,需要在传参时调用forward函数。

就比如上面的要想让右值不退化成左值,就可以用到forward函数。

template
void PerfectForward(T&& t)
{Func(std::forward(t));
}

经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数,这就是完美转发的价值。

 

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...