C++学习:六个月从基础到就业——C++11/14:右值引用与移动语义

C++学习:六个月从基础到就业——C++11/14:右值引用与移动语义

本文是我C++学习之旅系列的第三十九篇技术文章,也是第三阶段"现代C++特性"的第一篇,主要介绍C++11/14中引入的右值引用和移动语义。查看完整系列目录了解更多内容。

引言

C++11引入的右值引用和移动语义是现代C++最重要的特性之一,它解决了传统C++中昂贵的深拷贝问题,显著提高了程序性能,尤其是在处理大型对象和临时对象时。本文将深入探讨右值引用和移动语义的概念、实现方式以及实际应用,帮助你理解和掌握这一强大特性。

左值与右值的基本概念

在深入理解右值引用之前,我们需要先清楚左值(lvalue)和右值(rvalue)的概念。

传统的左值与右值

最初的定义非常直观:

  • 左值:可以出现在赋值表达式左侧的表达式
  • 右值:只能出现在赋值表达式右侧的表达式

但这个定义在现代C++中已经不够精确了。更现代的定义是:

  • 左值:有身份(可以取地址)且可以被移动的表达式
  • 右值:有身份或可以被移动,但不同时满足这两个条件的表达式

左值和右值示例

int x = 10;      // x是左值,10是右值
int y = x;       // x是左值,用于初始化另一个左值y
int& ref = x;    // 左值引用必须绑定到左值上
int&& rref = 20; // 右值引用绑定到右值20上// 函数返回的临时值是右值
int getVal() { return 42; }
// int& r = getVal(); // 错误:不能将左值引用绑定到右值
int&& rr = getVal(); // 正确:右值引用可以绑定到右值

左值引用与右值引用

  • 左值引用:使用单&符号,只能绑定到左值
  • 右值引用:使用双&&符号,只能绑定到右值
  • 常量左值引用:是个特例,可以绑定到左值或右值
int x = 10;
int& ref1 = x;            // 正确:左值引用绑定到左值
// int& ref2 = 10;        // 错误:左值引用不能绑定到右值
const int& ref3 = 10;     // 正确:const左值引用可以绑定到右值
int&& rref1 = 10;         // 正确:右值引用绑定到右值
// int&& rref2 = x;       // 错误:右值引用不能绑定到左值
int&& rref3 = std::move(x); // 正确:std::move将x转换为右值

右值引用详解

右值引用的语法与特性

右值引用使用双&&符号声明,主要用于绑定临时对象(右值):

// 右值引用基本语法
int&& rref = 42;  // 绑定到字面量(右值)
int&& rref2 = getVal();  // 绑定到函数返回的临时值(右值)

右值引用的关键特性:

  1. 延长临时对象的生命周期
  2. 允许修改被引用的临时对象
  3. 为移动语义提供基础

引用折叠规则

在模板和auto推导中,涉及到右值引用的引用(如 T&& &&)时,C++使用引用折叠规则:

  • T& & 折叠为 T&
  • T& && 折叠为 T&
  • T&& & 折叠为 T&
  • T&& && 折叠为 T&&

简单记忆:只要有一个是左值引用(单&),结果就是左值引用。

完美转发

完美转发是指在函数模板中,将参数按照其原始类型(保持左值/右值属性)转发给另一个函数:

template<typename T>
void perfectForward(T&& arg) {// std::forward保持arg的值类别(左值或右值)processArg(std::forward<T>(arg));
}int main() {int x = 10;perfectForward(x);        // x作为左值传递perfectForward(42);       // 42作为右值传递return 0;
}

std::forward的作用是:如果传入的是左值,则作为左值转发;如果传入的是右值,则作为右值转发。

移动语义

移动语义的基本概念

移动语义允许将资源(如动态分配的内存)从一个对象"偷"到另一个对象,而不是进行昂贵的复制。它特别适用于:

  • 临时对象被用于初始化另一个对象
  • 对象即将被销毁(如函数返回值)
  • 明确不再需要对象的原始状态

std::move的作用

std::move是一个用于将左值转换为右值引用的函数模板,它本身不移动任何东西,只是允许移动操作发生:

template <typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept {return static_cast<typename std::remove_reference<T>::type&&>(arg);
}

注意:调用std::move后,被移动对象进入"有效但未指定"的状态,不应再使用它的值(除非重新赋值)。

移动构造函数与移动赋值运算符

移动构造函数和移动赋值运算符是支持移动语义的关键组件:

class MyString {
private:char* data;size_t size;public:// 移动构造函数MyString(MyString&& other) noexcept: data(other.data), size(other.size) {// 将源对象置于有效但可预测的状态other.data = nullptr;other.size = 0;}// 移动赋值运算符MyString& operator=(MyString&& other) noexcept {if (this != &other) {delete[] data;  // 释放自身资源// 从other"窃取"资源data = other.data;size = other.size;// 将other置于有效但可预测的状态other.data = nullptr;other.size = 0;}return *this;}// 其他成员函数...
};

移动操作应该:

  1. 标记为noexcept(提高标准库容器性能)
  2. 检查自赋值(虽然移动自身很少见)
  3. 确保被移动对象保持在有效但可预测的状态

实际应用示例

避免不必要的深拷贝

#include <iostream>
#include <vector>
#include <string>
#include <chrono>// 测量函数执行时间的辅助函数
template <typename Func>
long long measureTime(Func func) {auto start = std::chrono::high_resolution_clock::now();func();auto end = std::chrono::high_resolution_clock::now();return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}int main() {// 准备一个大字符串std::string largeString(1000000, 'x');// 使用拷贝long long copyTime = measureTime([&largeString]() {std::vector<std::string> vec;for (int i = 0; i < 100; ++i) {vec.push_back(largeString); // 创建largeString的副本}});// 使用移动long long moveTime = measureTime([&largeString]() {std::vector<std::string> vec;for (int i = 0; i < 100; ++i) {std::string temp = largeString; // 先创建副本vec.push_back(std::move(temp)); // 移动而非复制}});std::cout << "Copy time: " << copyTime << " microseconds" << std::endl;std::cout << "Move time: " << moveTime << " microseconds" << std::endl;std::cout << "Performance improvement: " << (copyTime - moveTime) * 100.0 / copyTime << "%" << std::endl;return 0;
}

实现高效的swap

通过移动语义,可以实现零拷贝的swap操作:

template<typename T>
void swap(T& a, T& b) {T temp = std::move(a);  // 移动而非复制a = std::move(b);       // 移动而非复制b = std::move(temp);    // 移动而非复制
}

高效实现类的移动语义

下面是一个完整的示例,展示如何为一个管理动态资源的类实现移动语义:

#include <iostream>
#include <utility>  // 为std::moveclass DynamicArray {
private:int* data;size_t size;public:// 构造函数DynamicArray(size_t size) : size(size), data(new int[size]) {std::cout << "Constructor called. Size: " << size << std::endl;for (size_t i = 0; i < size; ++i) {data[i] = 0;}}// 析构函数~DynamicArray() {std::cout << "Destructor called. Data: " << data << std::endl;delete[] data;}// 拷贝构造函数 - 深拷贝DynamicArray(const DynamicArray& other) : size(other.size), data(new int[other.size]) {std::cout << "Copy constructor called" << std::endl;for (size_t i = 0; i < size; ++i) {data[i] = other.data[i];}}// 拷贝赋值运算符 - 深拷贝DynamicArray& operator=(const DynamicArray& other) {std::cout << "Copy assignment operator called" << std::endl;if (this != &other) {delete[] data;size = other.size;data = new int[size];for (size_t i = 0; i < size; ++i) {data[i] = other.data[i];}}return *this;}// 移动构造函数DynamicArray(DynamicArray&& other) noexcept : data(other.data), size(other.size) {std::cout << "Move constructor called" << std::endl;other.data = nullptr;other.size = 0;}// 移动赋值运算符DynamicArray& operator=(DynamicArray&& other) noexcept {std::cout << "Move assignment operator called" << std::endl;if (this != &other) {delete[] data;data = other.data;size = other.size;other.data = nullptr;other.size = 0;}return *this;}// 辅助方法size_t getSize() const { return size; }void setValue(size_t index, int value) {if (index < size) {data[index] = value;}}int getValue(size_t index) const {if (index < size) {return data[index];}return -1;}// 打印数组内容void print() const {std::cout << "Array at " << data << " with size " << size << ": ";for (size_t i = 0; i < size && i < 5; ++i) {std::cout << data[i] << " ";}if (size > 5) std::cout << "...";std::cout << std::endl;}
};// 返回一个临时DynamicArray对象
DynamicArray createArray(size_t size) {DynamicArray arr(size);for (size_t i = 0; i < size; ++i) {arr.setValue(i, i * 10);}return arr;  // 返回时会发生移动,而非拷贝
}int main() {std::cout << "=== Testing move semantics ===" << std::endl;std::cout << "\n1. Basic constructor:" << std::endl;DynamicArray arr1(5);arr1.print();std::cout << "\n2. Copy constructor:" << std::endl;DynamicArray arr2 = arr1;  // 调用拷贝构造函数arr2.print();std::cout << "\n3. Move constructor with temporary:" << std::endl;DynamicArray arr3 = createArray(3);  // 使用函数返回的临时对象arr3.print();std::cout << "\n4. Move constructor with std::move:" << std::endl;DynamicArray arr4 = std::move(arr1);  // 显式移动arr4.print();// arr1现在处于"有效但未指定"的状态,其数据成员被移走了std::cout << "arr1 after move: ";arr1.print();  // 应该显示空或默认值std::cout << "\n5. Move assignment:" << std::endl;DynamicArray arr5(2);arr5 = std::move(arr2);  // 移动赋值arr5.print();// arr2现在处于"有效但未指定"的状态std::cout << "arr2 after move: ";arr2.print();std::cout << "\n=== End of scope, destructors will be called ===" << std::endl;return 0;
}

常见陷阱与最佳实践

移动语义的陷阱

  1. 使用移动后的对象

    std::string s1 = "Hello";
    std::string s2 = std::move(s1);
    std::cout << s1 << std::endl;  // 危险:使用已移动的对象
    
  2. 在不适当的场景使用std::move

    // 不要在返回局部变量时使用std::move
    std::string badFunction() {std::string result = "value";return std::move(result);  // 反而阻止了RVO优化!
    }// 正确写法
    std::string goodFunction() {std::string result = "value";return result;  // 编译器会自动应用RVO/NRVO
    }
    
  3. 在条件表达式中使用std::move

    std::string s = condition ? std::move(a) : std::move(b);
    // 注意:无论选择哪个分支,a和b都会被std::move转换为右值!
    

最佳实践

  1. 总是标记移动操作为noexcept

    MyClass(MyClass&& other) noexcept;
    MyClass& operator=(MyClass&& other) noexcept;
    
  2. 确保移动后的对象处于有效状态

    // 在移动操作后
    other.data = nullptr;  // 防止原对象的析构函数释放内存
    other.size = 0;        // 将对象重置为空
    
  3. 实现"大五"法则
    如果定义了任何一个拷贝构造、拷贝赋值、移动构造、移动赋值或析构函数,就应该考虑定义所有五个。

  4. 考虑显式禁用不需要的操作

    class OnlyMovable {
    public:OnlyMovable(OnlyMovable&&) = default;OnlyMovable& operator=(OnlyMovable&&) = default;// 禁用拷贝OnlyMovable(const OnlyMovable&) = delete;OnlyMovable& operator=(const OnlyMovable&) = delete;
    };
    
  5. 使用RAII和智能指针简化资源管理

    class ModernResource {
    private:std::unique_ptr<int[]> data;size_t size;public:// 使用unique_ptr自动处理移动语义ModernResource(size_t s) : data(std::make_unique<int[]>(s)), size(s) {}// 移动构造和赋值由编译器自动生成且正确处理
    };
    

性能考量

移动语义的性能优势在处理大型对象时尤为明显。考虑以下情况:

// 假设每个字符串大小为1MB
std::vector<std::string> createAndFill(size_t n) {std::vector<std::string> result;std::string largeString(1024*1024, 'x');for (size_t i = 0; i < n; ++i) {// 在C++11前:这里会导致深拷贝// 在C++11后:push_back可以使用移动语义result.push_back(largeString);}return result;  // 返回值优化 + 移动语义
}

在这个例子中,如果没有移动语义,每次push_back都会创建一个1MB字符串的完整副本。而有了移动语义,我们可以避免大部分的内存分配和复制操作。

总结

右值引用和移动语义是现代C++中最重要的优化技术之一,它们通过减少不必要的对象复制,大幅提高了程序的性能,特别是在处理大型数据结构时。主要优势包括:

  1. 提高性能:通过"窃取"资源而不是复制,减少内存分配和数据复制
  2. 更高效的标准库:标准容器和算法通过移动语义获得显著性能提升
  3. 表达能力增强:能够明确区分对象的"移动"和"复制"语义

要充分利用右值引用和移动语义,建议:

  • 为管理资源的类实现移动操作
  • 理解并正确使用std::movestd::forward
  • 遵循移动语义的最佳实践
  • 使用智能指针和标准库容器自动受益于移动语义

在下一篇文章中,我们将探讨C++11/14中另一个重要特性:lambda表达式,它如何简化函数对象的创建和使用。


这是我C++学习之旅系列的第三十九篇技术文章。查看完整系列目录了解更多内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79470.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Qlearning强化学习的电梯群控系统高效调度策略matlab仿真

目录 1.算法仿真效果 2.算法涉及理论知识概要 2.1 Q-learning强化学习原理 2.2 基于Q-learning的电梯群控系统建模 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下&#xff08;完整代码运行后无水印&#xff09;&#xff1a; 仿真操作…

31.软件时序控制方式抗干扰

软件时序控制方式扛干扰 1. 软件时序控制抗干扰的时间逻辑2. 应用案例 1. 软件时序控制抗干扰的时间逻辑 &#xff08;1&#xff09;将受软件控制的功能或软件检测到的状态一一罗列&#xff1b; &#xff08;2&#xff09;将其中的潜在干扰和敏感信号分开&#xff1b; &#x…

Ubuntu环境下使用uWSGI服务器【以flask应用部署为例】

0、前置内容说明 首先要知道WSGI是什么&#xff0c;关于WSGI服务器的介绍看这篇&#xff1a;WSGI&#xff08;Web Server Gateway Interface&#xff09;服务器 由于从Python 3.11开始限制了在系统级 Python 环境中使用 pip 安装第三方包&#xff0c;以避免与系统包管理器&am…

d3_v7绘制折线图

<!DOCTYPE html> <html><head><meta charsetutf-8><title>需求</title><script src"https://d3js.org/d3.v7.min.js"></script><style>* {margin: 0;padding: 0;}html, body {width: 100%;height: 100%;displ…

Hotspot分析(1):单细胞转录组识别信息基因(和基因模块)

这一期我们介绍一个常见的&#xff0c;高分文章引用很高的一个单细胞转录组分析工具Hotspot&#xff0c;它可针对单细胞转录组数据识别有意义基因或者基因module&#xff0c;类似于聚类模块。所谓的”informative "的基因是那些在给定度量中相邻的细胞之间以相似的方式表达…

爬虫准备前工作

1.Pycham的下载 网址&#xff1a;PyCharm: The only Python IDE you need 2.Python的下载 网址&#xff1a;python.org&#xff08;python3.9版本之后都可以&#xff09; 3.node.js的下载 网址&#xff1a;Node.js — 在任何地方运行 JavaScript&#xff08;版本使用18就可…

基于Springboot旅游网站系统【附源码】

基于Springboot旅游网站系统 效果如下&#xff1a; 系统登陆页面 系统主页面 景点信息推荐页面 路线详情页面 景点详情页面 确认下单页面 景点信息管理页面 旅游路线管理页面 研究背景 随着互联网技术普及与在线旅游消费习惯的深化&#xff0c;传统旅游服务模式面临效率低、…

利用KMP找出模式串在目标串中所有匹配位置的起始下标

问题关键&#xff1a;完成首次匹配之后需要继续进行模式匹配。 到这一步后&#xff0c;我们不能直接将j 0然后开始下一轮匹配&#xff0c;因为已经匹配过的部分&#xff08;蓝色部分&#xff09;中仍然可能存在与模式串重叠的子串&#xff1a; 解决办法&#xff1a; 找到蓝…

RR(Repeatable Read)级别如何防止幻读

在 MySQL 数据库事务隔离级别中&#xff0c;RR&#xff08;可重复读&#xff09; 通过 MVCC&#xff08;多版本并发控制&#xff09; 和 锁机制 的组合策略来避免幻读问题。 一、MVCC机制&#xff1a;快照读与版本控制 快照读&#xff08;Snapshot Read&#xff09; 每个事务启…

Android运行时ART加载类和方法的过程分析

目录 一,概述 二,ART运行时的入口 一,概述 既然ART运行时执行的都是翻译DEX字节码后得到的本地机器指令了&#xff0c;为什么还需要在OAT文件中包含DEX文件&#xff0c;并且将它加载到内存去呢&#xff1f;这是因为ART运行时提供了Java虚拟机接口&#xff0c;而要实现Java虚…

Javase 基础加强 —— 02 泛型

本系列为笔者学习Javase的课堂笔记&#xff0c;视频资源为B站黑马程序员出品的《黑马程序员JavaAI智能辅助编程全套视频教程&#xff0c;java零基础入门到大牛一套通关》&#xff0c;章节分布参考视频教程&#xff0c;为同样学习Javase系列课程的同学们提供参考。 01 认识泛型…

Oracle VirtualBox 在 macOS 上的详细安装步骤

Oracle VirtualBox 在 macOS 上的详细安装步骤 一、准备工作1. 系统要求2. 下载安装包二、安装 VirtualBox1. 挂载安装镜像2. 运行安装程序3. 处理安全限制(仅限首次安装)三、安装扩展包(增强功能)四、配置第一个虚拟机1. 创建新虚拟机2. 分配内存3. 创建虚拟硬盘4. 加载系…

RAGFlow 接入企业微信应用实现原理剖析与最佳实践

背景 近期有医美行业客户咨询我们智能客服产品&#xff0c;期望将自己企业的产品、服务以及报价信息以企微应用的方式给到客户进行体验互动&#xff0c;提升企业运营效率。关于企业微信对接&#xff0c;我们分享下最佳实践&#xff0c;抛砖引玉。效果图如下&#xff1a; 这里也…

【心海资源】子比主题新增注册与会员用户展示功能模块及实现方法

内容改写&#xff1a; 本次分享的是子比主题顶部展示注册用户与会员信息的功能模块及其实现方式。 你可以通过两种方式启用该功能&#xff1a; 直接在后台进入“外观 → 小工具”启用该展示模块&#xff0c;操作简便&#xff1b;也可将提供的代码覆盖至子比主题目录中&#…

CSDN积分详解(介绍、获取、用途)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 积分**一、积分类型及用途****二、积分获取途…

【iview】es6变量结构赋值(对象赋值)

变量的解构赋值 以iview的src/index.js中Vue.prototype.$IVIEW改造为例练习下怎么使用变量的解构赋值 原来的写法&#xff1a; const install function(Vue, opts {}) {if (install.installed) return;locale.use(opts.locale);locale.i18n(opts.i18n);Object.keys(iview).fo…

【c++深入系列】:万字详解vector(附模拟实现的vector源码)

&#x1f525; 本文专栏&#xff1a;c &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 种子破土时从不问‘会不会有光’&#xff0c;它只管生长 ★★★ 本文前置知识&#xff1a; 模版 1.什么是vector 那么想必大家都学过顺…

MySQL基础关键_007_DQL 练习

目 录 一、题目 二、答案&#xff08;不唯一&#xff09; 1.查询每个部门薪资最高的员工信息 2.查询每个部门高于平均薪水的员工信息 3. 查询每个部门平均薪资等级 4.查询部门中所有员工薪资等级的平均等级 5.不用分组函数 max 查询最高薪资 6.查询平均薪资最高的部门编…

Jenkis安装、配置及账号权限分配保姆级教程

Jenkis安装、配置及账号权限分配保姆级教程 安装Jenkins下载Jenkins启动Jenkins配置Jenkins入门Jenkins配置配置中文配置前端自动化任务流新建任务拉取代码打包上传云服务并运行配置后端自动化任务流新建任务拉取代码打包上传云服务并运行账号权限分配创建用户分配视图权限安装…

虚函数 vs 纯虚函数 vs 静态函数(C++)

&#x1f9e9; 一图看懂&#xff1a;虚函数 vs 纯虚函数 特性虚函数&#xff08;Virtual&#xff09;纯虚函数&#xff08;Pure Virtual&#xff09;语法virtual void foo();virtual void foo() 0;是否必须实现✅ 必须在类中实现❌ 不在基类实现&#xff0c;派生类必须实现是…