【C++11新特性】右值引用和移动语义(移动构造,移动赋值)

news2025/7/14 6:15:14

文章目录

  • 前言
  • 一、列表初始化(不同于初始化列表)
  • 二、initializer_list
  • 三、decltype关键字
  • 四、nullptr
  • 五、右值引用
    • 移动拷贝和移动赋值
      • 被编译器识别成将亡值的原因
    • 左值引用和右值引用的场景和价值
    • 右值引用的场景
    • move函数
  • 六、关于右值引用的功能和属性问题(完美转发)
  • 总结


前言


一、列表初始化(不同于初始化列表)

列表初始化是C++11的一个新特性,不同于初始化列表。

列表初始化在对自定义类型时,会调用它的构造函数。


struct Point
{
	int _x;
	int _y;
};

int main()
{
	int x1 = 1;
	int x2{ 2 };
	int array1[]{ 1, 2, 3, 4, 5 };
	int array2[5]{ 0 };
	Point p{ 1, 2 };
	// C++11中列表初始化也可以适用于new表达式中
	int* pa = new int[4]{ 0 };
	return 0;
}

以前学习到的一般只有单个参数的构造函数支持隐式类型转换,现在的列表初始化实际上支持了多参数的构造函数支持隐式类型转换。
在上面的例子中就是{1,2}这个初始化列表会走多参数的构造,构造出一个Point临时对象,再进行拷贝构造,只不过这两步被编译器优化成直接构造。
在构造函数前加上explicit关键字就能禁止编译器优化了。

自定义类型本质上还是调用构造函数。

所以还能这样玩:

const Point& p1 = {1,2};
这也是多参数构造支持隐式类型转换,构造的临时对象具有常性,用const引用才支持。

二、initializer_list

在这里插入图片描述
initializer_list的基本情况如上,用法如下:

auto il = {1,2,3,4,5};

il就是一个intializer_list类型,等号右边是一个常量数组空间,存在常量区,intializer_list的大致结构如下:

template<class T>
class intializer_list
{
    const T* _start;
    const T* _start;
};

_start指向等号右边的常量数组空间的起始地址
_finush指向等号右边的常量空间的末尾元素地址的下一个

在这里插入图片描述

所以

auto il = {1,2,3,4,5};

的本质上还是调用il的构造函数。

有了initializer_list之后,各种容器就支持了用initializer_list来构造了。
比如说:

vector<int> v1 = {1,2,3,4,5};

在这里插入图片描述

本质上是先用等号右边的常量数组构造initializer_list,然后vector重载了一个用initializer_list来构造vector的函数,再调该函数完成从initializer_list到vector的构造。

还有一个容器:map

map<string,string> m1 = {{"sort","排序"},{"left","左边"}};

本质上右边的常量数组会被识别成pair,所以先调用pair的构造函数,生成一个pair<string,string>对象,然后再调用initializer_list<pair<string,string>>完成从pair到initializer_list的构造,最后,map支持了用initializer_list构造,所以最后构造出了map。
在这里插入图片描述

常量数组空间—>pair–>initializer_list < pair > -->map

三、decltype关键字

decltype是一个关键字,能推出变量的类型,再定义变量
它还可以作模板参数

int a;
//decltype推导对象类型,再定义变量
//还可以作模板参数
decltype(a) a1;
cout << typeid(a1).name() << endl;
vector<decltype(a)> v;

四、nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。

所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

#ifndef NULL
#ifdef __cplusplus
#define NULL  0
#else
#define NULL  ((void *)0)
#endif
#endif

结论:nullptr的本质就是 (void*)0


五、右值引用

要知道什么是右值,先知道什么是左值。
之前学的引用中,一般都是左值引用,无论左值引用还是右值引用,都是给对象取别名。

简单区分一下左值和右值的区别:

左值可以获取地址,右值不能获取地址

下面详细讲解:

1、下面的是左值,对左值的引用

int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
const int * ps = "hello c++";//字符串能取地址,也是左值,只不过是不能修改而已
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
//左值可以出现在赋值符号的右边
return 0;
}

特点:

  • 1)左值可以修改,但是const修饰的左值就不能修改了,左值可以被引用
  • 2)左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。

2.下面几个都是右值

10
x+y
fmin(a,b)

特点:

  • 1)右值不能被修改
  • 2)右值不能取地址
  • 3)右值不能出现在赋值符号左边

结论: 左值引用是给左值取别名(&),右值引用是给右值取别名。(&&)

int main()
{
	//1.右值引用,给右值取别名
	int&& a = 10;
	double x = 1.1, y = 2.2;
	double&& rx = x + y;  //虽然x,y单独都是左值,但是x+y表达式的返回值是一个右值。

	//2.左值引用,给左值取别名
	int b = 2;
	int& rb = b;

	//3.左值引用既能给左值取别名,也能给右值取别名
	const int& c = 10; //const引用能给右值取别名
	
	//4.右值引用不能给左值取别名,除非move()一下
	int&& r1 = b; //false
	int&& r1 = move(b); //true


	return 0;
}

还有两个点:

  • 1.左值引用既能给左值取别名,也能给右值取别名
  • 2.右值引用不能给左值取别名,除非move()一下

至于什么是move(),下面会讲

移动拷贝和移动赋值

在语法上,对于内置类型来说,把内置类型的右值称为纯右值
把自定义类型的右值称为将亡值。

看下面的例子:

dzt::string fun()
{
	dzt::string str("xxxxxxxxxxxxxxxxxxxxxx");

	return str;
}

int main()
{
	dzt::string ret = fun();//连续的两次拷贝构造会被编译器优化 成一次深拷贝
	//...
	return 0;
}

解析:

首先调用fun函数,对str进行一次构造,在返回str时,由于返回值类型是传值返回,所以str先构造一个临时对象,然后fun函数结束后,str销毁了,临时对象再进行拷贝构造给ret,总的来说就是两次深拷贝。

(不过现在编译器会对连续的构造/拷贝构造进行优化,优化成一次构造/拷贝构造)

在这里插入图片描述

因为这样的情况下,如果返回string&,会造成更大的问题,在fun函数调用结束时,就已经将str的资源释放了,然而ret还是str的别名,一旦访问就会出现非法访问内存问题。

有了移动构造和移动赋值的出现,就能完美解决上面两次深拷贝造成效率下降的问题。

在编译器看来,因为str虽然是一个左值,但是str构造的临时对象后再返回给ret,是一个传值返回,且str构造临时对象后自己就会销毁,不应该多此一举,而是直接将str自己的资源直接给ret。因为str会被编译器识别成将亡值

所以,有了移动构造,资源转移情况是这样的:
在这里插入图片描述

具体的实现:

以string为例

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

string& operator=(string&& s)
{
	cout << "string operator=(const string&& s)-- 移动赋值" << endl;
	swap(s); //现代写法,顺便在你要释放的时候把我不要的资源带走

	return *this;
	//返回的是s的引用,此时s已经拿到了想要的资源,且不要的资源在出了该作用域后会被将亡值释放
}

被编译器识别成将亡值的原因

这里str会被识别成将亡值的两个前提:

  • 1.连续的构造/拷贝构造,编译器会直接优化成一次构造/拷贝构造
  • 2.函数传值返回

所以,本质上,正是因为有编译器的优化,才会减少拷贝次数,大大提高效率。


左值引用和右值引用的场景和价值

使用场景:
1.做参数和做返回值都可以提高效率。

左值引用的价值:

  • 左值引用的出现是为了减少拷贝,提高效率。

右值引用的价值:

- 为了进一步减少拷贝,弥补左值引用没有解决的场景。

右值引用的场景

比如:
场景1:

自定义类型中深拷贝的类,必须传值返回的场景。(上面将的移动构造和移动赋值就是这个场景1)

场景2:

容器的插入接口,如果插入的对象是右值,可以利用移动构造将资源转移给数据结构中的对象,也可以减少拷贝。

比如:

int main()
{
	list<dzt::string> lt;
	dzt::string s1("111111111111111111111111111");
	lt.push_back(s1);
	cout << endl;


	dzt::string s2("222222222222222222222222222");
	lt.push_back(move(s2));
	cout << endl;

	lt.push_back("33333333333333333333333333333");
	cout << endl;

	return 0;
}

如果没有实现移动构造和移动赋值,运行后结果如下:

在这里插入图片描述

分析:

  • 1)第一条语句调用构造函数,是因为string支持单参数的构造函数能隐式类型转换,把字符串常量转换成string。
  • 2)第二条语句调用拷贝构造函数是因为:
    • list的push_back大致如下:
    • 在push_back时,会new一个节点,而该节点存储的val值就是string
  • -此时就会调string的拷贝构造,是一次深拷贝。
  • 第三条语句调用构造函数的原因是:在string的拷贝构造中,需要new一块新的空间,此时这里我使用的是 <<现代写法>> ,<<现代写法>> 中会调用一次构造函数。
  • 在这里插入图片描述

所以每次插入时都会产生一次深拷贝。

如果有了移动构造和移动赋值,情况截然不同:
编译器会将构造出的临时对象识别成将亡值,直接转移资源,从而每次都减少了一次深拷贝次数。
在这里插入图片描述

第一种情况没有走移动构造是因为,先是进行了隐式类型转换,调用构造构造了string,然后这个string再插入,再在push_back函数中调用拷贝构造,这并不是连续的行为,所以编译器不会优化,也就不会将s1识别成将亡值。

这里要区分左值和右值的原因是:

如果一个自定义类型的值不是右值,但编译器把它当成右值看待的话,那在拷贝构造/赋

值时被拷贝对象的资源就会被抢走。

move函数

move函数的返回值是一个右值属性。
比如

string ret = move(str);

move(str)的返回值是一个右值,但是str本身不会被改变成右值。

六、关于右值引用的功能和属性问题(完美转发)

模板中的&&不代表右值引用,而是代表万能引用。
万能引用:既能接收左值,又能接收右值
实参是左值,它就是左值引用(引用折叠)
实参是右值,它就是右值引用。

//完美转发
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);
}

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;
}

运行结果会超乎预期:
在这里插入图片描述
原因是:右值引用的属性还是左值引用,只是功能上是右值引用。

右值引用必须修改,否则就无法完成移动构造和移动赋值。
在这里插入图片描述

在移动构造和移动赋值中,都有这么一条语句,s是右值,右值绝对是无法传给左值的。如果它不能修改的话,这条语句是一定会报错的。

所以为了支持移动构造和移动赋值,右值引用变量的属性会被编译器识别成左值。

int main()
{
	int&& r = 10;
	++r;
	cout << r << endl;
	cout << &r << endl;
}

尝试运行这段代码,r只是右值引用的别名,是能修改的,也是有地址的。

上面说了,为了支持移动构造和移动赋值,编译器会将右值强行识别成左值属性。

可如果存在一些场景,就需要右值保持右值属性呢?
比如:
在这里插入图片描述
在容器的插入函数中,val是一个右值,如果将它识别成右值,会匹配下面的构造,可是在swap修改的时候就无法修改。

所以就有一个完美转发的东西能解决。
大致使用方法如下:

结合上面的代码,在这里就用到了完美转发。
保持了原生对象的属性。

// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}

总结

C++11前半部分就到这里,后半部分会持续更新。

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

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

相关文章

【HTML+CSS】零碎知识点

公告滚动条 <!DOCTYPE html> <html><head><title>动态粘性导航栏</title><style>.container {background: #00aeec;overflow: hidden;padding: 20px 0;}.title {float: left;font-size: 20px;font-weight: normal;margin: 0;margin-left:…

23款奔驰C260L升级小柏林音响 15个喇叭 是多了哪两个

22款奔驰C级在音响系统方面也做出了改变&#xff0c;20款奔驰C级的小柏林音响是13喇叭590瓦功率&#xff0c;小柏林音响是多了哪两个喇叭呢&#xff1f;也就是眼镜盒上的两个&#xff0c;在之前的C级车型上&#xff0c;小柏林是没有这两个扬声器的。无论是小柏林还是大柏林&…

凉鞋的 Godot 笔记 201. 第三轮循环:引入变量

201. 第三轮循环&#xff1a;引入变量 在这一篇&#xff0c;我们进行第三轮 编辑-测试 循环。 在之前我们编写了 输出 Hello Godot 的脚本&#xff0c;如下: extends Node# Called when the node enters the scene tree for the first time. func _ready():print("Hell…

MySQL——六、库表操作(下篇)

MySQL 一、INSERT语句二、REPLACE语句三、UPDATE语句四、delete和TRUNCATE语句五、MySQL用户授权1、密码策略2、用户授权和撤销授权 一、INSERT语句 #在表里面插入数据&#xff1a;默认情况下&#xff0c;一次插入操作只插入一行 方式1&#xff1a; INSERT [INTO] 表名 [(colu…

word如何设置页码?教你快速提升文档颜值!

在创建文档时&#xff0c;为了更好地组织内容&#xff0c;页码是一个必不可少的元素。但是很多人不知道word如何设置页码&#xff0c;其实word提供了多种设置页码的方法&#xff0c;以满足不同文档的需求。本文将详细介绍3种设置页码的方法&#xff0c;无论您是初学者还是有经验…

【数据结构】排序--快速排序

目录 一 概念 二 快速排序的实现 1. hoare版本 (1)代码实现 (2)单趟排序图解 (3) 递归实现图解 (4)细节控制 (5)时间复杂度 (6)三数取中优化 2 挖坑法 (1)代码实现 (2)单趟图解 3 前后指针法 (1) 代码实现 (2) 单趟图解 ​编辑4 优化子区间 5 非递归快速排…

零基础Linux_18(进程间通信)共享内存+消息队列+信号量

目录 1. 共享内存 1.1 共享内存概念 1.2 系统函数shmget 1.2.1 key值和ftok 1.2.2 sizeshmflg返回值 1.3 系统调用shmctl 1.4 系统调用shmat和shmdt 1.5 共享内存进程间通信前期代码 1.6 共享内存进程间通信 2. 消息队列(了解) 3. 信号量(了解) 4. 笔试选择题 答案…

git的介绍和安装、常用命令、忽略文件、分支

git介绍和安装 首页功能写完了 ⇢ \dashrightarrow ⇢ 正常应该提交到版本仓库 ⇢ \dashrightarrow ⇢ 大家都能看到这个 ⇢ \dashrightarrow ⇢ 运维应该把现在这个项目部署到测试环境中 ⇢ \dashrightarrow ⇢ 测试开始测试 ⇢ \dashrightarrow ⇢ 客户可以看到目前做的…

137. 只出现一次的数字 II (中等。位运算)

不会做&#xff0c;思路来自官解 对于每一位来说&#xff0c;每个数字只能是 0 或 1&#xff0c;如果所有数字的第 i 位和能被3整除&#xff0c;那么表示只出现一次的这个元素在这一位上为0&#xff0c;反之就是1 class Solution:def singleNumber(self, nums: List[int]) -&g…

java.math.BigDecimal cannot be cast to java.lang.String

1.数据库&#xff1a; oracle 数据类型&#xff1a; NUMBER 2.java代码示例 List<Map<String, String>> list new ArrayList<>(); 错误&#xff1a; String nchangestdrate list.get(i).get(“rate”); 或者 BigDecimal nchangestdrate (BigDecimal)li…

【扩散模型从原理到实战】Chapter2 Hugging Face简介

文章目录 Hugging Face的核心功能介绍Hugging Face开源库Hugging Face开源库Gradio工具介绍参考资料 Hugging Face是机器学习从业者协作和交流的平台&#xff0c;成立于2016年&#xff0c;在纽约和巴黎设有办事处&#xff0c;团队成员来自世界各地&#xff0c;远程办公。 致力于…

程序员凭本事赚钱被没收所得,是否体现了“重刑主义”?

近日&#xff0c;承德程序员翻墙“打工”被罚款一事冲上热搜&#xff0c;热度不减。 1.人们同情该程序员的不幸遭遇&#xff0c;为他鸣不平&#xff1b; 2.包括程序员群体在内的大众关心此事背后的谋生问题&#xff1b; 3.引发了大众对承德执法的反思与质疑&#xff1b; 4.甚至…

【QT开发(8)】QT 中使用tensorrt

在之前的文章《【TensorRT&#xff08;2&#xff09;】研究美团tech的yolov6的TensorRT部署》说明了tensorRT 的使用流程。今天尝试将其并入QT 项目中。 文章目录 项目地址参考资料&#xff1a;该分支主要做的工作Task 1:读取视频文件&#xff0c;然后通过 dds 发送的事情Task…

2008-2017年上市公司海外收入数据

2008-2017年上市公司海外收入数据 1、时间&#xff1a;2008-2017年 2、指标&#xff1a;上市公司海外收入 3、范围&#xff1a;A股 4、来源&#xff1a;WIND 5、指标解释&#xff1a; 上市公司&#xff08;The listed company&#xff09;&#xff0c;根据《公司法》第四…

交通目标检测-行人车辆检测流量计数 - 计算机竞赛

文章目录 0 前言1\. 目标检测概况1.1 什么是目标检测&#xff1f;1.2 发展阶段 2\. 行人检测2.1 行人检测简介2.2 行人检测技术难点2.3 行人检测实现效果2.4 关键代码-训练过程 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 毕业设计…

MySQL——七、MySQL备份恢复

MySQL 一、MySQL日志管理1、MySQL日志类型2、错误日志3、通用查询日志4、慢查询日志5、二进制日志5.1 开启日志5.2 二进制日志的管理5.3 日志查看5.4 二进制日志还原数据 二、MySQL备份1、备份类型逻辑备份优缺点 2、备份内容3、备份工具3.1 MySQL自带的备份工具3.2 文件系统备…

做接口测试的流程一般是怎么样的?UI功能6大流程、接口测试8大流程这些你真的全会了吗?

在讲接口流程测试之前&#xff0c;首先需要给大家申明下&#xff1a;接口测试对于测试人员而言&#xff0c;非常非常重要&#xff0c;懂功能测试接口测试&#xff0c;就能在企业中拿到一份非常不错的薪资。 这么重要的接口测试&#xff0c;一般也是面试笔试必问。为方便大家更…

k8s-20 hpa控制器

hpa可通过metrics-server所提供pod的cpu 或者内存的负载情况&#xff0c;从而动态拉伸控制器的副本数&#xff0c;从而达到后端的自动弹缩 官网&#xff1a;https://kubernetes.io/zh-cn/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/ 上传镜像 压测 po…

模型量化笔记--KL散度量化

KL散度量化 前面介绍的非对称量化中&#xff0c;是将数据中的min值和max值直接映射到[-128, 127]。 同样的&#xff0c;前面介绍的对称量化是将数据的最大绝对值 ∣ m a x ∣ |max| ∣max∣直接映射到127。 上面两种直接映射的方法比较粗暴&#xff0c;而TensorRT中的int8量化…

有没有好用的扫描爆破工具?来看这里

工具介绍 goon集合了fscan和kscan等优秀工具功能的扫描爆破工具。功能包含&#xff1a;ip探活、port扫描、web指纹扫描、title扫描、压缩文件扫描、fofa获取、ms17010、mssql、mysql、postgres、redis、ssh、smb、rdp、telnet、tomcat等爆破以及如netbios探测等功能。 参数说…