【C++练级之路】【Lv.22】C++11——右值引用和移动语义



快乐的流畅:个人主页


个人专栏:《算法神殿》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

  • 引言
  • 一、右值引用
    • 1.1 左值和右值
    • 1.2 左值引用和右值引用的范围
    • 1.3 左值引用的意义
  • 二、移动语义
    • 2.1 移动构造
    • 2.2 移动赋值
    • 2.3 右值引用的意义
    • 2.4 move
    • 2.5 移动插入
  • 三、完美转发
    • 3.1 万能引用
    • 3.2 forward
  • 四、新增默认成员函数
    • 4.1 移动构造函数
    • 4.2 移动赋值重载
    • 4.3 default
    • 4.4 delete

引言

关于C++11的final和override的知识,在之前已经提到过,这里不再赘述,有需要的请移步这篇博客【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)

一、右值引用

1.1 左值和右值

  • 左值:可取地址,可在等号左右
  • 右值:不可取地址,只能在等号右边
void test()
{int a;//左值10;//右值10 + 20//右值
}

一般情况下,左值均为变量名,而右值则为字面常量、表达式等。

1.2 左值引用和右值引用的范围

void test()
{int& ref1 = a;//左值引用,可以引用左值//int& ref2 = a + b;//左值引用,不能引用右值(权限放大)const int& ref2 = a + b;//const左值引用,可以引用右值int&& ref3 = a + b;//右值引用,可以引用右值//int&& ref4 = a;//右值引用,不能引用左值int&& ref4 = move(a);//右值引用,可以引用move后的左值
}
  • 左值引用,可以引用左值
  • const左值引用,可以引用右值
  • 右值引用,可以引用右值
  • 右值引用,可以引用move后的左值

ps:move的作用,是将左值强制转换为右值引用,详情见move章节。
ps:右值的引用属性为左值,将右值引用后,右值会被存储起来,并可以取到地址。

1.3 左值引用的意义

左值引用:

  1. 传引用传参,减少拷贝
  2. 传引用返回,减少拷贝(限制:函数内的局部对象,不能传引用返回)

左值引用已经解决了绝大多数拷贝问题,但是唯一的缺陷就是不能传引用返回局部对象。所以,这就是右值引用存在的意义,为了补全这块不足。

而要完全理解右值引用的意义,则需要学习移动语义,理解右值引用是如何减少拷贝的。

二、移动语义

首先,给出一个自己实现的精简版string类,方便调试和观察内部细节。

namespace my
{class string{public:typedef char* iterator;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){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}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+(char ch){string tmp = *this;tmp += ch;return tmp;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}

operator+是我们要重点观察的函数,特意拿出来方便对比:

string operator+(char ch)
{string tmp = *this;tmp += ch;return tmp;
}

2.1 移动构造

先来看看以下代码:

void test()
{my::string s1 = "hello";my::string s2 = s1 + '!';
}

在以往的经验中,operator+中的tmp(原始对象)传值返回,先拷贝构造给临时对象,tmp在函数域内销毁,然后临时对象再拷贝构造给s2(目标对象),总共有两次深拷贝。

但是,如果运用上右值引用的移动构造,加上以下代码:

// 移动构造
string(string&& s): _str(nullptr)
{cout << "string(string&& s) -- 移动" << endl;swap(s);
}

此时,operator+中的tmp(原始对象)传值返回,直接和s2互换资源(称之为移动),就可以直接无拷贝返回,直接减少了两次深拷贝。


ps:如果符合编译器优化,编译器会自动将tmp识别为左值,从而将连续三次拷贝构造优化成一次。(VS2022)
ps:如果不符合优化,编译器才会将tmp强制识别为右值,从而符合移动语义。
ps:如果只有const&,右值会匹配;如果有&&,右值则会匹配更适合的。


2.2 移动赋值

同理,再看看这段代码:

void test()
{my::string s1 = "hello";my::string s2;s2 = s1 + '!';
}

在以往的经验中,operator+中的tmp(原始对象)传值返回,先拷贝构造给临时对象,tmp在函数域内销毁,然后临时对象再赋值给s2(目标对象),总共有两次深拷贝。

但是,如果运用上右值引用的移动赋值,加上以下代码:

// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动" << endl;swap(s);return *this;
}

此时,operator+中的tmp(原始对象)传值返回,直接和s2互换资源(称之为移动),就可以直接无拷贝返回,直接减少了两次深拷贝。

ps:此时不是连续的拷贝构造,而是拷贝构造+赋值,所以编译器不会优化。

2.3 右值引用的意义

对于函数内的右值,分为两类:

  • 纯右值:内置类型的右值
  • 将亡值:自定义类型的右值

右值引用:把将亡值的资源直接移动,从而减少两次深拷贝

拷贝
拷贝
移动
原始对象
临时对象
目标对象

2.4 move

move 是一个模板函数,它接受一个左值引用,并返回一个右值引用。这允许我们指示编译器,我们可以安全地“移动”这个左值的资源,而不是复制它们。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{// forward _Arg as movablereturn ((typename remove_reference<_Ty>::type&&)_Arg);
}

ps:move 只是一个“建议”或“请求”,而不是强制。它告诉编译器:“这个对象我不再需要了,你可以安全地将其资源移动给另一个对象。”

但是,如果对象的类型没有定义移动构造函数或移动赋值运算符,或者这些函数被标记为 delete,那么编译器仍然会进行复制操作。


同时,使用move一定要慎重,如果要进行资源移动,要确保move的左值不会再使用。

void test()
{string s1 = "hello";string s2 = move(s1);
}

以上代码中,move后的s1被识别为右值,调用右值引用的移动构造,将s1的资源移动到s2,而s1本身就被置空了。

2.5 移动插入

C++11更新后,STL中所有容器都新增了移动版本的插入函数。那么,它与原先的插入函数有什么不同呢?


先来看看以下代码:

void test()
{vector<string> v;v.push_back("1111");
}

C++98:void push_back (const T& val);
先利用右值构造string,再拷贝构造插入vector。

C++11:void push_back (T&& val);
先利用右值构造string,再移动插入vector。

综上比较,移动插入相较于传统插入,减少了一次深拷贝,效率得到了提高。

ps:const& 延长右值生命周期(C++98)

三、完美转发

3.1 万能引用

template<typename T>
void PerfectForward(T&& t)//万能引用(引用折叠)
{}

函数模板参数中的T&&,不再代表右值引用,而是代表万能引用(又称引用折叠)。它能以统一的方式处理左值和右值,既能接收左值引用,也能接收右值引用

  • t为右值时,保持为T&&
  • t为左值时,折叠为T&

3.2 forward

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; }template<typename T>
void PerfectForward(T&& t)//万能引用(引用折叠)
{Fun(t);
}void test()
{PerfectForward(10);//右值int a;PerfectForward(a);//左值PerfectForward(move(a));//右值const int b = 8;PerfectForward(b);//const 左值PerfectForward(move(b));//const 右值
}

前面已经提到,右值的引用属性为左值(只有这样设计才能实现移动语义),那么在上述代码中,调用Fun函数就全部是左值引用,无法达到区分左值和右值的效果。

那么,如何在传递中保持参数的属性呢?这时就要用到完美转发!


forward 是一个模板函数,如果接收左值引用,则返回左值引用,如果接收右值引用,则返回右值引用

template<typename T>
void PerfectForward(T&& t)//万能引用(引用折叠)
{Fun(forward<T>(t));//完美转发
}

完美转发允许函数模板将其参数以原始值类别(左值或右值)转发给另一个函数。这通常用于包装或委托函数。

四、新增默认成员函数

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}//Person(const Person& p)//	:_name(p._name)//	,_age(p._age)//{}//Person& operator=(const Person& p)//{//	if(this != &p)//	{//		_name = p._name;//		_age = p._age;//	}//	return *this;//}//~Person()//{}
private:my::string _name;int _age;
};

4.1 移动构造函数

若未显式定义,且未显式定义拷贝构造、拷贝赋值、析构,编译器才会自动生成默认的移动构造函数。对内置类型值拷贝,对于自定义类型调用其移动构造函数(若未显式定义,则调用其拷贝构造)

4.2 移动赋值重载

若未显式定义,且未显式定义拷贝构造、拷贝赋值、析构,编译器才会自动生成默认的移动赋值重载。对内置类型值拷贝,对于自定义类型调用其移动赋值重载(若未显式定义,则调用其拷贝赋值重载)

void test()
{Person s1;Person s2 = s1;Person s3 = move(s1);//移动构造Person s4;s4 = move(s2);//移动赋值
}

4.3 default

强制生成默认成员函数

Person(Person&& p) = default;//强制生成默认移动构造
Person& operator=(Person&& p) = default;//强制生成默认移动赋值

4.4 delete

禁止生成默认成员函数

Person(const Person& p) = delete;//禁止生成默认拷贝构造
Person& operator=(const Person& p) = delete;//禁止生成默认拷贝赋值

ps:C++98中,将函数设置为private,以此达到禁止生成默认成员函数的目的。


真诚点赞,手有余香

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

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

相关文章

AI大模型探索之路-实战篇9:探究Agent智能数据分析平台的架构与功能

系列篇章&#x1f4a5; AI大模型探索之路-实战篇4&#xff1a;深入DB-GPT数据应用开发框架调研 AI大模型探索之路-实战篇5&#xff1a;探索Open Interpreter开放代码解释器调研 AI大模型探索之路-实战篇6&#xff1a;掌握Function Calling的详细流程 AI大模型探索之路-实战篇7…

中断处理过程介绍

概念 中断 中断源 分类 中断处理过程 中断请求 实现器件 中断判优 软件判优 过程 器件实现 程序实现 硬件判优 链路判优 器件实现 控制器判优 中断响应 中断服务 中断返回

STM32H750外设之ADC通道选择

目录 概述 1 通道选择功能介绍 2 通道选择&#xff08; SQRx、 JSQRx&#xff09; 2.1 通道复用 2.1.1 通道介绍 2.1.2 通道框图 2.2 转换分组 2.3 内部专用通道 3 通道预选寄存器 (ADCx_PCSEL) 3.1 功能介绍 3.2 预选通道寄存器 概述 本位主要介绍STM32H750外设之…

栈 队列

目录 1.1栈的基本概念 1.1.1栈的定义 1.1.2栈的基本操作 1.2栈的顺序存储结构 1.2.1构造原理 1.2.2基本算法 1.3栈的链式存储结构 1.3.1构造原理 1.3.2基本算法 2.1队列的基本概念 2.1.1队列的定义 2.1.2队列的基本运算 2.2队列的顺序存储结构 2.2.1构造原理 2.2.1基…

CRLF注入漏洞

1.CRLF注入漏洞原理 Nginx会将 $uri进行解码&#xff0c;导致传入%0a%0d即可引入换行符&#xff0c;造成CRLF注入漏洞。 执行xss语句 2.漏洞扩展 CRLF 指的是回车符(CR&#xff0c;ASCII 13&#xff0c;\r&#xff0c;%0d) 和换行符(LF&#xff0c;ASCII 10&#xff0c;\n&am…

FTP协议——LightFTP安装(Linux)

1、简介 LightFTP是一个轻量级的FTP&#xff08;File Transfer Protocol&#xff0c;文件传输协议&#xff09;客户端软件。FTP是一种用于在网络上传输文件的标准协议&#xff0c;允许用户通过TCP/IP网络&#xff08;如互联网&#xff09;在计算机之间进行文件传输。 2、步骤…

在ARM开发板上,栈大小设置为2MB(常用设置)里面存放的数据

系列文章目录 在ARM开发板上&#xff0c;栈大小设置为2MB&#xff08;常用设置&#xff09;里面存放的数据 在ARM开发板上&#xff0c;栈大小设置为2MB&#xff08;常用设置&#xff09;里面存放的数据 系列文章目录 在ARM开发板上&#xff0c;栈&#xff08;Stack&#xff09;…

Thingsboard规则链:Message Type Filter节点详解

一、Message Type Filter节点概述 二、具体作用 三、使用教程 四、源码浅析 五、应用场景与案例 智能家居自动化 工业设备监控 智慧城市基础设施管理 六、结语 在物联网&#xff08;IoT&#xff09;领域&#xff0c;数据处理与自动化流程的实现是构建智能系统的关键。作…

创新实训2024.05.28日志:记忆化机制、基于MTPE与CoT技术的混合LLM对话机制

1. 带有记忆的会话 1.1. 查询会话历史记录 在利用大模型自身能力进行对话与解答时&#xff0c;最好对用户当前会话的历史记录进行还原&#xff0c;大模型能够更好地联系上下文进行解答。 在langchain chat chat的chat函数中&#xff0c;通过实现langchain框架提供的ChatMemo…

【设计模式】创建型-工厂方法模式

前言 工厂方法模式是一种经典的创建型设计模式&#xff0c;它提供了一种灵活的方式来创建对象实例。通过本文&#xff0c;我们将深入探讨工厂方法模式的概念、结构和应用。 一、什么是工厂方法模式 工厂方法模式是一种创建型设计模式&#xff0c;旨在解决对象的创建过程和客…

Parquet使用指南:一个超越CSV、提升数据处理效率的存储格式

前言 在大数据时代&#xff0c;数据存储和处理的效率越来越重要。同时&#xff0c;我们在工作中处理的数据也越来越多&#xff0c;从excel格式到csv格式&#xff0c;从文件文档传输到直接从数据库提取&#xff0c;数据单位也从K到M再到G。 当数据量达到了G以上&#xff0c;几…

ROS | 自动导航

保存&加载地图&#xff1a; image:地图文件 resolution:地图分辨率&#xff08;珊格地图&#xff09; origin&#xff1a;地图左下标 第三个参数是偏转角度 加载创建好的yaml文件&#xff1a; 年轻人第一次导航&#xff1a; 全局规划器&#xff1a; 代价地图设置参数&#…

K-means聚类模型入门介绍

K-means聚类是一种无监督学习方法&#xff0c;广泛应用于数据挖掘、机器学习和模式识别等领域&#xff0c;用于将数据集划分为K个簇&#xff08;cluster&#xff09;&#xff0c;其中每个簇的数据具有相似的特征。其基本思想是通过迭代寻找使簇内点间距离平方和最小的簇划分方式…

K8S-pod资源 探针

一.pod资源限制&#xff1a; 对pod资源限制原因&#xff1a;高并发占用所有的cpu资源、内存资源、会造成雪崩 当定义 Pod 时可以选择性地为每个容器设定所需要的资源数量。 最常见的可设定资源是 CPU 和内存大小&#xff0c;以及其他类型的资源。 方式&#xff1a; 对pod做…

UML-系统架构师(二)

1、UML&#xff08;Unified Modeling Language&#xff09;是面向对象设计的建设工具&#xff0c;独立于任何具体程序设计语言&#xff0c;以下&#xff08;&#xff09;不属于UML中的模型。 A用例图 B协作图 C活动图 DPAD图 解析&#xff1a; UML一共14种图 结构图&…

【蓝桥杯——物联网设计与开发】拓展模块2 - 电位器模块

一、电位器模块 &#xff08;1&#xff09;资源介绍 &#x1f505;原理图 蓝桥杯物联网竞赛实训平台提供了一个拓展接口 CN2&#xff0c;所有拓展模块均可直接安装在 Lora 终端上使用&#xff1b; 图1 拓展接口 电位器模块电路原理图如下所示&#xff1a; 图2 …

python数据分析——apply 2

参考资料&#xff1a;活用pandas库 1、向量化函数 使用apply时&#xff0c;可以按行或按列应用函数。如果想应用自定义的函数&#xff0c;必须重写它&#xff0c;因为整列或整行传递到了函数的第一个参数中。可以利用向量化函数和装饰器对所有函数进行向量化。对代码进行向量化…

再论任何图≌自己这一几何最最起码常识推翻平面公理

黄小宁 有了解析几何使人类对直线和射线的认识有革命性的飞跃。几何学有史2300年来一直认定起点和射出的方向都相同的射线必重合&#xff0c;任两异射线必有全等关系&#xff1b;解析几何使我发现这是2300年肉眼直观错觉。 h定理&#xff08;参考文献中的定理&#xff09;&am…

台式机安装ubuntu过程

1.单系统参考 20231210-超详细Ubuntu20.04单系统安装_台式机安装ubuntu系统-CSDN博客 2.双系统参考 双系统启动效果_哔哩哔哩_bilibili 安装前一定要先清空电脑的硬盘数据&#xff0c;不然可能会出现以下图片异常 意思估计是分区被占用了&#xff0c;出现这个问题 &#xff0…

安全基础二

一、插件漏洞 统计使用了哪些插件这些插件有版本更新嘛检测这些插件是否存在已知漏洞 二、权限提升和持久化 SSRF&#xff08;Server-Side Request Forgery&#xff0c;服务器端请求伪造&#xff09; 想象一下&#xff0c;你是一个公司的内部员工&#xff08;服务器&#x…