C++ 中的每个表达式都会生成一个值,该值属于 (左值 Lvalue,右值 Rvalue, Xvalue) 类别之一。 C++ 语言及其工具和规则的许多方面都需要正确理解这些值类别以及对它们的引用。 这些方面包括获取值的地址、复制值、移动值、将值转发给另一函数。
引用内存位置的表达式称为“左值”表达式。 左值表示存储区域的“locator”值或“left”值,并暗示它可以出现在等号 ( = ) 的左侧。 左值通常是标识符。左值是表达式结束(不一定是赋值表达式)后依然存在的对象。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
左值 (location value) 可以取地址的,有名字的,非临时的就是左值。一个可以用来存储数据的变量,有实际的内存地址(变量名)。【一个表示数据的表达式(如变量名或解引用的指针),且可以获取他的地址(取地址),可以对它进行赋值;它可以在赋值符号的左边或者右边。】
右值(real value) 不能取地址的,没有名字的,临时的就是右值。“匿名”的“临时”变量,在表达式结束时生命周期终止。是一个可以生成临时对象的表达式或者是一个不可以被修改的值(字面值 literal (字符串常量除外))。【一个表示数据的表达式(如字面常量(string literals 除外)、函数的返回值、表达式的返回值),且不可以获取他的地址(取地址);它只能在赋值符号的右边。】
常见左值
- 有名变量,函数名,数据成员名
- 运算符重载并且返回值是引用的方法,std::cout <<1
- 返回左值引用的函数
- 前置操作符, ++i,--i,(前置操作符返回的是左值(对象本身))
- 由赋值表达式或赋值运算符连接的表达式 (a=b, a += b等)
- 指针解引用表达式是左值 *ptr = 5
- 字符串常量是左值, “hello, world”
- 成员访问(点)运算符的结果
- 指针成员访问(->)运算符的结果
- 下标运算符([])的结果
常见右值:
- 临时值 (“匿名”的“临时”变量)
- 字面常量,除字符串外,nullptr, true 和 false 都是右值
- 返回值为非引用的表达式,string.substr(), i++,str1 + str2,
- 返回值的函数
- 后置操作符 i++ ,(返回的是一个临时变量,无法赋值)
- 取地址表达式 &a 是右值,因为地址本身是纯右值
- lambda 表达式也是右值
- 比较表达式 a>b (无法 &(a>b) &(a == b) 也无意义)
- 算术表达式 a+b (无法 &(a+b) &(a * b) 也无意义)
- 逻辑表达式 a && b (&&, ||, ! : 无法 &(a && b),&(!b) 也无意义)
C++ Primer 3rd 建议用前置操作符(++i,--i),只有在必要的时候才使用后置操作符(i++)。前置操作符返回的是左值(对象本身),而后置操作符返回的是右值。前置操作符需要的工作更少,只需加 1 后返回加 1 后的结果;而后置操作符则必须先保存原来的数值,以便返回未加 1 时的值,可能会花费更大的代价。
将亡值
将亡值是指 C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&& 函数的返回值、std::move 函数的返回值、转换为 T&& 类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。(更侧重于自定义类型的函数的返回值,表达式的返回值。)
引用:别的变量的别名,不占用额外内存空间。使用上要保证定义时被初始化。
左值引用只能给左值取别名。【左值引用只能引用左值】
左值:int num = 9;
左值引用:int& a = num;
右值引用只能给右值取别名。【右值应用只能引用右值】
右值:8; (x + y); function(x, y);
右值引用:int&& b = 8;
常量左值引用可以通过左值、右值、左值引用、右值引用初始化。【const 左值引用可以左值,也可以引用右值(因为右值通常是不可以改变的值,所以用 const 左值引用是可以的)】
int num = 9; // num 为左值
const int& c = num; // 使用常量左值引用引用一个常量(不能修改其值)并且该引用本身也是不可修改的
const int& e = 10; // (因为右值通常是不可以改变的值,所以用 const 左值引用是可以的)
常量右值引用只能通过右值初始化
const int&& d = 8;
//const int&& d = b; // error
左值可以通过 move(左值)来转化为右值引用,继而使用右值引用。
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra1为 a 的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
//右值引用只能右值,不能引用左值。
int&& 1 = 10;
int a = 10;
int&& r2 = a; // message : 无法将左值绑定到右值引用
//右值引用可以引用 move 以后的左值
int&& r3 = std::move(a);
return 0;
}
右值直接使用右值引用接收就行,为什么要允许 const 左值引用能引用右值?这个规定为什么会存在呢?
我们都知道,函数传参会生成临时变量,有时我们为了减少拷贝,会将函数参数设置为引用传参,那如果只写成普通的引用传参,那今后我们调用该函数,只能传递左值,无法传入右值。那在C++11 之前,没有右值引用,只能使用 const引用传参来接收实参。所以,当传入引用传参时,建议加上 const。
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是 const 左值引用既可以引用左值,也可以引用右值。
- 为什么 const 左值引用 可以引用右值呢?-- 左值引用后,可以通过该变量改变引用的右值。所以加上 const,该变量就不能被改变,所以 const 左值引用就能引用右值。因为 const 能够保证被引用的数据不会被修改,维持了权限。
右值引用总结:
- 右值引用只能引用右值,不能引用左值。
- 但是右值引用可以引用 std::move<> 后的左值(当需要右值引用引用一个左值时,可以通过 std::move<> 函数将左值转化为右值引用)。
左值引用和右值引用引出
左值引用的意义在于:
- 1.函数传参 -- 引用做参数:实参传给形参时,可以减少拷贝。
- 2.左值引用做函数返回值,只要是出了作用域还存在的对象,那么就可以减少拷贝 (ROV)。
但是左值引用却没有彻底的解决问题:当函数返回对象是一个局部对象,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。函数传返回值时,如果返回值是出了作用域销毁的(出了作用域不存在的),那还需要多次的拷贝构造,导致消耗较大,效率较低。所以这也就是为什么出现了右值引用,当然这是是右值引用价值中的一个!
那在没有右值引用之前,我们是如何解决函数传返回值的拷贝问题呢?答案是:通过输出型参数。
#include <iostream>
#include <vector>
using namespace std;
//给一个数,去构建一个杨辉三角
//如果是函数返回值去解决,那么拷贝消耗是非常大的
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
//在没有右值引用之前,我们可以通过"输出型参数"来解决这个问题
//在没有右值引用之前,我们可以通过"输出型参数"来解决这个问题
void generate_new(int numRows, vector<vector<int>> vv) {
vv.reserve(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return;
}
当然这种方法还是有局限性的,而且平时也不会经常使用,所以很有必要去了解右值引用的强大解法!!
右值引用的价值:
1 补齐左值引用的短板 ---- 通过 右值引用 实现 move 语义 ( move constructor & move assignment operator 移动构造和移动赋值运算符)(尤其是自定义类型数据做参数,或者局部变量作为返回值。)
- a 减少函数返回值时拷贝
- b 减少函数传参时拷贝 (STL很多容器接口实现了移动语义, 比如 list/vector ,push_back())
2 万能引用和完美转发
注意区分右值引用和万能引用:
如下 void fun(T && t); 中 T&& 并不是万能引用,因为 T 的类型在模板实例化时已经确定,当实例函数 void fun(T && t); 时 T 的类型已经确定。
template<typename T>
class A
{
void fun(T&& t); // 这里是右值引用
};
这里是万能引用(函数模板):
一个模板参数 T,T&& 不是类型为 T 的右值引用,语法规定 T&& 为万能引用,万能引用能接收左值和右值。
template<typename T>
class A
{
template<typename U>
void fun(U&& u); // 这里是万能引用
};
- 模板中的 && 不代表右值引用,而是万能引用,其既能接收左值又能接收右值;
- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力;
- 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值;
- 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
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 <class T> void PerfectForward(T&& t) { Fun(t); } int main() { int a = 10; PerfectForward(a); // 左值 PerfectForward(move(a)); // 右值 const int b = 10; PerfectForward(b); // const左值 PerfectForward(move(b)); // const右值 return 0; }
万能引用既可以接收左值也可以接收右值,也可以接收左值引用和右值引用,所以大大的简化了函数个数,比如上例写一个模板函数就够了。
t 既能引用左值,也能引用右值。这种现象也被称作为引用折叠。
引用折叠的原则就是一个右值引用绑定到一个右值引用的时候会变成一个右值引用,其他种类的引用都是左值引用。
引用折叠的规则:
- 一个 rvalue reference to an rvalue reference 会变成 (“折叠为”) 一个 rvalue reference.
- 所有其他种类的"引用的引用" (i.e., 组合当中含有lvalue reference) 都会折叠为 lvalue reference.
翻译出来就是下面的形式:
& + & = &
& + && = &
&& + & = &
&& + && = &&
完美转发的真正含义就是在多次转发函数参数的时候不丢失参数的左右值属性。即使 T 被折叠成左值,forward<> 也能将其属性转为右值。
【Modern C++】深入理解左值、右值 (qq.com)
在 C++11 之前,引用分为左值引用和常量左值引用两种,但是自 C++11 起,引入了右值引用,也就是说,在 C++11 中,包含如下3中引用:
-
左值引用
-
常量左值引用(不希望被修改)
-
右值引用
在 C++11 中引入了右值引用,因为右值的生命周期很短,右值引用的引入,使得可以延长右值的生命周期。在 C++ 中规定,右值引用是 && 即由2个 & 表示,而左值引用是一个 & 表示。右值引用的作用是为了绑定右值
。
在这里,我们需要特别注意的一点就是右值引用虽然是引用右值,但是其本身是左值。
int&& i = 1;
cout << "i = " << i << " at address = "<< hex << &i << endl; // i = 1 at address = 0x7ffd7adbe584
在上述代码中,a 是一个右值引用,但是其本身是左值,合适因为:
-
a 出现在等号(=)的左边
-
可以对 a 取地址
一个表达式有两个属性,分别为类型和值类别。本节说的左值引用和右值引用就属于类型
,而左值和右值则属于值类别范畴
,这个概念很重要,千万不能混淆。
有左值引用,const 左值引用;右值引用,但却没有提到 const 右值引用。
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址。(右值被右值引用以后就成为了左值)。
例如: 不能取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 取地址,也可以修改 rr1。如果不想rr1 被修改,可以用 const int&& rr1 去引用。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1++;
//rr2++; //不可以修改
cout << &rr1 << endl;
cout << &rr2 << endl;
return 0;
}
完美转发:
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward()。
返回值优化:
返回值优化 (RVO) 是一种 C++ 编译优化技术,当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++ 标准允许省略调用这些复制构造函数。
那什么时候编译器会进行返回值优化呢?
- return的值类型与函数的返回值类型相同
- return的是一个局部对象
Return Value Optimization | Shahar Mike's Web Spot
不要对函数返回的具名局部变量做 std::move 操作: 在 C++11 之前,返回一个本地对象意味着这个对象会被拷贝,除非编译器发现可以做返回值优化(named return value optimization,NRVO),能把对象直接构造到调用者的栈上。从 C++11 开始,返回值优化仍可以发生,但在没有返回值优化的情况下,编译器将试图把本地对象移动出去,而不是拷贝出去。这一行为不需要程序员手工用 std::move 进行干预——使用std::move 对于移动行为没有帮助,反而会影响返回值优化。
总结:
- 可以对表达式取地址(&)的就是左值,否则为右值。
- 被声明出来的左值引用和右值引用都是左值,因为被声明出来出来的的左值引用和右值引用都是有地址的。
- std::move() 是为了转移对象的所有权,其功能是把左值强制转换为右值,让右值引用可以指向左值,其实现等同于一个类型转换:static_cast<T&&>(lvalue),单纯的 std::move() 不会有性能提升。
- 移动语义就是为了减少拷贝,std::move 将左值转为右值引用,这样就可以重载到移动构造函数了,移动构造函数将指针赋值一下就好了,不用深拷贝了,提高性能。
- 如果不使用 std::move(),会有很大的拷贝代价(调用拷贝构造函数,或者拷贝赋值函数),使用移动语义可以避免很多无用的拷贝,提供程序性能,C++ 所有的 STL 都实现了移动语义。
- 注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型 int、float 等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。
- 万能引用:函数模板,既可以接左值引用,也可以接右值引用,但当一个右值作为参数进行传参时,相应函数接口在接收该值时,会将该值识别成左值。
- 在类型声明当中, “&&” 要不就是一个 rvalue reference (右值引用(移动构造,移动赋值中)),要不就是一个 universal reference (万能引用)– 一种可以解析为 lvalue reference 或者 rvalue reference 的引用。在模板函数中,对于某个被推导的类型
T
,universal references 总是以T&&
的形式出现。 - 使用完美转发 std::forward<>模板类来保持参数的原有类型(左值和右值)。
- std::move 与 std::forward 本质都是
static_cast
转换,对于要使用右值引用的地方使用 std::move,对于要使用万能引用的地方使用 std::forward -- 希望在函数传递参数的时候,可以保存参数原来的 lvalueness 或 rvalueness,即是说把参数转发给另一个函数。
【C++】右值引用(极详细版)_c++ 右值引用_The s.k.y.的博客-CSDN博客