当前位置: 首页 > news >正文

C++11 移动语义与右值

C++11 移动语义与右值引用核心知识点

C++11 引入的移动语义(Move Semantics)和右值引用(Rvalue Reference)是现代 C++ 中至关重要的性能优化特性。它们旨在解决旧版 C++ 中临时对象(temporary objects)产生的不必要的深拷贝开销问题。

1. 左值(Lvalue)与右值(Rvalue)

在理解移动语义之前,必须先区分左值和右值。

  • 左值 (Lvalue - "Locator Value"): 指的是表达式结束后依然存在的、拥有明确内存地址的对象。简单来说,可以取地址、可以放在赋值运算符 = 左边的就是左值

    • 例如:变量、函数返回的引用、字符串字面量等。

  • 右值 (Rvalue - "Read Value"): 指的是表达式结束后就不复存在的临时对象。简单来说,不能取地址、不能放在赋值运算符 = 左边的就是右值

    • 例如:字面量(10, true)、函数按值返回的临时对象、表达式的中间结果(a + b)等。

int x = 10; // x 是左值, 10 是右值
std::string s = "hello"; // s 是左值, "hello" 是一个特例,是左值
int y = x + 1; // y 和 x 都是左值, (x + 1) 的计算结果是一个临时对象,是右值int& ref = x; // 左值引用 ref 绑定到左值 x
// int& ref2 = 10; // 错误!不能将一个左值引用绑定到右值const int& ref3 = 10; // 正确,const 左值引用可以绑定到右值,并延长其生命周期

2. 右值引用(Rvalue Reference)

为了能够“捕获”并操作即将销毁的右值,C++11 引入了右值引用,其语法是 T&&

  • 核心功能:只能绑定到右值(临时对象)。

  • 主要目的:识别出临时对象,从而安全地“窃取”其内部资源,避免昂贵的拷贝操作。

int&& r_ref1 = 10; // 正确,右值引用绑定到右值
int x = 5;
// int&& r_ref2 = x; // 错误!不能将右值引用绑定到左值std::string s1 = "hello";
// std::string&& s_ref = s1; // 错误!s1 是左值std::string&& s_ref2 = std::string("world"); // 正确,绑定到一个临时 string 对象

注意: 一个被声明的右值引用本身是一个左值!因为它有名字,可以被取地址。

int&& r_ref = 10;
// int&& r_ref2 = r_ref; // 错误!r_ref 是一个有名字的变量,所以它是一个左值

3. 移动语义(Move Semantics)

移动语义的核心思想是资源所有权的转移。对于一个持有动态分配资源(如堆内存、文件句柄、网络套接字等)的对象,当它被用作一个右值时(例如,作为函数返回值或即将被销毁),我们可以将其内部资源的指针直接转移给新的对象,而无需重新分配和复制资源内容。

这通过两个特殊的成员函数实现:移动构造函数移动赋值运算符

示例:一个简单的 MyBuffer

我们创建一个管理动态内存的类来说明。

#include <iostream>
#include <utility> // for std::move
#include <vector>class MyBuffer {
private:int* _data;size_t _size;public:// 默认构造函数MyBuffer() : _data(nullptr), _size(0) {std::cout << "默认构造函数 MyBuffer()\n";}// 带参构造函数MyBuffer(size_t size) : _size(size) {_data = new int[size];std::cout << "带参构造函数 MyBuffer(" << size << ")\n";}// 析构函数~MyBuffer() {std::cout << "析构函数 ~MyBuffer() " << (_data ? "释放数据" : "空指针") << "\n";delete[] _data;}// 1. 拷贝构造函数 (深拷贝)MyBuffer(const MyBuffer& other) : _size(other._size) {std::cout << "拷贝构造函数 (深拷贝)\n";_data = new int[_size];for (size_t i = 0; i < _size; ++i) {_data[i] = other._data[i];}}// 2. 拷贝赋值运算符 (深拷贝)MyBuffer& operator=(const MyBuffer& other) {std::cout << "拷贝赋值运算符 (深拷贝)\n";if (this == &other) {return *this;}delete[] _data; // 释放旧资源_size = other._size;_data = new int[_size];for (size_t i = 0; i < _size; ++i) {_data[i] = other._data[i];}return *this;}// 3. 移动构造函数 (浅拷贝 + 资源转移)MyBuffer(MyBuffer&& other) noexcept : _data(other._data), _size(other._size) {std::cout << "移动构造函数 (资源转移)\n";// 将源对象的指针置空,防止其析构函数释放资源other._data = nullptr;other._size = 0;}// 4. 移动赋值运算符 (资源转移)MyBuffer& operator=(MyBuffer&& other) noexcept {std::cout << "移动赋值运算符 (资源转移)\n";if (this == &other) {return *this;}delete[] _data; // 释放当前对象的资源// 窃取源对象的资源_data = other._data;_size = other._size;// 将源对象置于有效但“空”的状态other._data = nullptr;other._size = 0;return *this;}
};// 一个返回 MyBuffer 临时对象的函数
MyBuffer createBuffer(size_t size) {return MyBuffer(size);
}int main() {std::cout << "--- 场景1: 移动构造 ---\n";// createBuffer(10) 返回的是一个右值(临时对象)// 因此会匹配并调用移动构造函数来创建 b1MyBuffer b1 = createBuffer(10); std::cout << "\n--- 场景2: 拷贝构造 ---\n";// b1 是一个左值,因此调用拷贝构造函数创建 b2MyBuffer b2 = b1; std::cout << "\n--- 场景3: 移动赋值 ---\n";MyBuffer b3;// createBuffer(20) 返回的是右值,调用移动赋值运算符b3 = createBuffer(20);std::cout << "\n--- 程序结束 ---\n";return 0;
}

代码讲解:

  • 拷贝构造/赋值:创建全新的内存,并将数据逐一复制。开销大。

  • 移动构造/赋值:不分配新内存,而是直接“偷”走源对象(other)的 _data 指针,然后将源对象的指针设为 nullptr。这样,当源临时对象析构时,它不会释放我们已经接管的内存。这个过程非常快,只涉及几个指针的赋值。

  • noexcept关键字:建议移动操作不抛出异常。这对于标准库容器的性能至关重要。

4. std::move

std::move 本身不进行任何移动操作。它的唯一功能是将一个左值强制转换为右值引用,从而让我们可以对这个左值实施移动语义。

它像是在告诉编译器:“我知道这是一个左值,但我承诺之后不再使用它的资源了,你可以把它当作临时对象来处理,移动它的资源吧。”

int main() {std::vector<int> vec1 = {1, 2, 3, 4, 5};// vec1 是左值,这里会调用拷贝构造函数,vec1 和 vec2 内容相同但内存独立std::vector<int> vec2 = vec1; // 使用 std::move 将左值 vec1 转换为右值引用// 这会触发 vector 的移动构造函数// vec2_moved 的内容是 {1,2,3,4,5}// vec1 的资源被转移,其状态变为空(或某个有效的未定义状态)std::vector<int> vec2_moved = std::move(vec1); std::cout << "vec1 size after move: " << vec1.size() << std::endl; // 输出通常为 0std::cout << "vec2_moved size: " << vec2_moved.size() << std::endl;// 警告:使用 std::move 之后,不应该再对原对象(vec1)做任何假设,// 除非重新给它赋值。它的状态是有效的,但内容是未知的。
}

5. 完美转发(Perfect Forwarding)与 std::forward

这是一个更高级的应用场景,通常用在模板编程中。

  • 问题:一个函数模板接收一个参数,并需要将其“原封不动”地传递给另一个函数。这里的“原封不动”指的是要保持参数原有的左值/右值属性

  • 解决方案:使用**转发引用(Forwarding Reference,也叫通用引用 Universal Reference)**和 std::forward

    • 转发引用:当函数模板参数是 T&& 形式,且 T 是一个需要推导的类型时,这个参数就是转发引用。它既可以接收左值,也可以接收右值。

    • std::forward:一个条件转换工具。当传递给它的原始参数是右值时,它会将其转换为右值引用;当原始参数是左值时,它会将其转换为左值引用。

#include <iostream>
#include <utility>struct Widget {Widget() { std::cout << "默认构造\n"; }Widget(const Widget&) { std::cout << "拷贝构造\n"; }Widget(Widget&&) { std::cout << "移动构造\n"; }
};// wrapper 是一个工厂函数,它接收参数并将其完美转发给 Widget 的构造函数
template<typename T>
void wrapper(T&& arg) {std::cout << "在 wrapper 中转发: ";// 使用 std::forward 保持 arg 的原始值类型Widget w = std::forward<T>(arg); 
}int main() {Widget x;std::cout << "传递左值:\n";wrapper(x); // x 是左值,wrapper(Widget&),std::forward<Widget&> 转为左值,调用拷贝构造std::cout << "\n传递右值:\n";wrapper(Widget()); // Widget() 是右值,wrapper(Widget&&),std::forward<Widget> 转为右值,调用移动构造std::cout << "\n传递 move 后的左值:\n";wrapper(std::move(x)); // std::move(x) 是右值,调用移动构造return 0;
}

代码讲解: wrapper 函数中的 std::forward<T>(arg) 是关键。如果没有它,argwrapper 函数体内永远是左值(因为它有名字),那么传递给 Widget 构造函数的将永远是左值,导致总是调用拷贝构造,完美转发就失败了。std::forward 确保了当 wrapper 接收一个右值时,传递给 Widget 的也是一个右值。

总结

  1. 右值引用 T&&:一种新的引用类型,专门用于绑定到即将销毁的临时对象(右值)。

  2. 移动语义:利用右值引用,通过移动构造函数移动赋值运算符,实现资源所有权的转移,而非昂贵的深拷贝。

  3. std::move:一个强制类型转换工具,将左值转换为右值引用,是手动启用移动语义的开关。使用后,原对象状态未知,不应再使用。

  4. 完美转发:在模板函数中,通过转发引用 T&&std::forward,可以保持参数在传递过程中的左值/右值属性不变。

掌握这些特性是编写高性能、现代 C++ 代码的基础。

http://www.dtcms.com/a/395411.html

相关文章:

  • FPGA学习笔记——图像处理之对比度调节(直方图均衡化)
  • 如何进行人脸识别
  • 计算机视觉笔试选择题:题组1
  • 第八篇:常量表达式:从const到constexpr的革命
  • RV1126 NO.30:RV1126多线程获取音频AI的PCM数据
  • 基于蚁群算法解决车辆路径问题(VRP)的MATLAB实现
  • C语言自学--C语⾔内存函数
  • 磁带记录仪:从磁带到数字的数据存储之旅
  • 【运维】Ubuntu上WebDAV挂载与自动同步完整指南
  • Ubuntu之旅-04 Docker
  • python(73) 引用.dll文件并调用函数
  • Chrome 学习小记5——demo:(动态壁纸基础)
  • 手写 Android Dex VMP 壳:自定义虚拟机 + 指令解释执行全流程
  • 【Netty】创建一个 SSL 处理器,实现客户端与服务器之间的安全通信
  • 13 Python数据结构与算法
  • 爱:宇宙的心跳
  • Python字节数据写入文本文件完全指南:从基础到高级实战
  • 零基础Windows10安装LLVM
  • selenium三种等待方式详解
  • Leetcode总结速记
  • 手写 Android Dex VMP 壳:指令流 AES 加密 + 动态加载全流程
  • 视频融合平台EasyCVR国标GB28181视频诊断功能详解与实践
  • ORACLE adg 备库也能单独提取AWR报告
  • Angular由一个bug说起之十九:Angular 实现可拓展 Dropdown 组件
  • Kafka核心架构与高效消息处理指南
  • flink1.18配置多个上游source和下游sink
  • 快速查看自己电脑的ip地址:一个命令见本机私网ip,一步查询本地网络公网ip,附内网ip让外网访问的通用方法
  • 插件化(Plugin)设计模式——Python 的动态导入和参数解析库 argparse 的高级用法
  • 【JavaSE】【网络原理】UDP和TCP原理
  • 高防IP真的能抵御DDoS攻击吗?