关于左值、右值、将亡值和移动构造
c++中所有的变量都是以下三者之一:
- 纯右值prvalue(Pure Right-hand-side Value):返回值是一个单纯的字面常数,被保存在寄存器中,C语言只有操作内存的能力而没有染指寄存器的能力,我们只能感知到他的值而不能改变。即prvalue是没有内存实体的值,所以prvalue没有地址
- 左值lvalue:不存在所谓返回中转值,实际上就是子栈帧直接负责输出到父栈帧中,此时它是一个具有地址的,实实在在可以被操作的值。
- 将亡值xvalue(Expiring Value):可调用对象的返回值被保存在一个匿名内存空间中,它也像纯右值那样只是一段数据信息而没有实体!不能被修改,不能取地址。它在完成某个动作后就立刻失效了,失效得很快,所以称之为将亡值。move函数返回的右值变量就是将亡值
将亡值和纯右值都是临时对象,如果不使用它(也就是找个左值变量或者右值引用来保存它)就会被立即销毁,
例如函数原型为 int func(); 如果我们直接调用 func(); ,那么他的返回值会立即被丢弃,如果我们需要使用它就要用一个变量来保存他的返回值: int i=func();
又如:
#include <iostream> using namespace std; int idx=0; class a{ private: int id=0; public: a():id(idx++){cout<<"a" << id << " is construct"<<endl;} a(a &var){id = var.id;cout<<"a" << id << " is copy construct"<<endl;} // 注意下面的入参的const性质,这个const是必须有的,如果我们把const去掉,编译会出错! // 因为在下面的代码中有 val = func(); 函数的返回值是一个将亡值,它具有const性质(不能修改)!所以如果这里的入参是非const的话调用就会出错! a& operator=(const a &rhs) {id = rhs.id;cout<<"a" << id << " is copy ="<<endl;return *this;} ~a(){cout<<"a" << id << " is destroy"<<endl;} }; a func(){ a var; return var; } int main(){ a val; val = func(); //func(); return 0; } /* 结果: a0 is construct // val被构造 a1 is construct // func中的局部变量var被构造 a1 is copy = // func返回的临时变量被赋给val,调用赋值运算符 a1 is destroy // func中的局部变量var被销毁 a1 is destroy // val被销毁 */ /* 结论:对于 func()函数,在return时候,并不是返回变量var本身, var在func()函数结束时生存期也结束,var的值会被存储在一个临时量中,这个临时量就叫做“将亡值” 把var存储到将亡值是不会发生拷贝构造的!所以函数返回的时候只会发生1次复制(将亡值被赋给val的时候调用operator=) */
可以把右值赋给左值变量,也可以赋给右值引用。左值变量可以接收左值,也可以接受右值,但右值引用(为什么不说右值变量呢?因为右值变量是形如2,“hello”,2.5这样的纯字面值)只能接收右值或将亡值,不能接收左值或左值引用。
(但有趣的是,对模板而言,右值引用模板参数T &&可以接收左值也可以接收右值,而左值引用T&只能接收左值 c++模板的引用类型参数折叠问题解释)
例如:
int i = 2; // 正确,2是纯右值(或者说右值变量)
int &&r = 2; // 正确,r是右值引用
int &a = 2; // 错误,不能赋给左值引用
int &&b = std::move(r); // 正确,std::move(r) 是将亡值
int &&c = b; // 错误,=号右边是表达式,所以b是一个变量表达式,变量表达式都是左值(cpp primer P472)!所以不能把左值赋给右值引用!
int &&c = std::move(b); // 正确
右值变量只有内容,没有承载这个内容的实体,他表示一个数据信息,你不能像修改左值那样去修改右值变量,不能去取右值变量的地址
右值引用是右值变量的别名,左值引用是左值变量的别名。可以把左值想象为容器(不是stl那个容器),而右值是容器里的内容。
std::move操作是获取一个基本内置类型的右值引用,对一个左值使用move操作后会破坏这个左值!要注意move之后这个左值就不能再使用它原来的值了(cpp primer P471)!(但是可以给它赋新值)
移动构造操作是一种“窃取”源对象的资源的行为,他不会为新构造的对象分配任何内存,而是依赖move操作,用源对象的右值作为入参,接管源对象的资源,在移动构造函数内我们直接让新对象的成员调用operator=源对象成员即可
(因为入参本来就是源对象的右值,所以相当于直接把右值赋给左值,相当于对每个成员都使用 a = std::move(old_a) 操作,新对象的成员直接结果了老对象的资源,省去了构造操作)(cpp primer P473)
注意:
- 如果源对象中有成员指针指向堆内存,则移动构造会让新对象指针指向源对象的内存而不是像拷贝构造那样申请新内存再拷贝过来。
- 对于非指针成员,虽然说移动构造过程不会申请新内存,但是源对象和新对象的非指针成员在栈内存中并不同一个(例如下面的代码中str1和str2并不是同一地址),只不过新对象是用源对象的将亡值来进行初始化,这是移动构造比拷贝构造更高效的关键所在
自定义类的移动构造函数依然以右值作为入参,所以需要在调用移动构造的地方对源对象使用 std::move(源对象)将其转换为右值(eg: cpp primer P473)
#include <iostream> using namespace std; int main(){ string str1("hello"); cout<<"addr str1: "<<&str1<<endl; string str2 = move(str1); cout<<"str2: "<<str2<<" addr str2: "<<&str2<<endl; cout<<"addr str1 after: "<<&str1<<endl; cout<<"str1: "<<str1<<endl; return 0; } /* result: addr str1: 0x7fffffffda10 str2: hello addr str2: 0x7fffffffda30 addr str1 after: 0x7fffffffda10 str1: */
什么时候使用移动构造:
- 类似 IO类(istream,ostream)和unique_ptr类不能被复制或共享;
- 有些对象我们只是想给它挪个位置,比如当前位置内存空间不足的时候需要挪到新空间(eg: cpp primer P482)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~