[C++11] 右值引用、移动语义与std::move C11 右值引用、移动语义与 std::move 详解一、什么是左值 右值1. 左值lvalue2. 右值rvalue简易记忆口诀二、左值引用 C11 新增右值引用1. 回顾C98 左值引用 2. C11 右值引用 基础示例关键特性三、为什么需要右值引用—— 解决拷贝浪费1. 传统拷贝的问题执行流程C98 模式2. 解决方案移动语义四、移动构造函数移动语义核心1. 语法格式2. 给上面的MyString 添加移动构造函数3. 完整运行 效果对比C11 执行流程补充规则五、std::move 函数强制转为右值1. 作用2. 语法3. 示例演示4. 重要使用提醒六、移动赋值运算符1. 语法2. 使用场景七、四大构造/赋值函数总结八、常见使用场景 实战建议1. 什么时候会自动触发移动语义2. 什么时候手动用 std::move3. 注意事项总结头文件utility一、什么是左值 右值在 C 中表达式根据能否取地址、是否持久存在分为左值和右值这是理解后续知识点的基础。1. 左值lvalue定义有名字、可以取地址的表达式程序运行中内存持久存在。常见例子变量、数组元素、引用。inta10;// a 是左值有变量名可以 a 取地址intba;// a 依旧是左值intarr[3]{1,2,3};arr[0];// 数组元素也是左值特点能出现在赋值运算符左边所以叫左值。a 20;合法a是左值。2. 右值rvalue定义没有名字、不能取地址的临时数据用完就销毁生命周期极短。常见例子字面量、表达式运算结果、函数返回的临时对象。100;// 字面量右值无法 100 取地址ab;// 加法运算结果临时值右值std::string(test);// 临时字符串对象右值特点只能出现在赋值运算符右边不能取地址。100 a;编译报错右值不能被赋值。简易记忆口诀有名字、能取地址 → 左值无名字、临时值、不能取地址 → 右值二、左值引用 C11 新增右值引用引用就是给变量起别名C98 只有左值引用C11 补充了右值引用。1. 回顾C98 左值引用 语法类型 引用名 左值;只能绑定左值不能直接绑定纯右值。inta10;intraa;// 合法左值引用 绑定 左值// int rb 20; // 编译报错左值引用不能直接绑定字面量右值constintrc20;// 特殊const 左值引用可以绑定右值C98 特性//这么做会延长右值的生命周期2. C11 右值引用 语法类型 引用名 右值;作用专门用来绑定右值临时对象符号两个和左值引用区分开基础示例// 1. 绑定字面量右值intr1100;r1200;// 右值引用本质也是别名可以修改coutr1;// 输出 200//普通右值引用 T 可以修改const T 不能修改几乎不用。// 2. 绑定表达式临时结果右值intx1,y2;intr2xy;coutr2;// 输出 3关键特性右值引用只能绑定右值不能直接绑定普通左值inta10;// int r a; // 编译报错左值不能绑定到右值引用一旦右值引用绑定了临时右值这个引用本身就变成了左值有名字、可寻址。三、为什么需要右值引用—— 解决拷贝浪费1. 传统拷贝的问题对于自定义类、字符串、容器这类占用堆内存的对象普通拷贝会做深拷贝完整复制一份内存数据临时对象产生 → 拷贝 → 临时对象销毁产生大量不必要的内存开销和性能损耗。举个例子自定义字符串类模拟std::string#includeiostream#includecstringusingnamespacestd;classMyString{private:char*data;public:// 构造函数MyString(constchar*str){cout构造函数endl;intlenstrlen(str);datanewchar[len1];strcpy(data,str);}// 拷贝构造函数深拷贝MyString(constMyStringother){cout拷贝构造函数深拷贝endl;intlenstrlen(other.data);datanewchar[len1];// 重新开辟内存strcpy(data,other.data);}// 析构函数~MyString(){cout析构函数endl;delete[]data;}};// 返回临时 MyString 对象右值MyStringgetTempStr(){returnMyString(hello c11);}intmain(){MyString sgetTempStr();return0;}执行流程C98 模式getTempStr()内部创建临时对象 → 调用构造函数临时对象赋值给s→ 调用拷贝构造函数完整复制内存临时对象生命周期结束 → 调用析构函数释放内存问题临时对象马上就要销毁我们明明可以直接把临时对象的内存“抢过来”用却还要完整拷贝一遍纯纯资源浪费。2. 解决方案移动语义移动语义借助右值引用把即将销毁的临时对象右值的内存资源直接转移给新对象而非拷贝。动作转移所有权不是复制数据效率几乎零开销比深拷贝快得多四、移动构造函数移动语义核心C11 允许我们重载移动构造函数语法基于右值引用。1. 语法格式类名(类名临时对象){// 转移资源不拷贝}2. 给上面的MyString 添加移动构造函数// 移动构造函数参数是 右值引用MyString(MyStringother){cout移动构造函数转移资源endl;// 1. 直接接管对方的堆内存指针dataother.data;// 2. 把原对象指针置空防止原对象析构时重复释放内存other.datanullptr;}3. 完整运行 效果对比再次运行MyString s getTempStr();C11 执行流程创建临时对象 → 构造函数临时对象是右值编译器自动匹配移动构造函数转移指针无内存拷贝临时对象析构指针已置空不会释放有效内存总结有了移动构造临时对象不再深拷贝直接移交资源性能大幅提升。补充规则编译器匹配规则传入左值→ 调用 拷贝构造const 类传入右值→ 调用 移动构造类移动构造函数中必须把源对象的资源指针置空否则两个对象指向同一块堆内存析构时会重复释放程序崩溃。五、std::move 函数强制转为右值1. 作用std::move定义在utility头文件中功能只有一个强制把一个左值转换成右值场景明明是有名字的左值对象不会马上销毁但我们确定不再使用它希望把它的资源移动给其他对象而非拷贝。2. 语法std::move(左值变量)3. 示例演示沿用MyString类intmain(){MyStrings1(test move);// s1 是普通左值// 直接赋值左值 → 调用拷贝构造MyString s2s1;// 使用 std::move把左值 s1 强制转为右值MyString s3std::move(s1);return0;}运行结果s2 s1调用拷贝构造s3 std::move(s1)s1被转为右值调用移动构造4. 重要使用提醒执行std::move(s1)之后s1的堆内存资源已经被转移走内部指针变为nullptr不要再正常使用 s1相当于一个“空壳对象”强行读写会出问题std::move本身不移动任何数据它只是做类型转换真正转移资源的是移动构造/移动赋值函数。六、移动赋值运算符和拷贝赋值对应C11 也支持移动赋值同样使用右值引用。1. 语法类名operator(类名other){cout移动赋值运算符endl;// 1. 释放自身原有资源delete[]data;// 2. 接管对方资源dataother.data;// 3. 原对象置空other.datanullptr;return*this;}2. 使用场景MyStrings4(aaa);MyStrings5(bbb);s5std::move(s4);// 移动赋值而非拷贝赋值七、四大构造/赋值函数总结C11 之后一个常规类默认存在 6 个特殊成员函数这里区分核心 4 个函数类型参数形式触发场景行为普通构造函数类名(参数)创建新对象时初始化资源拷贝构造函数类名(const 类)用左值对象初始化新对象深拷贝数据移动构造函数类名(类)用右值对象初始化新对象转移资源拷贝赋值运算符operator(const 类)左值对象赋值深拷贝数据移动赋值运算符operator(类)右值对象赋值转移资源八、常见使用场景 实战建议1. 什么时候会自动触发移动语义函数返回临时对象、字面量构造的临时对象 → 编译器自动识别为右值调用移动构造。2. 什么时候手动用 std::move明确某个左值对象后续不再使用想把它的资源转移出去手动转成右值。例容器元素转移、局部对象移交。3. 注意事项右值引用只能绑定右值普通左值不能直接赋值给右值引用std::move只是类型转换不是移动动作本身移动资源后原对象变为空壳禁止继续正常使用移动构造/移动赋值中一定要将源对象指针置空防止重复析构崩溃内置类型int/double移动和拷贝无区别移动语义主要针对堆内存对象string、vector、自定义类。总结左值有名字、可寻址右值临时值、无名字、不可寻址。C11 新增右值引用专门绑定右值。移动语义利用右值引用转移临时对象资源替代低效深拷贝。移动构造/移动赋值实现移动语义的核心函数。std::move强制将左值转为右值手动触发移动语义。核心价值优化内存拷贝提升程序运行效率。