[1] 左值与右值
在 C 语言中, 常常会提起左值 (lvalue), 右值(rvalue) 这样的称呼.
而在编译程序时, 编译器有时也会在报出的错误信息中包含左值, 右值的说法.
不过左值, 右值通常不是通过一个严谨的定义而为人所知的.
事实上, 之所以只知道一些关于左值, 右值的判断而很少听到其真正的定义的一个原因就是很难归纳. 而且即使归纳了, 仍需要大量的解释.
(1)左右值的判别方法一
大多数时候, 左右值的一个最为典型的判别方法:
在赋值表达式中, 出现在等号左边的就是 "左值", 而在等号右边的, 则称为 "右值". 比如:
a = b + c;
在这个赋值表达式中, a 就是一个左值, 而 b+c 则是一个右值.
这种识别左值, 右值的方法在 C++ 中依然有效.
(2)左右值判别方法二
不过 C++ 中还有一个被广泛认同的说法, 那就是:
可以取地址的, 有名字的就是左值, 反之, 不能取地址的, 没有名字的就是右值.
那么这个加法赋值表达式中,&a 是允许的操作, 但 &(b + c)这样的操作则不会通过编译.
因此 a 是一个左值,(b+c)是一个右值. 这些判别方法通常都非常有效.
(3)左右值判别方法三
更为细致地, 在 C++11 中, 右值是由两个概念构成的, 一个是纯右值(prvalue,Pure Rvalue), 另一个则是将亡值(xvalue,eXpiring Value).
其中纯右值就是 C++98 标准中右值的概念, 指的是用于辨识临时变量和一些不跟对象关联的值.
比如: 返回类型为非引用的函数返回的临时变量值就是一个纯右值.
一些运算表达式, 比如 1+3 产生的临时变量值, 也是纯右值.
而不跟对象关联的字面量值, 比如: 2,'c',true, 也是纯右值.
此外, 类型转换函数的返回值, lambda 表达式等, 也都是右值.
而将亡值则是 C++11 新增的跟右值引用相关的表达式, 这样表达式通常是将要被移动的对象(移为他用), 比如:
[1] 返回右值引用 T&& 的函数返回值
[2] std::move 的返回值
[3] 转换为 T&& 的类型转换函数的返回值.
而剩余的, 可以标识函数, 对象的值都属于左值.
所以, 在 C++11 的程序中, 所有的值必属于左值, 将亡值, 纯右值三者之一.
[2] 左值引用与右值引用
在 C++11 中, 右值引用 (rvalue reference) 就是对一个右值进行引用的类型.
为了区别于之前 C++98 中的引用类型, 我们称 C++98 中的引用为 "左值引用"(lvalue reference).
右值引用和左值引用都是属于引用类型, 无论是声明一个左值引用还是右值引用, 都必须立即进行初始化.
其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存, 只是该对象的一个别名.
左值引用是具名变量值的别名, 而右值引用则是不具名 (匿名) 变量的别名.
事实上, 由于右值通常不具有名字, 我们也只能通过引用的方式找到它的存在.
通常情况下, 我们只能是从右值表达式获得其引用. 比如:
T && a = ReturnRvalue();
这个表达式中, 假设 ReturnRvalue 返回一个右值, 我们就声明了一个名为 a 的右值引用, 其值等于 ReturnRvalue 函数返回的临时变量的值.
ReturnRvalue 函数返回的右值在表达式语句结束后, 其生命也就终结了(通常我们也称其具有表达式生命期).
而通过右值引用的声明, 该右值又 "重获新生", 其生命期将与右值引用类型变量 a 的生命期一样.
即就是, 只要 a 还 "活着", 该右值临时量将会一直 "存活" 下去. 所以, 相比于以下语句的声明方式:
T b = ReturnRvalue();
刚才的右值引用变量声明, 就会少一次对象的析构及一次对象的构造.
因为 a 是右值引用, 直接绑定了 ReturnRvalue()返回的临时量, 而 b 只是由临时值构造而成的, 而临时量在表达式结束后会被析构掉, 因应就会多一次析构和构造的开销.
[3] 常量左值引用 与 常量右值引用
(1)常量左值引用
通常情况下, 右值引用是不能够绑定到任何的左值的. 比如下面的表达式就是无法通过编译的:
- int c;
- int && d = c;
那么, 在 C++98 标准中就已经出现的左值引用是否可以绑定到右值 (由右值进行初始化) 呢? 比如:
- T & e = ReturnRvalue();
- const T & f = ReturnRvalue();
这样的语句是否能够通过编译呢? 答案是: e 的初始化会导致编译时错误, 而 f 则不会. 出现这样的状况的原因:
常量左值引用在 C++98 标准中开始就是个 "万能" 的引用类型. 它可以接受非常量左值, 常量左值, 右值对其进行初始化.
而且, 在使用右值对其初始化的时候, 常量左值引用还可以像右值引用一样将右值的生命期延长.
不过相比于右值引用所引用的右值, 常量左值所引用的右值在它的 "余生" 中只能是只读的.
相对地, 非常量左值只能接受非常量左值对其进行初始化.
(2)常量右值引用
为了语义的完整, C++11 中还存在着常量右值引用. 比如通过以下代码声明一个常量右值引用:
const T && crvalueref = ReturnRvalue();
但是, 一来右值引用主要就是为了移动语义, 而移动语义需要右值是可以被修改的, 那么常量右值引用在移动语义中就没有用武之处;
二来如果要引用右值且让右值不可以更改, 常量左值引用往往就足够了.
因此, 在现在的尴尬情况下, 还没有看到常量右值引用有何用处.
(3)引用类型可引用值的类型表
下面列出了在 C++11 中各种引用类型可以引用的值的类型:
值得注意的是, 只要能够绑定右值的引用类型, 都能够延长右值的生命期.
[4] 如何判断?
有的时候, 我们可能不知道一个类型是否是引用类型, 以及是左值引用还是右值引用(这在模板中比较常见).
标准库在 < type_traits > 头文件中提供了 3 个模板类: is_rvalue_reference,is_lvalue_reference,is_reference, 可供我们进行判断. 比如:
- #include <iostream>
- #include <type_traits>
- using namespace std;
- int main()
- {
- cout <<is_reference<int>::value <<endl; // 0
- cout << is_rvalue_reference<string &&>::value <<endl; // 1
- cout << is_lvalue_reference<string &> ::value <<endl; // 1
- cout << is_rvalue_reference<const string &&>::value <<endl; // 1
- cout << is_lvalue_reference<const string &> ::value << endl; // 1
- }
我们通过模板类的成员 value 就可以打印出 stirng && 是否是一个右值引用了.
配合 C++11 中的类型推导操作符 decltype, 我们甚至还可以对变量的类型进行判断.
good good study, day day up.
顺序 选择 循环 总结
来源: http://www.bubuko.com/infodetail-3395383.html