【C++11】右值引用 + 移动语义 + 完美转发(重点)

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

  • 前言:STL中一些变化
      • • 新容器
      • • 容器中的一些新方法
  • 一、如何判断左值和右值
  • 二、左值引用和右值引用
  • 三、左值引用 vs 右值引用
  • 四、move函数
  • 五、 右值引用使用场景
      • 5.1 场景一:移动语义
      • 5.2 场景二:STL容器插入接口函数
      • 5.3 场景三:完美转发
        • 5.3.1 万能引用
        • 5.3.2 完美转发保持值的属性
  • 六、相关代码

前言:STL中一些变化

• 新容器

C++11中新增了以下几个容器(用橘色圈起来):

在这里插入图片描述

实际上最有用的是哈希系列unordered_mapunordered_set

剩下的容器arrayforward_list非常鸡肋,实际上很少使用。

  • array容器

文档介绍:点击跳转

在这里插入图片描述

C++11标准中,引入了一个容器array,它的底层使用了非类型模板参数,是一个真正意义上的泛型数组(定长数组),这个是用来对标C语言传统数组的。

以下是array容器的基本用法:

在这里插入图片描述

看完以上接口,array支持的,数组也都是支持的。那么它们有什么区别呢?

  • 相同点:array也并没有进行初始化。

在这里插入图片描述

  • 要说有区别的话:array对于越界读、写检查更为严格;传统数组越界读写,不会发生报错

在这里插入图片描述

【吐槽】虽然对越界行为检查严格 ,但在实际开发中,很少使用array容器,因为它对标传统数组,连初始化都没有,并且大小也是固定的,因此不够灵活。

相比之下,vector也是类似于数组的容器,它允许动态改变大小、对于越界读和写检查也一样严格。因此,在功能和实用性上可以全面碾压array,因此可以说array是一个鸡肋的容器。

  • forward_list容器

文档介绍:点击跳转

在这里插入图片描述

以下是forward_list的常见接口:

在这里插入图片描述

forward_list翻译过来就是单链表,因此一个结点只存值和指向下一个结点的指针。算了,直接开始(吐槽)吧。首先先说结论:forward_list还是一个非常鸡肋的容器。

  • 从以上接口可以看出,它只支持头删pop_front和头插push_front。为什么不支持尾删和尾插呢?因为它效率低啊!尾插需要找到尾结点、尾删需要找到尾节点的前一个结点,这些操作都要从头部开始向后遍历,时间复杂度铁铁的O(N)
  • 另外,forward_list还不提供size()接口

如果要说forward_list有优势,那就是内存占用更小(每个结点节省了一个前驱指针),但是它还是比较鸡肋,因此在实际中使用list会更多一点。

• 容器中的一些新方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。

比如cbegincend

在这里插入图片描述

这玩意其实也很鸡肋,因为普通版的beginend都已经重载了const版本。对于C++开发人员还是区分得开的。

有坏当然也有好的方面,比如:

  1. 所有容器均重载了initializer_list<T>,使容器初始化更加方便。
  2. 所有容器均支持移动构造和移动赋值,可以极大提高效率(本篇重点)

在这里插入图片描述

  1. 所有容器均支持右值引用相关插入接口,同样可以提高效率(本篇重点)

在这里插入图片描述


一、如何判断左值和右值

要想搞懂左值引用和右值引用,首先要得明白什么是左值和右值。很多人认为在赋值符号左边的就是左值,在赋值符号右边的就是右值。 这其实并不完全正确,比如:

int main()
{
	int a = 1; // a是左值
	int b = a; // a又变成右值?
}

所以,以上变量a到底是左值还是右值?(答案是:左值)

  • 交给大家简单粗暴的判断左值的方法:可以取地址就是左值

举个例子,以下是常见的左值:

在这里插入图片描述


  • 如何判断右值?

右值通常被认为是临时的、一次性的值或者是将亡值右值可以出现在赋值符号的右边,但是绝不能出现出现在赋值符号的左边。就这么说吧,只要 不能取地址的就是右值。常见的右值有:字面常量、表达式返回值,函数返回值(临时对象返回)等。

在这里插入图片描述

二、左值引用和右值引用

大家只要记住一句话,不管是什么引用,都是取别名左值引用就是对左值取别名,右值引用就是对右值取别名

  • 首先先来看看左值引用的例子
int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;

	return 0;
}

由上可以看出,左值引用就是C++刚入门学的那个引用,唯一有区别的还是右值引用。

  • 用两个&&表示右值引用。
double Min(double x, double y)
{
	return x > y ? y : x;
}

int main()
{
	// 以下几个都是常见的右值
	10;
	1.1 + 2.2;
	Min(1.1, 2.2);

	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = 1 + 2;
	double&& rr3 = Min(1, 2);

	return 0;
}

三、左值引用 vs 右值引用

  • 问题1:左值引用能否给右值取别名?

在这里插入图片描述

正常来说是不行的,但由于 右值都具有常属性,因此用const引用即可

在这里插入图片描述

  • 问题2:右值引用能否引用左值?

在这里插入图片描述

编译器已经给出很明确的报错信息了:无法将右值引用绑定到左值

但右值引用可以引用move以后的左值。点击跳转

既然左值引用即可以引用左值,也能引用右值。那么以下情景是否构造函数重载是否存在调用歧义呢

在这里插入图片描述

答案是当然构成重载,编译器会选择最匹配的参数调用

在这里插入图片描述

四、move函数

  • 虽然右值引用不能直接引用左值,但是可以通过调用一个名为move函数来获得绑定到左值上的右值引用

  • move的主要作用是显式地标识对象为右值,以便编译器能够选择调用移动语义相关的操作,而不是进行拷贝操作

int a = 0;
int&& rr = move(a);

可以这么理解:当右值引用 引用右值时,则是先将引用的对象的临时资源 “转移” 到特定位置(不会发生拷贝),然后指向该位置中的资源,此时可以对右值进行修改操作。

在这里插入图片描述

另外,虽然右值引用引用的是右值,但右值引用本身是可以取地址的

在这里插入图片描述

除此之外,语法还支持给右值引用加const,这样做的含义是 不能修改右值引用后的值

在这里插入图片描述

但我们一般建议不要用const右值引用,因为使用右值引用就是为了“转移”资源,加了const还不如直接改用const左值引用。


注意:不要轻易使用move函数,左值中的资源可能会被转走。如果此时我们直接将左值move后构造一个新对象,会导致原本左值中的资源丢失

在这里插入图片描述

五、 右值引用使用场景

5.1 场景一:移动语义

前面我们可以看到左值引用既可以引用左值也可以通过const引用引用右值,那为什么C++11还要提出右值引用呢?

既然提出了就一定是为了解决左值引用存在的缺陷,那么我们可以通过分析左值引用的使用场景及核心价值来推断。

【左值引用】

  • 使用场景:1. 做输出型参数(形参的改变影响实参)。 2. 做返回值。
  • 核心价值:减少拷贝,提高效率。

我们知道,左值引用做返回值是有一定的缺陷的!如果是左值引用做返回值,出了作用域,对象不能被销毁;如果出了作用域,对象被销毁,那么就不能使用左值引用做返回值
(不知道为什么可以看看这篇博客:点击跳转)

string func()
{
	string str("hello world");
	return str;
}

int main()
{
	string s = func();
	cout << s << endl;
	return 0;
}

str是局部对象,出了func函数作用域,对象销毁,那么就不能用左值引用返回,那么按照惯例只能使用传值返回。而 传值返回是有代价的,对于较大的对象(如大型结构体、类对象等),可能会导致较大的性能开销,因为它需要在内存中复制整个对象的内容

接下来可以简单的来验证一下,下面是简单模拟实现的string类,为了更好的观察是否发生了深拷贝行为,在 拷贝构造函数中加入了对应的打印语句。

  • string.h
#pragma once
#include <iostream>
#include <assert.h>
namespace wj
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		
		void swap(string& s)
		{
			swap(_str, s._str);
			swap(_size, s._size);
			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;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		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)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}
  • Test.cpp

在这里插入图片描述

以上代码本来应该是两次拷贝构造,但对于一行的两次拷贝构造,新一点的编译器会优化成一次拷贝构造。虽然优化成一次拷贝构造,但是还是需要拷贝整个对象的内容,但是有很大的消耗。

因此,C++11中允许使用移动语义之移动构造解决上述问题:在wj::string中增加移动构造,移动构造本质是窃取别人的资源来构造自己,占位已有,那么就不用做深拷贝了(提高性能),所以它叫做移动构造。

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

接下来再来看看结果:

在这里插入图片描述

那么问题来了,对象str是一个左值啊,它是怎么调用移动构造函数的?

这其实是编译器的优化!当Myfunc函数返回一个wj::string对象时,编译器会在其内部将str视为右值,并使用移动构造函数来将其内容转移到 s 中。这样做可以避免不必要的拷贝操作,提高了程序的性能


接下来再来看,如果两次的拷贝构造不再同一行,编译器就不能实现优化,那么就是实打实的两次拷贝构造,那么这个消耗是巨大的。(在此之前我屏蔽了移动构造)

在这里插入图片描述

第三次打印的结果是无法避免的,因为在调用operator=会重新开辟空间来深拷贝对象的资源。

在这里插入图片描述

因此,C++11同时也引入了移动语义之移动赋值,用于在对象之间实现资源的高效转移。移动赋值运算符允许将一个对象的资源从另一个对象转移到自身,而不是通过拷贝构造或拷贝赋值运算符来进行资源的复制。

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

在这里插入图片描述

接下来再将移动拷贝的代码取消注释,然后再来看看打印结果

在这里插入图片描述

综上,移动语义(移动构造 + 移动赋值)弥补了自定义类型中深拷贝的类,必须传值返回的场景。避免不必要的资源复制,提高了程序的性能和效率

5.2 场景二:STL容器插入接口函数

C++11标准出来之后,STL中的容器除了增加移动构造和移动赋值之外,STL容器插入接口函数也增加了右值引用版本。

在这里插入图片描述

那么右值引用版本插入函数的意义是什么呢?

如果list容器当中存储的是string对象,那么在调用push_backlist容器中插入元素时,可能会有如下几种插入方式:

在这里插入图片描述

  • 对于第一个一定会完成深拷贝,因为s对象是左值,那么lt对象在调用push_back一定会选择最合适的,也就是void push_back (const value_type& val);

  • 剩下的一定会调用void push_back (value_type&& val);。字符串字面量(如 "11111111111111")时,它会调用右值引用版本的 push_back。这是因为字符串字面量是临时对象,是右值,而 push_back 函数的右值引用版本接受右值参数(提高效率)。

5.3 场景三:完美转发

5.3.1 万能引用

在模板中的&&不代表右值引用,而是万能引用,其既能接收任意类型的左值和右值。

  • 如果传入的实参是左值,那么编译器就会将模板实例化为左值引用,也称做引用折叠。
  • 如果传入的实参是右值,那么编译器就会将模板实例化为右值引用。

【基本语法】

template<class T>
void PerfectForward(T&& t)
{
	//...
}

下面重载了四个Func函数,参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值,在PerfectForward函数中再调用Func函数。

我们来判断是否真的很“万能”

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

【程序结果】

在这里插入图片描述

输出的结果好像和我们一开始说的不太一样,最终都匹配到了左值引用版本的Func函数。接下来可以分析为什么?

首先先看第一次调用PerfectForward(10),由于PerfectForward函数的参数类型是万能引用,因此在编译器眼中其实是如下这样的:

// 实参10是int类型,那么对应的T应该实例化为int
// 并且实参10是右值,编译器就会将模板实例化为右值引用
template<typename int>
void PerfectForward(int&& t)
{
    Fun(t);
}

int main()
{
	PerfectForward(10); // 右值
	return 0;
}

这下好像有点眉目了,实参(右值)10传递给形参t,然后再通过t去调用Func函数,而t虽然引用右值,但是它本身是可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。

也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发。

5.3.2 完美转发保持值的属性

要想在参数传递过程中保持其原有的属性,需要在传参时调用forward函数。基本语法如下:

template<class T>
void PerfectForward(T&& t)
{
	Func(forward<T>(t));
}

经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数,这就是完美转发的价值。

在这里插入图片描述

六、相关代码

Gitee仓库链接:点击跳转

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软考高级架构师:嵌入式系统的内核架构

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

Linux 多线程

目录 初识线程 线程的概念 Linux下的线程 线程优缺点 线程控制 线程创建 线程终止 线程等待 线程分离 线程取消 其它 线程互斥 互斥的概念 互斥锁的使用 锁的本质 线程同步 线程同步的概念 条件变量的概念 条件变量的使用 信号量 信号量的概念 信号量接口…

带头双向循环链表实现

1.结构及特性 前面我们实现了无头单向非循环链表&#xff0c;它的结构是这样的&#xff1a; 在这里的head只是一个指向头结点的指针&#xff0c;而不是带头链表的头节点。 而带头双向循环链表的逻辑结构则是这样的 这就是链表的结构&#xff0c;链表的每一个节点都有两个指针…

Sharding Sphere JDBC使用Mybatis的saveBatch无法返回主键的问题

问题背景 项目中使用了MybatisPlus框架&#xff0c;数据库是PostgreSQL&#xff0c;配置了主键自增&#xff0c;新增数据后返回主键到实体类中。 项目中因为数据量问题&#xff0c;需要用到分库分表&#xff0c;因此引入了Sharding Sphere JDBC框架。但是Sharding Sphere JDB…

数据结构-基本概念

1.什么是数据结构&#xff1f; 数据 数据&#xff0c;是对客观事物的符号表示&#xff0c;在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。 结构 &#xff08;1&#xff09;线性结构&#xff08;比如图书目录文件&#xff0c;一对一的关系&#x…

【JAVASE】面向对象程序三大特性之一( 封装)

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609;\n &#x1f34e;个人主页&#xff1a;再无B&#xff5e;U&#xff5e;G-CSDN博客 目标&#xff1a; 1.包的使用 2.static关键字的使用 3.代码…

【Python使用】python高级进阶知识md总结第7篇:死锁,1. 死锁的概念【附代码文档】

python高级进阶全知识知识笔记总结完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;操作系统&#xff0c;虚拟机软件。ls命令选项&#xff0c;mkdir和rm命令选项。压缩和解压缩命令&#xff0c;文件权限命令。编辑器 vim&#xff0c;软件安装。获取进程编号…

今年过去了多少天?(switch)

//今年已经过去了几天&#xff1f; #include <stdio.h> int monthday(int year,int month){switch(month){case 1:return 31;case 2:if ((year % 4 0 && year % 100 ! 0)||year % 400 0){return 29;}else{return 28;}break;case 3:return 31;case 4:return 30;…

谨慎使用通过光纤传输的HDMI光纤线,存严重缺陷

严重缺陷&#xff1a; 1.只能单向传输 只能单向传输&#xff0c;从一端到另一端&#xff0c;和二极管一样&#xff0c;只能单向传输信号。某些情况你需要变更传输方向时&#xff0c;你将欲哭无泪.传统的HDMI线&#xff0c;不带放大器的&#xff0c;都可以双向传输.网上搜索布…

非关系型数据库(缓存数据库)redis的集群

目录 一.群集模式——Cluster 1.原理 2.作用 3.特点 4.工作机制 哈希槽 哈希槽的分配 哈希槽可按照集群主机数平均分配&#xff08;默认分配&#xff09; 根据主机的性能以及功能自定义分配 redis集群的分片 分片 如何找到给定key的分片 优势 二. 搭建Redis群集…

创新数智化全场景福利解决方案,打造极致员工体验

众所周知&#xff0c;企业面临两个市场&#xff0c;一个是前端的产品&#xff08;服务&#xff09;市场&#xff0c;面对的是客户&#xff0c;另一个便是后端市场&#xff0c;即愈来愈烈的人才市场。在风云变幻、人潮涌动的知识经济时代&#xff0c;员工已成为企业未来的竞争关…

C#.手术麻醉系统源码 手麻系统如何与医院信息系统进行集成?

C#.手术麻醉系统源码 手麻系统如何与医院信息系统进行集成&#xff1f; 手术麻醉系统与医院信息系统的集成是一个关键步骤&#xff0c;它有助于实现信息的共享和流程的协同&#xff0c;从而提高医疗服务的效率和质量。手麻系统与lis、his、pacs等系统的对接是医院信息化建设的重…

【亲测有效】微信公众号设置菜单栏显示,未开启自定义菜单,微信公众平台自定义菜单接口开发

微信公众平台自定义菜单接口开发 问题:运营人员在设置微信公众号设置菜单栏显示,未开启自定义菜单解决方案(微信公众平台自定义菜单接口开发):自定义菜单-创建接口请求链接完整代码第一步:在WeChat类里添加代码情况一:没有WeChat类情况,如果已有请看情况二情况二:已有…

柱状图中最大的矩形-java

题目描述(力扣题库 84): 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 解题思想: 单调栈: 利用先进后出的思想, 先算出长度更高的柱子所能勾勒…

jdk目录结构

jdk目录详解 JDK(Java Development Kit&#xff0c;Java开发包&#xff0c;Java开发工具)是一个写Java的applet和应用程序的程序开发环境。它由一个处于操作系统层之上的运行环境还有开发者 编译&#xff0c;调试和运行用Java语言写的applet和应用程序所需的工具组成。 JDK(J…

以动态库链接库 .dll 探索结构体参数

Dev c C语言实现第一个 dll 动态链接库 创建与调用-CSDN博客 在写dll 插件中发现的函数指针用途和 typedef 的定义指针的用法-CSDN博客 两步之后&#xff0c;尝试加入结构体实现整体数据使用。 注意结构体 Ak 是相同的 代码如下 DLL文件有两个&#xff0c;dll.dll是上面提到…

揭开“栈和队列”的神秘面纱

前言 在线性表中不止有顺序表和链表&#xff0c;今天的主角就如标题所说--->认识栈和队列。把他们俩放一起总结是有原因的&#xff0c;还请看官听我娓娓道来~ 什么是栈&#xff1f; 栈&#xff08;stack&#xff09;是限定仅在表尾进行插入和删除操作的线性表 咱可以把栈理…

qt自定义窗口在拖动过程中出现抖动且拖动后位置看上去不对

自定义窗口拖动 引言开发环境关键性代码运行结果原因分析改进代码运行结果globalPos()globalPosition()再次修改代码运行结果区别 引言 本文旨在一个问题的记录&#xff1a;自定义窗口拖动的过程中&#xff0c;窗口不能很好的跟随鼠标移动&#xff0c;此外会出现窗口拖动时抖动…

C语言数据结构(11)——归并排序

欢迎来到博主的专栏C语言数据结构 博主ID&#xff1a;代码小豪 文章目录 归并排序两个有序数组的合并归并归并排序 归并排序的代码 归并排序 两个有序数组的合并 当前有两个有序数组arr1和arr2&#xff0c;我们创建一个可以容纳arr1和arr2同等元素个数的新数组arr。 让一个…

蓝桥杯 经验技巧篇

1. 注意事项 &#x1f468;‍&#x1f3eb; 官方通知 &#x1f468;‍&#x1f3eb; 资料文档 时间&#xff1a;4月13日 9:00~13:00 &#xff08;时长 4小时&#xff09;物品 准考证&#xff08;赛前一周开放下载&#xff0c;自行打印&#xff09;学生证身份证笔、水、外套&a…