什么是左值和右值?
什么是左/右值引用?
左/右值引用的作用是什么?
move的用法?
声明:这里只做理解性介绍
一般来讲,需要名字(变量名)来表示内存中的某些数据,如
int i = 100;
这里, “i"是一个整形变量, 也可以叫做对象, 对象指的就是一块存储区域.
左/右值解释:
左值,从字面意思来讲就是:“能用在赋值语句等号左侧的内容”,(它得代表一个地址),为了把左值这个概念阐述的更清楚, 又定义了"右值"的概念,他是为了对比左值,那么右值就是"不能为左值的值”.所以,右值不能直接出现在赋值语句中的等号的左侧.
但是一个左值有时又能被当做右值使用,如下:
int i = 0;
i = i + 1;
"i"出现在了赋值语句等号的左侧,所以"i"是左值, 但是又可以注意到, "i"在赋值语句等号的右侧也出现了, 但这并不表示"i"是一个右值, 应为"i"已经是左值了, 所以他不会同时是左值又是右值.
“i”, 这里就将他看成一个对象:
①当这个对象在赋值语句 “=” 的左侧时, 用的是对象在内存中的地址, 此时可以成这个对象有一种左值属性;
②当这个对象在赋值语句 “=” 右侧时, 用的就是这个对象的值, 此时可以称这个对象称这个对象有一种右值属性;
所以不难看出它的性质: 一个左值可能同时具有左值属性和右值属性;
概念:C++的表达式要么是左值,要么是右值。左值表达式的结果一般为一个对象或者一个函数
性质:当一个对象被当做右值使用的时候,用的是内容(值)。当一个对象作为左值使用的时候用的是对象的身份(对象在内存中的地址)。
使用的原则:左值既可以当做左值使用,也可以当做右值使用。右值一定不可以当做左值使用
(1) 赋值运算符"=":
赋值运算符左侧的对象就是一个左值, 其实整个赋值语句的结果仍然是左值, 只不过进行输出的时候被当做右值使用(左值具有右值属性).
int a;
printf("%d\n", a = 4);
为什么说整个赋值语句的结果仍然是左值呢? 如下例子
(a = 4) = 8; //不报错, 最终结果为8
//a = 4看做一个整体,在等号的左边,是一个标准的左值.
(2) 取地址运算符"&":
int a = 5;
&a;
“&” 必须作用于一个左值对象("&a"), 比如"&123" 肯定不成立
但是注意, 这里反回的是一个地址(指针), 这个指针是一个右值,存在实际值
(3) string, vector的下标运算符[]等都要用到左值; 迭代器的递增, 递减运算符也要用到左值:
string abc = "Hello World";
abc[0]; //"H" 如123[0]肯定不成立
//同样具备右值属性
vector<int>::iterator iter;
//...
iter++; iter--; //如123++不成立
还有其他很多运算符都会用到左值, 怎么判断一个运算符是否用到左值呢?如果这个运算符在一个直面上的值不能操作,那么这个运算符基本上就是用到左值的,比如i++成立,3++可以吗,肯定不行, 这种方法可以简单的判断出来.
补充:左值表达式和右值表达式,其含义可以理解为左值和右值.比如一个变量,也可以叫做一个表达式.
回顾一下引用:
int value = 10;
int &abc= value; // value的别名是abc, &在此不是求地址,而是标识一个引用
abc= 10; //等价于value = 10;
下面介绍三种形式的引用:
(1)左值引用(绑定到左值):
引用希望改变值的对象,上述例子中"abc"就是左值引用,左值引用带一个"&"
(2)const引用:
也是左值引用的一种, 引用那些不希望改变值的对象, 如常量等;
const int &bcd = value;
//bcd = 18; 报错,"表达式必须是可以修改的左值"
(3)右值引用(绑定到右值):
右值引用示C++11新标准中的概念.首先也是一个引用,但右值引用所侧重表达的意思往往是表示所引用对象的值在使用之后就无需保留了(如临时变量),用法是带两个"&&".(下文会详细介绍)
//int& bcd = 3; 报错,"非常量引用的初始值必须是左值"
const int& abc = 3; //可以, 常量引用
//右值引用
int && right_value = 3; //可以绑定一个常量
right_value = 5; //还可以修改值
下面详细介绍左值/右值引用:
//int& bcd = 3; 报错,"非常量引用的初始值必须是左值"
//3是很显然的右值
不难看出,左值引用就是引用左值的,换句话说就是,绑定到左值的引用;
刚刚了解到左值,左值代表一个地址,一个变量的这种感觉,所以左值引用比较好理解.
引用不像指针,指针可以指向NULL或者nullptr以表示指针指向一个空,或者说是空指针,但是引用没用空引用这个说法, 引用是一定要对应或者绑定一个对象的,所以必须要初始化引用
示例:
(这里再详细罗列一遍,上文中可能已经出现多次的例子)
int a = 1;
int &b{a}; //int &b = a; b绑定a,可以
//int &c; //报错引用必须要初始化.
//int &c = 1; //报错,"非常量引用的初始值必须是左值"(跟上文例子一样)
const int &c = 1; //可以,const引用可以绑定到右值上,const引用比较特殊
//等价于
int tmp = 1; //可以把tmp看成一个临时变量
const int& c = tem;
上述中"左值引用就是引用左值的",那么右值引用也可以这么理解,“右值引用就是引用右值的”,换句话说就是绑定到右值的引用.
右值引用就是必须绑定到右值的引用, 要通过"&&“而不是”&"来获得右值引用,一般来讲,右值引用其实主要是用来绑定到那些"即将销毁/临时的对象"上.
下面会同过范例理解这句话;
首先:能绑定到左值引用上的内容一般绑不到右值上面去,反之亦然,
int value = 10;
int &abc = valude;//abc能绑到左值abc上
//int &bcd = 5; //不可以,5是右值,左值引用不能绑定在右值
//通过这个例子可以对这句代码有更好的理解
int &&cde = 5; //可以, 右值绑右值
int a = 0;
//int&& b = a; //不可以, 不能将右值引用绑定在左值,就是说a是左值
各种范例:
string strtest{""};
string strtest{"Hello World!!"};
string &r1{ strtest }; //可以, 左值绑定左值
//string &r2{ "Hello World!!" }; //不可以,左值引用不能绑定在临时变量上.临时变量被系统当做右值
const string& r3{ "Hello World!!" }; //可以,const可以绑定到右值,还可以执行string的隐式类型转换,并将所得到的值放到string临时变量中.
//string&& r4{ strtest }; //不可以,strtest是左值
string&& r5{ "Hello World!!" }; //可以, 绑定一个临时变量(右值), 临时变量的内容是 "Hello World!!"
总结:
可以将左值引用绑定到以下这些表达式上:返回左值引用的函数,赋值,下标,解引用,前置递增递减运算符等,都是返回左值的例子;
返回非引用类型的函数,算数,关系,位,以及后置递增运算符,都生成右值, 不能将一个左值引用绑定到这类表达式上, 但是可以将一个cosnt的左值引用,或者一个右值引用绑定到这类表达式的结果上.
解引用补充:
int a = 8;
int *p = &a;
(*p) = 5; //这里'*'为解引用操作符,它返回指针p所指的地址所保存的值,这里等价于a=5
int &q = (*p); //将左值引用绑定到左值,因为(*p)返回的是左值
//(*p)就是a,a是左值,所以(*p)肯定是左值
解释一下前置递减运算符和后置增运算符,这些属于特例:
前置递增递减运算符(+ +i, - -i)
意思就是先加/减后用的意思,那么为什么前置的会是左值呢?下面用++i举例:
int i = 5;
(++i) = 10; //i被赋值为10
后置递增递减运算符(i+ +, i- -)
意思是先用后加/减的意思,这为什么是右值,i++举例:
int i = 5;
(i++) = 10; //报错 , 表达式必须是可修改的左值 ,这就说明(i++)是右值
原因:
i++先产生一个临时变量来保存i的值用于使用目的, 再给i加1, 接着返回临时变量, 之后系统会释放再给临时变量, 临时变量被释放掉了,不能在赋值,因此是右值表达式.
重点补充:
(1)如下例子,虽然&&r1可以绑定到右值,但r1本事是左值(要把他看成一个变量),因为他位于等号左边,当然,因为它是一个左值,所以左值引用能成功绑定它
int &&r1 = 1;
//int&& r2 = r1;// 报错,r1是左值
(2)任何函数里的形参都是左值,void f(int &&w), 就算是这种写法,实际上w本身也是左值.
(3)临时对象都是右值,后续补充什么是临时对象
链接:更新中…
提高程序运行效率,方法是把复制对象变成移动对象从而提高程序运行效率.
在C++11中,标准库在中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast
范例(1):
void func(int&& abc) {} //测试函数, 放在main{}之外
//int main(){ /...
int i = 10;
//int&& b = i; //报错, i是左值,不能绑到右值引用
int&& c = std::move(i); //i被转成右值
c = 15; //现在c就代表i,执行后i的值也会变成15
i = 25; //可以,i也就代表c,执行后c的值会变成25
//func(i); //报错, 同上
func(std::move(i));
int&& a1 = 100; //可以
//int&& b1 = a1; //报错
int&& b1 = std::move(a1); //可以
a1 = 50; //执行后b1也变成50
b1 = 25; //执行后a1也变成25
下面这个范例比较特殊:
string st = "Hello World!!";
string def = std::move(st); //string里的移动构造把st的内容转移到了def中去,这个转移并不是std::move干的
执行后会发现,st中的内容变空("")了,而def中的内容变为了("“Hello World!!”),表面看好像std::move将st的值移动到def中去,实际上并不是,开头说过,std::move没有移动能力,那么事实上,整个std::move(st)就是一个右值,而语句string def = std::move(st);会导致string整个类里面的移动构造函数执行,而这个移动构造函数的功能就是把st清空,并把原来st里的值移动到了def所代表的字符串中去.
在def中,实际是重新开辟了一块内存,然后把这段字符串给复制进去的(可以打印st和def的指针,可以发现并不相同),所以语句"string def = std::move(st);"和"string def = st;"比,并没有节省成本,也没提高什么效率.
注意:这里后续代码就不应该使用st对象了,因为st对象里面的有些内容已经被移走了,st已经残缺不全.
string st = "Hello World!!";
std::move(st);执行后,st没变空,其实值根本没变,这更证明前面范例中的st变空是string这个类中的移动构造函数所致.
范例(2)
string st = "Hello World!!";
string&& def = std::move(st); //这个不会触发string的移动构造,st不会变空,这行代码是将st转成右值并绑到def上
st = "abc"; //def变成abc
def = "bcd"; //st变成bcd
这个例子不会触发string的移动构造,只是一个单纯的动作,所以后续的代码无论使用st对象还是def这种右值引用都可以,两者代表同一个对象.
补充:
系统建议在调用move后,除了对move中的参数赋值或者销毁外,将不在使用它.