文章预览:

  • 一. 左值和右值
  • 二. 引用分类
  • 三. 左值引用(1个地址符&)
  • 四. 右值引用(2个地址符&)
  • 五. std::move函数



一. 左值和右值

int i;
//赋值语句
i = 20;  //左值:i(int类型的对象,代表一块内存区域),右值:20(代表一个值)

左值(左值表达式):能用在赋值语句等号左侧的东西,就称之为左值。它能够代表一个地址(也即能代表一块内存区域)。

右值(右值表达式):不能用在赋值语句等号左侧的东西,就称之为右值。它能代表一个值(任何类型),不代表内存地址。

左值和右值这种东西其实我们之前一直在使用,只不过可能没有很明确的概念而已。就比如一般的变量或者对象就是个左值,常量就是个右值。

结论:

  • C++中的任何一条表达式,要么是左值,要么就是右值,不可能两者都不是;
  • 左值有时候也可当做是右值来使用;
  • 不能作为左值的值就是右值;
  • 不能修改的左值也是右值;(比如常量,包括常数以及const修饰的变量)

tips:在判断表达式到底是左值还是右值的时候,先判断是否是左值,若不是,则一定是右值;若是,则是左值,但可能会有右值属性。
如:

int i = 20;
i = i + 1;      //i在左边,代表的是内存中的地址,这时我们称其具有左值属性,这里就可以判断其是左值了
                //i在右边,代表的是常量20,这时我们称其具有右值属性(不是右值)
int a = 1;
const int b = 2;
1 = a;          //错!能作为左值表达式得必须是可修改的左值!
b = a;          //错!能作为左值表达式得必须是可修改的左值!
int c = a;      //左值有时候也可当做是右值来使用
int i = -1;

得哪些运算符用到左值

1)赋值运算符=

int a = 1;
//整个赋值语句的结果仍然是左值
(a = 4) = 8;  //这种写法虽然有点奇怪,但是由于整个赋值语句的结果仍然是左值,代表一个内存地址,其仍然可以被赋值为8,最终a=8

2)取地址运算符&

int a = 5;   //变量a是左值
&a;          //可以,左值可以取其地址(因为左值就代表了一块内存空间)
&8;          //不可以,右值不可以取地址

3)stringvector等容器所重载的[ ]号运算符

string str = "aaa";
str[0] = 'c';

4)容器的迭代器也是左值

auto x = str.begin();
x++;
x--;
cout << *x << endl;//c
x = x + 2;
cout << *x << endl;//f

总结:通过看一个运算符在字面上能否进行操作,我们就可以判断出该运算符是否用到的是左值。能做算术运算的基本就是左值,不能做算术运算的基本就是右值。


二. 引用分类

引用&:即给一个对象/变量取别名,且引用类型的变量必须要初始化(因为引用的本质是指针常量,指针是个常量,也即该指针指向的对象的地址是个const的,如果不初始化会导致该指针常量乱指向)

三种形式的引用

左值引用(绑定到左值)

int value = 10;
int& refval = value;
refval = 13;   //等价于==> value = 13;

const常量引用

本质上常量引用也是左值引用,只不过常量引用时不可修改对象/变量的值。也即,const常量引用既可以绑定到左值上,又可以绑定到右值上,也即const的通吃的,绑谁都行。

const int& refval2 = value;//==> refval2 = 13; 常量引用可以绑定到左值上,值不可被修改
refval2 = 11;     //错误,表达式必须是可修改的左值!
refval2 = refval; //错误,表达式必须是可修改的左值!

string str{ "I Love China!" };
const string& s{ "I Love China!" }; //常量引用可以绑定到临时变量上()

右值引用(绑定到右值):是C++11的新标准中引入的概念,用两个引用符号&&

右值引用的引用对象侧重于针对临时对象/变量,起个别名。

//这是C98写的左值引用和常量引用的常见用法:
int a = 1;
int& b = a; //正确,可将左值引用绑定到左值上,b == 1
int& b = 1; //错误,非常量引用的表达式必须为左值
//也即无法将左值引用变量b绑定到右值1上
 
//这是C++11新引入的右值引用的用法:
int aa = 11;
int&& bb = aa; //错误,无法将右值引用绑定到左值
//也即无法将右值引用变量bb绑定到aa这个左值上
int&& bb = 11; //正确,可将右值引用绑定到一个常量上
bb = 13;       //正确,右值引用可被修改

三. 左值引用(1个地址符&)

左值引用:必须绑定到左值的引用类型(左值引用一般是不能绑定右值,除非你用const的左值引用,其实就算const常量引用了)。

int a = 1;
int& aa;          //错误,引用必须在定义时就初始化
int& b{ a };      //正确,将左值引用b绑定到左值a上
nt& c = 1;        //错误,左值引用不能绑定到右值,必须绑定到左值
const int& c = 1; //正确,将const常量引用c绑定到1上
		          //在编译器的内部const int& c = 1;这行代码时,系统是这样工作的:==> int tempVal = 1; const int& c = tempVal

四. 右值引用(2个地址符&)

右值引用:必须绑定到右值的引用类型(右值引用一般是不能绑定到左值上的)。

  • 目的:系统希望使用右值引用绑定一些即将销毁或一些临时对象上。
string str{ "I Love China!" };
string& r1{ str };                   //可以,左值引用绑定到左值(对象)
string& r2{ "I Love China!" };       //不可以,左值引用不能绑定到常量/临时变量上,临时变量被系统当做右值
const string& r3{ "I Love China!" }; //可以,常量引用绑定右值("I Love China!"是临时变量)

string&& r3{ "I Love China!" };      //可以,右值引用绑定右值(I Love China是临时变量)
string&& r4{ str };                  //不可以,右值引用无法绑定到左值上(对象)

当然,右值引用也有const的形式,也即:const常量右值引用

const int && rr = 1;//可以,右值引用可以绑定到右值上,且rr值不可被改变
rr = 1;             //不可以,const限定了rr值不可被改变

小结

  • 如果想绑定的是变量或是对象,那么需要左值引用(一个地址符&);
  • 如果想绑定的是常量或是临时对象(变量),那么需要右值引用(两个地址符&)或者const常量引用
  • 能绑定到左值上的引用,一般都不能绑定到右值上,反之亦是;

返回引用类型的函数,连同赋值=、取下标[]、解引用、和前置递增递减运算符--i,++i),都是返回左值(左值表达式)的例子。

返回非引用类型的函数,连同算术运算符、关系、位和后置递增递减运算符i--,i++),都是返回右值(右值表达式)的例子。
注:不能将一个左值引用绑定到这类表达式上,但是我们可以将一个const的左值引用或一个右值引用绑定到这类表达式上。

int i = 10;
int& leftRi = (--i);  //可以,--i为左值,leftRi变成i的别名
//int& leftRi = (i--);  //不可以,不能绑到右值表达式上去
int&& rightRi = (i--);//i--为右值
 
//--i: 先自减,再使用(左值)
//i--: 先使用,再自减(右值),编译器先生成要给临时的变量_tempi=i;然后用i去do事情,最好再让i = i-1;并返回_tempi,
//     所以我们会发现i--时我们输出的i是原来没减1的值!
int i = 10;
int a = --i;  //a == 9!
i = 10;
int b = i--;  //b == 10!

重点强调:

1、左值引用绑定到左值上后,这2个值是命运共同体,因为是只是取的别名嘛(左值始终专一)。

int i = 10;
int& r1 = ++i; //注意,r1这个左值绑定为i的值后,r1的值与i的值是共命运的关系,因为是起了别名嘛
r1 += 10;
cout << "r1 = " << r1 << endl; //r1 = 21;
cout << "i = " << i << endl;   //i = 21;

2、右值引用绑定到右值上后,这2个值是没有任何关系的,互相独立(右值不负责任) 。

int i = 10;
int&& r2 = i++; //注意,r1这个右值绑定为i的值后,r1的值与i的值相关之间是独立的
r2 += 10;
cout << "r2 = " << r2 << endl; //r2 = 20;
cout << "i = " << i << endl;   //i = 11;

3、右值引用虽然绑定到了右值,但其本身还是个左值

int i = 110;
int&& r1 = i++;
int& rr1 = r1;       //r1虽然是右值引用,但r1本身是个左值(变量)
int&& r2 = r1;       //错误,右值引用r1也是左值,而右值引用r2不可以绑定到左值变量r1上
cout << rr1 << endl; //110
cout << r1 << endl;  //110

4、所有的变量/对象都是左值,因为每个变量/对象名都代表了一个内存地址。

5、任何函数的形参(就是个变量)都是左值

void func(int a,int&& b);  //形参a是左值,因为形参也是变量嘛;b是右值引用类型,但也是一个左值

6、临时对象/匿名对象都是右值。

引入右值引用的目的C++11引入的右值引用,用&&符号表示

  • 拷贝对象变成移动对象提高程序运行效率
    移动对象概念:假设对象A不需要在使用,这时可以把对象Anew过的一些内存块的所有权转移给B,这时对于B来说不需要在重新new一些内存块出来了,直接使用A的原有内存块就好了,我们把这个就叫做移动对象。
  • 右值引用&&是用在移动构造函数以及移动赋值运算符函数中的。

五. std::move函数

std::move() C++11从名字上看是一个移动的函数,但实际上,这个函数根本就没有做移动的操作std::move()作用只有一个:将一个左值强制转换为一个右值。且书上建议我们,当该左值使用std::move()函数强制类型转换为右值后就不能够再使用该左值去做事情了。

int i = 10;
//int&& ri1 = i;                  //正常情况下,无法将右值引用变量ri1绑定到左值i上
int&& ri1 = i++;
cout << "ri1 = " << ri1 << endl;  //10
cout << "i = " << i << endl;      //11
i += 20;//i = 31
cout << "ri1 = " << ri1 << endl;  //10
cout << "i = " << i << endl;      //31

i = 10;
int&& ri2 = std::move(i);         //用std::move()函数将左值强制转换为右值
cout << "ri2 = " << ri1 << endl;  //10
cout << "i = " << i << endl;      //10
i += 20;
cout << "ri2 = " << ri2 << endl;  //30
cout << "i = " << i << endl;      //30

注意:通过上面的代码例子,可以发现,常规右值引用绑定了右值后,左右两边的值是相互独立的。但一旦右值引用绑定了通过 std::move()函数强制转换为的右值时,左右两边的值又同步相关了。总结:std::move()将不负责任的男人又变成专一的男人了~

注意:如果你用容器类的对象使用std::move()的话,我们会发现原容器对象的内容为空了,新容器对象的内容变成了原容器对象的内容。这里我们会有一种假象是std::move()完成的转移,而实际是容器的移动构造函数完成的,并且不是转移,而是新开辟的了个内存用于保存新容器中的内容,并没有节省内存。

就会造成不但没有利用该容器类的移动构造函数将一个对象的某段内存的权限直接给到另外一个对象,还会额外开辟了内存。 再将该内存中的内容拷贝到这个新的内存中

[C++ 从入门到精通] 18.左值、右值,左值引用、右值引用、move_开发语言

string str = "I Love China!";
string def = std::move(str);    //会触发string类容器的移动构造函数,str内容为空,def内容变成"I Love China!"
string &° = std::move(str);  //右值引用,这里不会触发string类容器的移动构造函数,deg和str的值又同步相关了

下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。