天天开心!!!
文章目录
- 一、什么是左值和右值?
- 1. 左值(Lvalue)
- 2. 右值(Rvalue)
- 3. 右值的两种分类
- 4. 左值引用(Lvaue Reference)
- 5. 右值引用(Rvalue Reference)
- 二、std::move()
- 1. std::move()的作用
- 2. std::move()的底层原理
一、什么是左值和右值?
1. 左值(Lvalue)
- 具有地址
- 可以出现在赋值号=的左侧
- 可以取地址&
- 变量、对象、数组元素都是左值
示例:
int x = 10; //x是左值
x = 20; //左值可以出现在赋值号=的左侧
int *p = &x; //可以取地址
int a=10;
上面这段代码等同于
00007FF631951E6C mov dword ptr [a],0Ah
对于上面这幅图,我们可以这样理解,左值是有房子的,而右值是住在这个房子里的人
接下来,我们继续深入,我们将代码变换成下面这样
int a=10;a = 20;
我们来观察这个反汇编代码
其实也就是将原本放10的变换成20,如下图
2. 右值(Rvalue)
- 通常没有地址,存储在寄存器或临时内存中
- 不能出现在赋值号=的左侧
- 不能取地址&(除非绑定到const左值引用)
- 字面量、表达式计算结果都是右值
- 示例:
int a =10 + 5; //(10+5)是右值
10=a; //❌错误,右值不能在‘ = ’左侧
int &ref = 10; //❌错误,普通引用不能绑定右值
const int &ref = 10; //✔正确const引用可以绑定右值
对于右值来说,我们可以这样理解,右值是一个没有房子的、孤零零的一个人,这没法改变
int a=10; //这里的10就是右值,右值没有地址属性的
int& b = a; //引用是变量的别名,这里的b是左值引用,b是一个左值,b有地址属性的
也就是下面这样
b = 20; //这里的20就是右值,右值没有地址属性
rax也就是a的地址
还有接下来这种情况
const int &&c = 20; /*右值引用,这里的c就是对右值20的引用,
相当于一个指针指向20,只不过20是一个右值,20没有地址属性*/
[rbp+64h]是一个栈顶指针,因为此时的20是没有家(也就是地址属性),所以只能用一个临时住所(地址),此时这个20就有了‘家’,但这个家只是临时的,很快就会消失,然后我们将这个指针指向我们的c,c就存储这个右值,称为右值的临时变量。
int a=10;
const int &&c = a; //这行代码是会报错的
//因为a是一个左值,左值有地址属性,而右值引用只能引用右值,不能引用左值
因为a是一个左值,左值有地址属性,而右值引用只能引用右值,不能引用左值
于是此时我们可以引入C++中的std::move()来解决这个问题
const int&& c = std::move(a);//std::move(a)将a转换为了右值,所以c可以引用a
但此时也出现了一个问题,让我们来看一下汇编代码
它这里是读取了a的地址,然后让这个c指向这个a,这时我们会不会想起左值引用,是不是一样的???
我们来看看两个的汇编指令代码
指令是完全相同的,实际上,这个c也是指向a的指针,和这个b的没有任何的区别,汇编代码都是一样的,这里的move仅仅是将a的性质变了,这个move告诉我们的编译器,现在的这个a是右值了,准确的来说应该是std::move(a)是右值了,这个右值又称为“ 亡值 "(亡值就有了地址).其目的是,可以让这个c在传递参数的时候或者在赋值的时候触发移动构造!!!避免深拷贝。
3. 右值的两种分类
- 纯右值(PRvalue):真正的右值,如10,x+y
- 亡值(Xvalue):即将销毁但仍占有资源的对象,如std::move(obj)
类型 | 示例 | 能否取地址? | 能否赋值? |
---|---|---|---|
左值(Lvalue) | int x=10; | √可以 | √可以 |
纯右值(PRvalue) | 10,x+y | ❌不能 | ❌不能 |
亡值(Xvalue) | std::move(obj) | √可以 | ❌不能 |
4. 左值引用(Lvaue Reference)
- 定义:T&绑定到左值
- 特性:可以修改原变量,不能绑定右值
示例:
int a=10;
int& b = a;//左值引用绑定左值
b = 20; //可以修改a
错误示例:
int &ref2=10;//❌错误,右值不能绑定左值引用
5. 右值引用(Rvalue Reference)
- 定义:T&&绑定右值
- 特性:用于移动语义,避免拷贝,提高性能
示例:
int &&ref=10;//✔右值引用绑定右值
不能绑定左值:
int b=20;
int &&ref=b;//❌错误,左值不能被右值引用
std::move()强制转换左值为右值
int &&rref=std::move(b);//✔,b被转换成右值
二、std::move()
1. std::move()的作用
- 将左值转换成右值,以触发移动语义
- 不会真正”移动数据“,只是改变对象的属性
- 用于触发移动构造和移动赋值,避免深拷贝,提高性能
示例:
#include<iostream>
#include<utility>
#include<string>
using namespace std;
int main()
{string s1="hello";string s2=std::move(s1);//std::move(s1)将s1转换为了右值,所以s2可以引用s1cout<<s1<<endl; //输出空字符串,因为s1已经被移动了,s1的内容已经被转移到了s2中cout<<s2<<endl; //输出helloreturn 0;
}
如果这里的s2没有使用move(),直接使用s1=s2,这实际上就是拷贝构造,触发的是深拷贝;但这也产生了一个问题,如果我们的s1很长,那么我们在拷贝的时候就会浪费很多很多的资源,这就会导致我们的性能下降。。为了避免这个深拷贝带来的资源浪费,我们就很有必要去使用这个move.。
2. std::move()的底层原理
std::move()只是static_cast<T&&>,不会改变对象的生命周期
template<typename T>
constexpr remove_reference_t<T>&& move(T&& t) noexcept {return static_cast<remove_reference_t<T>&&>(t);
}
示例:
int a=10;
int &&ref=std::move(a);//ref绑定a的右值引用
- std::move(e)只是告诉编译器a变成右值,但不会修改a本身
- ref绑定a的地址,可以继续使用a,但内容可能已经变空