C++11——右值引用和移动语义

发布时间:2026/5/15 22:39:12

C++11——右值引用和移动语义 在C11之前的版本基本沿用了C语言之中对于左值与右值的定义。说起来也很简单“在C之中的变量只有左值与右值两种其中凡是可以取地址的变量就是左值而没有名字的临时变量、字面量这种不可以取地址的就是右值”。 正是因为这两种变量常常分别位于的左右两侧所以被命名为左值与右值。1.左值、左值引用左值是一个表示数据的表达式如变量名、解引用的指针左值的根本特征可以取地址所谓左值引用其实前面所学的引用指的就是左值引用左值引用就是给左值的引用给左值取别名。int main() { // 以下的 p、b、c 以及 *p 都是左值 int* p new int(0); int b 1; const int c 2; // 以下几个是对上面左值的左值引用 int* rp p; int rb b; const int rc c; int pvalue *p; //左值也可能在右边 int i 1; int j i; return 0; }左值的定义为何说是表达式而不说是变量因为如*p 这个就属于表达式但它也是左值。const int c 2;//注意这也是左值严格说它叫做常变量const int * p1c;//虽然不能给它进行赋值但它也可以取地址但需要const修饰使得权限是平移的而不是放大的2.右值、右值引用右值也是一个表示数据的表达式如字面常量、表达式返回值函数返回值(非左值引用返回)等等右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边。右值的根本特征不可以取地址右值也就是临时值它们没有持久的内存地址是临时的地址可能立即失效因此不能取地址。右值引用就是对右值的引用给右值取别名。int main() { double x 1.1, y 2.2; // 以下几个都是常见的右值 10; x y; fmin(x, y); //传值返回函数的返回值 // 以下几个都是对右值的右值引用 int rr1 10; double rr2 x y; double rr3 fmin(x, y); // 下面编译会报错“”: 左操作数必须为左值 10 1; x y 1; fmin(x, y) 1; return 0; }2.1右值引用可以取地址右值虽然不能取地址的但是给右值取别名后会导致右值被存储到特定位置然后就可以取到该位置的地址了。例如不能取字面量10的地址但是使用rr1引用后可以对rr1取地址也可以修改rr1。如果不想rr1被修改可以用const int rr1 去引用这是很神奇的用法。int main() { //右值不能取地址 //cout 10 endl; int aa 10; cout aa endl; //但是使用右值引用去引用右值后可以对右值引用取地址而且可以以此来修改右值 int* ptr aa; *ptr 20; cout aa endl; return 0; }2.2左值引用绑定右值int main() { int i1,j2; //右值不能取地址 cout 0; //会报错 cout(i j); //报错 fmin(i,j); //注意是传值返回 return 0; }i j;fmin(i,j);//这两个所指的右值就是返回的临时对象临时值右值它没有持久的内存地址因此不能取地址。它们是临时的地址可能立即失效。其生命周期默认到 当前表达式结束 时销毁但允许绑定到 const T从而延长生命周期注意是使用const修饰的左值引用代码int main() { //使用 const T常量左值引用可以让 左值引用 “间接”绑定右值 int r1 10; //会报错 这里左值引用不能给右值取别名 原因权限放大右值是不可修改的左值引用后就变成可以修改的了这显然是不行的 const int r2 10; //可行 //左值引用不能给右值取别名但是const左值引用可以 return 0; }2.3右值引用给左值取别名右值引用不能给左值取别名但是可以给 move(左值) 后的左值取别名int main() { int i 1; int i1 i; //会报错 int i1 move(i); //可行 return 0; }2.4创建右值引用的意义要知道引用最大的特点就是减少拷贝而大多数场景下左值引用已经可以解决问题1.引用传参2.引用返回 ——前提引用的对象出了作用域生命周期还在如果出了作用域被引用的对象出生命周期已经不在了就不能使用引用返回string to_string(int x) { string ret; while (x) { int t x % 10; ret (0 t); x / 10; } reverse(ret.begin(), ret.end()); return ret; //ret对象出了作用域就会销毁不能使用引用返回 }那么右值引用的意义是什么我们看下面的例子3. 移动构造(拷贝) 和 移动赋值首先自己模拟实现一个string类方便观察namespace jxy { class string { public: typedef char* iterator; size_t size() const { return _size; } size_t capacity() const { return _capacity; } iterator begin() { return _str; } iterator end() { return _str _size; } string(const char* str ) :_size(strlen(str)) , _capacity(_size) { //cout string(char* str) endl; _str new char[_capacity 1]; strcpy(_str, str); } void swap(string s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // 拷贝构造 string(const string s)//const左值引用可以引用右值 { cout string(const string s) -- 深拷贝 endl; string tmp(s._str); swap(tmp); } // 赋值重载 string operator(const string s) { cout string operator(string s) -- 深拷贝 endl; if (this ! s) //不是自己给自己赋值 { char* tmp new char[s._capacity 1]; strcpy(tmp, s._str); delete[] _str; _str tmp; _size s._size; _capacity s._capacity; } return *this; } ~string() { delete[] _str; _str nullptr; } char operator[](size_t pos) { assert(pos _size); return _str[pos]; } void reserve(size_t n) { if (n _capacity) { char* tmp new char[n 1]; strcpy(tmp, _str); delete[] _str; _str tmp; _capacity n; } } void push_back(char ch) { if (_size _capacity) { size_t newcapacity _capacity 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] ch; _size; _str[_size] \0; } string operator(char ch) { push_back(ch); return *this; } string operator(const char* s) { return *this; } const char* c_str() const { return _str; } private: char* _strnullptr; size_t _size0; size_t _capacity0; // 不包含最后做标识的\0 }; //string s1(hello); string to_string(int x) { string ret; while (x) { int t x % 10; ret (0 t); x / 10; } reverse(ret.begin(), ret.end()); return ret; } } int main() { jxy::string s; s jxy::to_string(1234); return 0; }但是这里我们是想使用引用返回的否则需要用ret去调用拷贝构造函数深拷贝返回的临时对象而这个临时对象返回之后也会很快销毁增加了时间和空间的消耗。那么如何解决问题呢拯救ret对象显然是做不到的ret在栈帧里面出了作用域就会销毁。3.1改进利用移动构造 和 移动赋值没有移动构造 和 移动赋值时出现右值拷贝或者赋值的情况会去调用拷贝构造和拷贝赋值因为它们二者的形参类型是const Tconst左值引用可以引用右值。//移动构造 string(string s) { cout string(string s) -- 移动拷贝 endl; swap(s); } //移动赋值 string operator(string s) { cout string operator(string s) -- 移动赋值 endl; swap(s); return *this; } //注意参数这里的右值引用不能加const修饰因为要对右值引用的对象进行修改 int main() { jxy::string s; s jxy::to_string(1234); return 0; }有了移动构造 和 移动赋值函数后同时每个对象指向的空间只会释放一次因为是在互相交换指向空间的指针所以每个空间都会在对应的对象调用析构函数时去释放。s jxy::to_string(1234);就比如这里如果使用的还是拷贝赋值的话s对象的空间会先释放一次然后开辟新空间再去深拷贝函数返回的临时对象STL中的容器都增加了移动构造和移动赋值3.2这样设计的原因在上述的代码中无论是局部对象ret还是函数返回的临时对象实际上都是即将被销毁的对象既然即将被销毁那我们就可以考虑物尽其用虽然对象本身要销毁但是对象所指向的空间可以不去销毁以此来减少空间和时间的消耗也因此我们把它们或直接或间接作为右值去调用移动构造和移动赋值。移动构造本质上就是将参数右值的资源窃取过来占为己有那么就不用做深拷贝了所以它叫做移动构造就是窃取别人的资源来构造自己。还有一点移动构造和移动赋值对于涉及深拷贝的类才有价值如果只涉及浅拷贝比如日期类Date那么移动构造和移动赋值其实是没有什么价值的。3.3补充jxy::string to_string(int x) { jxy::string ret; while (x) { int t x % 10; ret (0 t); x / 10; } reverse(ret.begin(), ret.end()); return ret; }还有一个注意事项上述所有分析都是基于最普通的情况实际上越新的编译器对于构造、拷贝和赋值的优化就越强力比如使用VS2022的话上面的很多情形是观察不到的因为存在大量的、恐怖的优化。比如连续的构造拷贝构造、连续的拷贝构造拷贝构造会优化为一个构造。甚至在to_string函数中局部对象的构造拷贝构造临时对象拷贝构造s对象会直接合三为一优化为直接构造jxy::string to_string(int x) { jxy::string ret; while (x) { int t x % 10; ret (0 t); x / 10; } reverse(ret.begin(), ret.end()); return ret; } int main() { jxy::string sjxy::to_string(1234); return 0; }优化的核心目的之一就是尽量不去生成临时对象从而减少时间和空间的消耗3.4探究VS2022的优化可以借助下面的代码去探究 VS2022 的优化//string s1(hello); jxy::string to_string(int x) { jxy::string ret; while (x) { int t x % 10; ret (0 t); x / 10; } reverse(ret.begin(), ret.end()); return ret; } string to_num(string s) { //string s1; cout s.size() endl; cout to_num函数 endl; return s; } //... int main() { //探究VS2022的优化 //一、 jxy::string s1; s1to_num(jxy::string(hello)); jxy::string s1to_num(jxy::string(hello)); //二、 jxy::string s; cout endl; jxy::to_num(move(s)); cout endl; //这里会调用移动构造 jxy::to_num(jxy::string(hello)); //这里会调用构造 return 0; }3.5应用场景3.5.1swap3.5.2杨辉三角力扣有一道题目杨辉三角涉及到了传值返回118. 杨辉三角 - 力扣LeetCode可以看到移动构造和移动赋值极大地优化了传值返回函数的效率3.5.3容器的插入函数容器中有关插入的函数也实现了参数为右值引用的版本还可以这样书写int main() { listjxy::string lt; lt.push_back(1234); //先构造一个临时对象再调用移动构造去构造push_back的对象 return 0; }为什么不是使用1234调用移动构造去构造临时对象而是调用构造函数去构造临时对象移动构造的前提必须有一个已存在的 jxy::string 对象作为源即“资源”的来源。1234 是字符串字面量不是 jxy::string 对象因此无法直接“移动”它的资源。必须先通过构造函数从 1234 创建一个 jxy::string 临时对象之后才能移动如果适用。可以看到移动语义不止在传值时可以减少拷贝提高效率在传参时也可以3.6后续补充4.纯右值和将亡值C11扩展了右值的概念将右值又细分成了纯右值和将亡值。4.1纯右值纯右值基本等同于我们之前所理解的右值指的是临时变量或字面量值。比如10、ab、fmin(a,b)、string(hello)、传值返回函数返回的临时对象4.2将亡值将亡值可以理解为一个对象这个对象的资源即将被释放或转移对象的资源指的就是比如string这种封装的类内部有一个指向一段空间的指针这种才算是有资源的对象比如一、通过std::move生成int main() { string s; move(s); //这里不会改变 s 自身的值类别它只是生成一个将亡值xvalue的表达式 return 0; }二、返回右值引用的函数std::string get_temp_string() { std::string tmp Temp; return std::move(tmp); // 危险返回局部变量的右值引用仅示例用途 } std::string s3 get_temp_string(); // get_temp_string() 返回将亡值函数返回右值引用时返回值是将亡值但需注意避免返回局部变量的引用这是由语言标准明确定义的表示资源可被安全“窃取”。可以不太准确地认为纯右值是指内置类型的右值将亡值是指自定义类型的右值。4.2.1为什么不能按类型区分一、内置类型也可以有将亡值虽然对int、float等移动无意义但语法上std::move(42)仍生成将亡值。二、自定义类型可以有纯右值临时对象如 string(hello)是纯右值。4.3区别纯右值和将亡值的一个区别在于是否能够显式可移动而不是是否可移动。举例int to_num(string s) { cout s.size() endl; cout to_num函数 endl; return 1; } int main() { jxy::string s; jxy::to_num(move(s)); 这里会调用移动构造 jxy::to_num(jxy::string(hello)); 这里不会调用移动构造呢 这两个传递过去的对象不都是右值吗为什么不都调用移动构造呢 return 0; }4.3.1显式移动和隐式移动纯右值如42、func()是隐式临时值。将亡值是显式标记为“可移动”的表达式。参考博客C雾中风景10:聊聊左值纯右值与将亡值 - HappenLee - 博客园5.moveint main() { jxy::string s1(hello world); //构造 jxy::string s2 s1; //拷贝构造 jxy::string s3 move(s1); //移动构造 //这里会把s1的资源转移给s3使得s1的_str就变为空了 //相当于s1变成这样构造的了 string s1; //move(s1)给一个正常的左值对象打上了将亡值的标签使得它的资源可能会被转移走 return 0; }举一个不恰当的例子就是左值就相当于是正常人右值就相当于是将死的人将死的人可以很合理地去捐赠器官为社会做最后的贡献而s1本身是个正常人但却强行把他标识成了将死的人以此对他强行进行器官捐献。所以不要轻易地对一个左值去move5.1疑问既然move不会改变 s1 自身的值类别它只是生成一个将亡值xvalue的表达式那为什么这里发生了资源交换使得s1指向的空间变成了s3所指向的nullptrmove(s1)只是会返回一个右值的对象而不会把s1本身的属性修改5.2显式允许左值被当作右值使用int main() { jxy::string s1(hello world); jxy::string s4 move(s1); //右值引用只能绑定右值 for (auto e : s1) { cout e; } cout endl; //打印 hello world s4 !; for (auto e : s1) { cout e; } cout endl; //打印 hello world! //可以看到通过右值引用可以修改左值s1 return 0; }6.模拟实现list的右值版本插入函数6.1引入首先我们模拟实现一个list类然后实现它的右值版本的插入函数实现list的右值插入函数后调试分析后可以发现这里就存在一个知识点右值被右值引用 引用之后属性是左值6.2如此设计的原因这是为什么为何要这样设计其实道理很简单我们回顾下我们之前实现的移动构造和移动赋值右值不能直接修改但是右值被右值引用后需要能被修改否则是无法实现移动构造和移动赋值的6.3正确写法正确的写法是要在每次传递右值引用的参数前使用move函数使得传递的参数属性是右值。伪代码如下templateclass T struct list_node { T _data; list_nodeT* _prev; list_nodeT* _next; list_node(const T x T()) :_data(x) ,_prev(nullptr) ,_next(nullptr) {} list_node(T x) :_data(move(x)) //注意这里也要使用move函数 , _prev(nullptr) , _next(nullptr) {} //节点的构造函数也要实现一个右值版本 }; //左值版本 void push_back(const T x) { insert(end(), x); } //右值版本 void push_back(T x) { insert(end(), move(x)); //注意使用move函数 } //右值版本 iterator insert(iterator pos, T x) { Node* cur pos._node; Node* newnode new Node(move(x)); //注意使用move函数 Node* prev cur-_prev; //prev newnode cur prev-_next newnode; newnode-_prev prev; newnode-_next cur; cur-_prev newnode; _size; return iterator(newnode);//指向新插入的元素 }查看结果7.完美转发7.1万能引用在C中使用模板实现的函数可以实现万能引用的功能// 函数模板万能引用 templatetypename T void PerfectForward(T t) { Fun(t); } 这个函数的参数t看似它的类型是右值引用 但实际上它可以接收左值也可以接收右值模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。可以称为引用折叠传递左值时折叠为传递右值时折叠为。注意必须使用模板来实现函数否则是做不到万能引用的我们书写一些代码来验证一下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; } // 函数模板万能引用 templatetypename T void PerfectForward(T t) { Fun(t); } int main() { PerfectForward(10); //右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }结果其实还是老问题虽然模板中的既能接收左值又能接收右值但是无论是接收左值还是接收右值引用之后的参数 t 都会变成左值属性模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要使用一个新的知识点完美转发7.2完美转发实现万能引用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; } // 函数模板万能引用 templatetypename T void PerfectForward(T t) { // 期望保持实参的属性 // 使用完美转发 Fun(std::forwardT(t)); } int main() { PerfectForward(10); //右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }std::forward 完美转发在传参的过程中保留对象原生类型属性此时就真正实现了万能引用上面模拟实现的list中的右值版本插入函数也可以使用完美转发来解决问题。7.3注意事项这样是实现不了万能引用的templatetypename T // 假设这是list类模板的声明 class list { public: void push_back(T x) // 注意这里的 T 是类模板参数 { insert(end(), std::forwardT(x)); } };push_back(T x) 中的 T不是万能引用而是普通的右值引用。万能引用必须满足两个条件一、类型 T 需要被推导如函数模板参数或 auto。二、形式必须严格为 T不能有 const 等修饰。因为这里的T 是类模板参数而不是函数模板参数T 在类实例化时就会确定不会被二次推导。

相关新闻