标题:[C++11] 右值引用与移动语义
@水墨不写bug
目录
正文开始:
一、C++11的新增语法:右值引用
引用是C++中传统的语法,而C++11中新增了右值引用的语法,因此,为了区分,从现在开始,我们之前学的引用称为左值引用。无论是左值引用还是右值引用,都是给对象取别名。
1.左值与左值引用
a.简介
左值是一个表示数据的表达式(如变量名或解引用的指针)。我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边。
定义时const修饰符后的左值,仍然是左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
b.在语法上:
普通引用(左值引用)
- 语法形式:
类型& 引用名 = 初始值;
- 示例:
int a = 10; int& ref = a;
- 特点:
- 必须在声明时初始化。
- 只能绑定到左值上。
- 相当于变量的别名,不占用额外内存空间。
#include<iostream>
using namespace std;
int main()
{
//p,b,c都是左值
int* p = new int[3] {0};
int b = 3;
const int c = 1;
//rp,rb,rc是对左值的引用
int*& rp = p;
int& rb = b;
const int& rc = c;
return 0;
}
c.常见的应用场景:
普通引用(左值引用)
- 作为函数调用时传入参数,避免拷贝,提高效率。
- 作为函数返回值,可以返回对象的引用(但需保证引用对象在函数返回后依然有效)。
2.右值与右值引用
a.简介
右值也是一个表示数据的表达式。如:字面常量、表达式返回值,函数返回值(不能是左值引用返回)等等。
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
#include<iostream>
using namespace std;
int main()
{
double x = 1.0;
int y = 5;
//一下是常见的右值
19;
x + y;
fmax(x, y);
//一下是对右值的引用
int&& r1 = 19;
double&& r2 = x + y;
double&& r3 = fmax(x,y);
//右值无法出现在赋值符号的左边
//编译报错:左操作数必须为可修改的左值
10 = 1;
x + y = 10;
fmax(x, y) = 0;
return 0;
}
b.语法
右值引用(也称为移动语义引用)
- 语法形式:
类型&& 引用名 = 初始值;
- 示例:
int&& rref = std::move(a);
- 特点:
- 必须在声明时初始化。
- 可以绑定到临时对象(右值)上。
- 允许修改绑定的对象(这就是说,在右值引用之后,引用本身退化为左值)。
c.常见应用场景
右值引用
- 实现移动语义和完美转发,提高资源管理的效率。
- 用于编写高效的 移动赋值运算符重载和 移动构造函数。
- 在模板编程和泛型编程中,右值引用允许函数模板接受右值作为参数,进而实现移动语义。
此外,补充注意:
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
左值引用可以引用右值,但是前提是需要将左值引用类型声明为const;
右值引用也可以引用move之后的左值。
3、左值引用与右值引用总结
左值的特点是可以取地址,可以出现在赋值符号左侧和右侧。
左值引用总结:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
右值的特点是无法取地址,可以出现在赋值符号右侧,但是不能出现在赋值符号左侧。
右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
二、移动语义
其实,你一定早已发现:右值代表的是将亡值,它的生命周期将要到达尽头。而左值就是我们创建的一般变量。
1.移动构造
在下面这几个场景中,你会发现使用拷贝构造会很憋屈,这也就是移动构造要解决的问题。
场景一: 在给map的insert传递参数时,我们需要传递一个pair类型的参数,但是为了传递参数而专门创建一个对象,这就很麻烦。在这样的情况下我们通常选择传递一个临时对象的方法来解决。但是,传临时对象,也会有需要面临的问题:需要把临时对象深拷贝到map中。
为了避免深拷贝,我们可以把右值的数据交换到map中,这样就减少了拷贝。这就引出了移动语义。
场景二:一个函数需要传递一个局部变量返回,由于局部变量出了作用域就会销毁,所以只能传值返回,那么这样一般就需要两次拷贝:局部变量拷贝给临时对象,临时对象拷贝给接受返回值对象,非常浪费资源。
在没有实现移动构造时,str会被识别为右值,但是编译器没有选择,只能使用深拷贝传值返回。但是在实现了移动构造之后,编译器会采用更加适合的方式:调用移动构造。
为了能看清楚移动构造内部的情况,我们使用自己提前写好的string;移动构造参数类型为右值引用,传入的参数是一个将亡值,在函数内部,构造*this,然后需要把参数将亡值与*this的数据交换即可,实现如下:
// 移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
swap(s);
}
2.其他移动语义:移动赋值
在解释清楚移动构造之后,移动赋值就十分清晰了:函数参数为右值引用;在函数内部实现*this与将亡值的交换:
// 移动赋值
string& operator=(string&& s)
{
swap(s);
return *this;
}
移动语义的意义:
小结:左值引用做参数和做返回值都可以提高效率;但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
移动构造的出现是为了实现减少拷贝,把将亡值的数据移动给目的处的赋值对象,移动构造中没有开辟新空间,拷贝数据,所以效率提高了。
完~
未经作者同意禁止转载