序言
在前两篇文章中,我们介绍了 C++11 新特性里面的新增关键字以及语法糖 以及 lambda 表达式,大家如果感兴趣,可以点击查看。在这篇文章中,我们介绍另一个非常重要的新特性 --- 右值引用及移动语义 ,这允许更灵活的资源管理。
在文章开始时,我想请问大家一个问题,请问 const int A = 1
,在这里 A
是左值还是右值?请带着你的疑问或者是答案,在文章中寻求解答吧。
1. 左值以及左值引用
1.1 什么是左值
左值是编程语言中的一个重要概念,它指的是一个 表达式 ,该表达式表示一个具有 持久状态 的对象的身份(即内存中的位置)。左值的主要特点是它们可以出现在赋值语句的左侧,作为被赋值的对象。左值具有以下特点:
- 可寻址性 :左值可以被取地址。这意味着你可以使用
&
运算符来获取左值所表示对象的内存地址。 - 持久性:左值表示的对象在内存中有一个持久的位置,直到程序结束或对象被显式销毁。
- 可修改性(除非被声明为const):左值可以被修改。它们可以出现在赋值语句的左侧,作为接收赋值的目标。
在 C++ 中,大多数变量(包括基本数据类型的变量和对象的实例)都是左值。
1.2 什么是左值引用
左值引用是 C++ 中的一种引用类型,它用于引用一个左值。左值引用通过在变量类型后加上 &
符号来声明,它创建了对原始左值对象的别名。如:
cpp
int A = 1;
int& Ref = A;
左值引用具有以下特点:
- 绑定性:左值引用在声明时必须与一个左值绑定。一旦绑定,它就不能再被重新绑定到另一个对象上。
- 初始化 :左值引用在声明时必须初始化,并且不能是
nullptr
。 - 别名性 :左值引用是其所绑定对象的别名。对引用的任何非
const
修改都会反映到原始对象上。
左值引用在 C++ 中非常有用,因为它们允许函数以引用的方式接收参数,从而 避免了不必要的对象拷贝,提高了程序的效率。就比如:
cpp
void Func(vector<int>& v) {
// ...
}
int main() {
vector<int> v(100000);
Func(v);
return 0;
}
函数 Func
需要接受一个 vector<int>
作为参数,如果在以前,就需要拷贝,因为 形参是实参的拷贝 嘛,但是有了引用之后,我们直接传引用过去,这时候,在函数中的形参也是主函数的实参,大大减少了拷贝带来的花销。
1.3 左值引用的短板
在上面我们说到,使用左值引用作为参数的传递值以及返回的对象大大地减少了拷贝带来的时间花销,但是左值引用也存在一个短板,以实际例子引出问题:
cpp
string& to_String(vector<int>& v) {
string s = "ABC";
// ...
return s;
}
int main() {
vector<int> v;
string s = to_String(v);
cout << s << endl;
return 0;
}
我们的函数返回一个 string局部变量
的引用,这显然会出问题的,因为当执行函数结束时,s
的生命周期也就结束了,会自动调用析构函数,这是我们在主函数中接受的 s
打印出的结构就为空。所以说,我们 不能返回局部变量的引用,只能值返回。
2. 右值以及右值引用
2.1 什么是右值
右值通常表示一个临时的、不具有标识符的表达式,它们往往是一个值而不是一个具体的对象。在赋值操作中,右值位于赋值运算符的右侧。右值具有以下特点:
- 临时性:右值通常是临时的,它们不持久存在于内存中,一旦表达式执行完毕,右值就会被销毁。
- 不可寻址性:右值通常没有明确的内存位置,因此不能通过取地址运算符获取其地址。
右值又可分为 将亡值和纯右值:
纯右值
定义:
纯右值是指那些 非引用返回的临时对象、运算表达式产生的临时结果、原始字面量以及lambda表达式等 。纯右值在表达式求值后通常会被销毁,且不能通过取地址运算符 &
获取其地址。
示例:
- 字面值(如
42、"hello"
) - 运算表达式的结果(如
a + b
,其中a
和b
是变量) - 函数返回的临时对象(如
string func() { return string("temp"); }
中的返回值) - lambda表达式(如
[](int x) { return x * 2; }
)
将亡值
定义:
将亡值是 C++11 中引入的一个新概念,在这之前所有的值都是纯右值,它指的是那些 与右值引用相关的表达式 。将亡值通常表示一个即将被销毁的对象,但其资源可以被移动(而非拷贝)到另一个对象中,从而实现资源的有效利用。
示例:
move
的返回值(move
是一个模板函数,它将其参数转换为对应的将亡值)
2.2 什么是右值引用
右值引用是 C++11 中引入的一个新特性,它允许程序员显式地表示一个变量可以绑定到右值上。右值引用的语法是 T&&
,其中T是类型,例如:int&& A = 1;
右值引用的功能我们将在下文详细向大家介绍,他的特性和左值引用是类似的。
3. 移动语义
3.1 函数值返回时发生了什么
在讲解移动语义之前,我们先了解当函数值返回时,发生了什么:
- 首先,将返回值
temp
拷贝构造生成一个临时对象 - 之后,将临时对象拷贝构造生成
arr
注意:
这是理论情况,在实际情况中,编译器发现这里有连续的拷贝构造,可能直接优化为一次拷贝构造,temp
直接拷贝构造生成 arr
。但我们还是以理论情况讨论。
3.2 移动构造 --- 神之一笔
上面说到,在理论情况下,需要进行两次拷贝构造才可以接受返回值,对于高效的 C++ 来说,这是不能忍的。所以说,为了解决这个问题,他们想有没有减少拷贝构造次数的方法呢?他们成功的找到了,请看代码:
cpp
// 拷贝构造
Arr(const Arr& arr) {
_ptr = new int[arr._size];
_size = arr._size;
}
void Swap(Arr&& arr) {
std::swap(_ptr, arr._ptr);
std::swap(_size, arr._size);
}
// 移动构造
Arr(Arr&& arr) {
Swap(move(arr));
}
我来细致的讲解一下,发生了什么:
temp
调用拷贝构造函数Arr(const Arr& arr)
,形成临时变量这一步骤是不变的临时变量具有常性
,所以在临时变量构造arr
时,调用的不再是普通的拷贝构造函数,而是Arr(Arr&& arr)
- 将临时变量的资源和接受对象
arr
交换,之后临时变量被析构了
怎么证明呢?通过调试可以发现:
返回值 temp
和 接收对象 arr
的成员变量指针都是指向同一块区域!
夺舍啊!!!大家想一想,临时变量本身就是中间过渡的量,一旦被使用后,马上就被销毁了,所以说,反正你都是暂时的,那我就把你的资源拿来,免得我进行二次拷贝!从两次拷贝构造减少到一次,这个效率,相比原来,不是嘎嘎强多了吗!
3.3 移动赋值
这个就和上面类似了:
cpp
Arr& operator=(Arr&& arr) {
Swap(move(arr));
return *this;
}
当你接受一个临时对象的赋值时,如 arr = Arr(2);
,就不会调用拷贝赋值,直接移动赋值。
4. move 函数作用
移动语义针对的对象都是 将一个临时对象(右值)赋值给正常对象(左值)
。那有没有办法将一个左值变为右值呢?是有的,使用 move(var)
。就比如:
可以看到,对象 move
之后,就从左值变为了右值。但是不推荐向 gif 中演示的那么做,因为我们知道临时对象会将自己的资源全部给到初始化或者是赋值的对象,这样会导致被 move
的对象被悬空。这就好比,我把我的作业给你抄,你咋直接把我的作业占为己有呢!!!但是临时对象就好比没人要的作业,谁都可以直接拥有。
5. 完美转发
5.1 万能引用
在前面说过,类型 + &&
(比如 int&&
)代表的是具体类型的右值引用,那如果是这样呢:
cpp
template<class T>
void PerfectForward(T&& t)
{
Fun(t);
}
这里我们使用了模板,但这就不是单纯的右值引用了哈,模板中的 &&
不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
5.2 类型退化
这个问题需要从实际的运用出发,就比如:
cpp
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);
}
void test_5() {
int A = 1;
PerfectForward(A);
PerfectForward(1);
}
我们首先调用了 PerfectForward(T&& t)
函数,该函数的参数是万能引用,可以接受任何引用。之后,我们又分别调用了 Func()
函数,该函数经过重载后,可以分别接受四个引用类型。我们预期的打印结构,肯定是:
左值引用
右值引用
但是实际情况是:
左值引用
左值引用
这是为什么呢? 因为函数接受右值引用后,该引用退化为了左值引用。 那为什么会发生这种情况呢?当你有一个右值引用 T&& x
时,x
本身是一个左值表达式,因为它有一个持久的名称(即函数参数名)。但是,这并不意味着 x
的"类型"变为了左值引用。它仍然是一个右值引用,只不过在表达式上下文中,它被视为左值。
5.3 完美转发
为了解决传递过程中保持它的左值或者右值的属性的需求,提出了完美转发。
使用方法也很简单,在变量前加上 forward<T>
,如:
cpp
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
完美转发在传参的过程中保留对象原生类型属性。
6. 总结
右值引用的提出是为了解决,局部类型值返回导致多次拷贝的问题。但是,现在右值引用的使用场景非常的丰富,就比如临时变量作为参数传递,也可以减少拷贝的次数。但是在右值引用传递的过程中,可能会导致类型退化为左值,传递出现错误的问题。所以,在右值引用传递时,最好加上 forward<T>
完美转发。
在文章开头提出的疑问,你在阅读完之后,心里是否有了答案,A 是左值!
因为 A
是可以取地址的。
希望大家有所收获,咋们下篇文章见!