【C++11】右值引用与移动构造、万能引用与完美转发

news2024/10/5 15:27:30

目录

一、右值引用

1.1 左值引用和右值引用

1.2 左值引用与右值引用比较

1.3 右值引用的使用场景和意义

二、移动构造

2.1 移动构造的实现

2.2 移动赋值

2.3 默认成员函数

2.4 default关键字

2.5 delete 关键字

2.6 STL中的移动构造

二、完美转发

2.1 模板中的万能引用

2.2 完美转发


一、右值引用

1.1 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了右值引用语法特性,所以从现在我们之前学习的引用就叫做左值引用。

其实,无论左值引用还是右值引用,都是给对象起别名。

什么是左值?什么是左值引用?

        左值是一个表述数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号的左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

        左值引用就是给左值的引用,给左值取别名。

左值:可以取地址 (最重要的特征)

左值还可以对其进行赋值,但是 const 修饰的变量不能再次进行修改,不过 const 修饰的后的左值也仍然是左值。

左值引用,即引用左值:

那什么是右值?什么是右值引用?

        右值也是一个表示数据的表达式,如:字面常量,表达式返回值,函数返回值(不能是左值引用返回)等等。右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。

        右值引用就是对右值的引用,给右值起别名。

常见的右值  --- (不能取地址,不能出现在赋值符号左边):

右值引用就是对右值的引用,给右值起别名。就是在这种情况下使用的:

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,就比如上图中可以将 rr1重新赋值并且取地址,本质上rr1出现在赋值符号的左边,rr1 本质上成为了一个左值。

1.2 左值引用与右值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可以引用左值,也可以引用右值。

为什么const左值可以引用呢?

        左值引用后,可以通过该变量改变引用的右值。所以加上const,该变量就不能被改变,所以左值引用就能引用右值,是利用了权限的缩小进行的引用。

右值直接使用右值引用接收就行,为什么要允许 const 左值能引用右值?这个规定为什么会存在呢?

        我们都知道,函数传参会生成临时变量,有时我们为了减少拷贝,会将函数参数设置为引用传参,那如果指写成普通的引用传参,那今后我们调用该函数,只能传递左值,无法传入右值。那在C++11之前,没有右值引用,只能使用const引用传参来接收实参。

 所以,当传入引用传参时,建议加上const。

右值引用总结:

  1. 右值引用只能引用右值,不能引用左值。
  2. 但是右值引用可以引用 move 后的左值(当需要右值引用引用一个左值时,可以通过move函数将左值转化为右值)。

1.3 右值引用的使用场景和意义

左值引用解决了哪些问题:

  1. 做参数。
    1. 减少拷贝,提高效率。
    2. 作输出型参数。
  2. 做返回值
    1. 减少拷贝,提高效率。
    2. 引用返回,可以返回修改的对象(如map中的operator[ ])。

前面我们看到左值引用既可以引用左值又可以引用右值,那么为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

左值引用的短板:

当函数返回对象是一个局部对象,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少依次拷贝构造(如果是一些旧的编译器可能进行两次拷贝构造)

 再比如说例题杨辉三角:118. 杨辉三角

返回的对象也不能使用左值引用返回,因为我们创建的数组是局部变量,函数调用完就销毁了,所以只能进行拷贝返回。而这个二维数组的拷贝返回消耗就非常大了。

 那写下来我们探究一下 to_string(int value) 接口返回不使用引用,对象被拷贝了几次。

参数-fno-elide-constructors是关闭g++所有的编译优化。

发现如果不加上引用,聪明的编译器不加以处理的话,那 to_string(int value)这个返回值会被拷贝两次。

这两次拷贝的时机与原因,详见下图:

二、移动构造

2.1 移动构造的实现

所以我们应该对右值属性的返回采取一些措施,来提高程序的运行效率。

右值分为:

  1. 内置类型右值,又称为纯右值,比如 x+y 的结果。
  2. 自定义类型右值,又称为将亡值,比如 局部对象 str 作返回值时。

我们可以为 string 类提供一个移动构造接口,不去调用拷贝构造。即,当将亡值作为返回值返回时,不要进行深拷贝,而是进行资源的转移。

移动构造的本质就是及那个参数右值的资源窃取过来,占位己有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

// s1.swap(s2)
void swap(string& s)
{
	::swap(_str, s._str);
	::swap(_size, s._size);
	::swap(_capacity, s._capacity);
}

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

接下来我们写两句代码来验证,如果添加了移动构造,对于右值数据进行构造对象,会出现什么场景:

int main()
{
    Brant::string str1("hello,Brant");
    cout << "str1构造完成" << endl;
    cout << "--------------" << endl;
    Brant::string str2(str1);
    cout << "--------------" << endl;
    Brant::string str3(move(str1));
    cout << "--------------" << endl;
    Brant::string ret = Brant::to_string(-3456);
    return 0;
}

发现str3的构造使用的是移动构造,并且to_string返回右值时调用的也是移动构造。

注意:

        不要轻易 move 一个左值属性的数据。

我们将 srt1 进行move的前后对比,我们来看 str1 的情况:

 move之后:

所以,添加移动构造后,同一段 to_string(int value) 的函数返回,就会是不同的调用情况:

(注意,如果是拷贝构造,拷贝构造是深拷贝,拷贝构造会根据右值创建一个新的对象给临时变量;而移动构造只是拿着右值的数据,将数据进行了转移。差别自然是很大的)

即,像上图举例的传值返回的情况中,我们就可以设计移动构造来提高效率。

 当没有移动构造接口时,程序为什么会走拷贝构造?

        因为拷贝构造参数是const 左值引用,const 左值引用既能被左值调用,也能被右值调用。

2.2 移动赋值

不仅仅有移动构造,还有移动赋值:

再我们自己的string类中,再去调用to_string(1234),不过这次是将to_string(1234)返回的右值对象赋值给 ret1 对象,这时调用的是移动赋值。

// 赋值重载
string& operator=(const string& s)
{
	cout << "string& operator=(string s) -- 深拷贝" << endl;
	string tmp(s);
	swap(tmp);
	return *this;
}

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动赋值" << endl;
	swap(s);
	return *this;
}
int main()
{
	Brant::string ret1;
	ret1 = Brant::to_string(1234);
	return 0;
}

2.3 默认成员函数

原来的C++11之前,类中有 6 个默认成员函数:

1.构造函数;2.析构函数;3.拷贝构造函数;4.赋值运算符重载函数(拷贝赋值)5.取地址运算符重载函数;6.const 取地址运算符重载函数;

重要的是前4个函数,后两个用处不大。默认成员函数即我们不书写编译器会默认生成的。

引入了右值引用后,C++11则添加了两个默认成员函数:移动构造函数与移动赋值(运算符重载)函数。

移动构造函数的注意点:

  1. 生成条件:自己实现了移动构造函数,编译器不会提供拷贝构造函数。如果没有实现移动构造函数,并且析构函数、拷贝构造函数、拷贝赋值函数都没有实现的话,那么编译器会自动生成一个移动构造函数。
  2. 执行方式:默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个类是否实现了移动构造,如果实现了就调用自身的移动构造函数,没有实现就调用拷贝构造函数。(默认移动构造与移动赋值执行方式相似)。

移动赋值函数的注意点:

  • 生成条件:自己实现了移动构造函数,编译器不会提供拷贝构造函数。如果没有实现移动构造函数,并且析构函数、拷贝构造函数、拷贝赋值函数都没有实现的话,那么编译器会自动生成一个移动构造函数。
  • 执行方式:默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节进行拷贝,自定义类型成员,则需要看这个类是否实现了移动赋值函数,如果实现了就调用自身的移动赋值函数,没有实现就调用拷贝赋值函数(默认移动赋值与移动构造执行方式相似)。

接下来我们添加一个Person类,类中使用我们自己实现的string类,便于观察。

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
private:
	Brant::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;            //拷贝构造
	Person s3 = std::move(s1); //移动构造
	Person s4;              
	s4 = std::move(s2);        //移动赋值
	return 0;
}

运行结果如下:

因为我们没有实现移动构造,并且没有实现析构、拷贝、拷贝赋值函数,所以编译器会默认生成的移动构造,该移动构造会去调用自定义类型的移动构造,

 如果我们添加一个析构函数,则不会默认生成移动构造,则结果会统统调用拷贝构造。

2.4 default关键字

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如下面这个场景:

如果我们实现析构、拷贝构造、拷贝赋值中的一个,仍然想让编译器为我们生成移动构造或移动赋值,我么可以使用default关键字来让编译器强制生成。

 但是!如果强制生成了移动构造,那析构、拷贝构造、赋值拷贝、移动赋值这些函数就不会被编译器默认生成!

所以,我们还要将要使用的函数也进行强制默认生成处理:

关于 default 关键字,其实并不会应用在上面这种场景,而常应用在:

比如某些类涉及深拷贝,所以书写了拷贝构造,这种就会导致默认构造函数无法生成,因为拷贝构造也算是一种构造,所以我们可以使用default关键字让编译器强制生成构造函数。

2.5 delete 关键字

如果能想要限制某些默认函数的生成,在C++98中,将该函数设置称为private属性,并且不进行定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需要在该函数声明后加上 delete 关键字即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

接下来我们来设计一个特殊类

  •  该类只能在堆上创建对象,不能在栈或静态区等空间创建。

那我们就可以使用 delete 关键字删除该类的析构函数,这样对象就不在栈上或静态区中创建了,而只能使用 new 创建,而 new 创建就是在堆上创建对象。 

 那问题又来了,如果该类中会开辟额外的空间,我们将析构函数删除了,则会导致内存泄漏问题,那应该如果处理呢?如下图:

这样的话我们只能额外提供一个 Destroy 函数用于释放开辟的空间。但是Destory 函数也只能释放类额外开辟的空间,那类本身的空间该如何释放呢?

故我们可以使用 operator delete 函数释放类本身的空间。

关于operator delete 和 delete 的区别可以看这篇博客:C++的delete以及operator delete重载。

 到此,这个类的对象就只能在堆上创建了,并且处理好了其空间释放的问题。

2.6 STL中的移动构造

STL容器插入接口函数也增加了右值引用版本:

vector、list的push_back函数

 

 此时库中的push_back也支持了右值进行插入,这样其插入就会调用移动构造,效率也是得到了极大的提升。

二、完美转发

2.1 模板中的万能引用

比如下面这段有着模板参数的函数:

注意:一定要是模板才能触发引用折叠!!!

template<typename T>
void PerfectForward(T&& t)
{
 Fun(t);
}

t 既能引用左值,也能引用右值。这种现象也被称作为引用折叠,如果传来左值,那两个引用就会变成一个引用,就会进行左值的处理,如果传入的是右值,也会进行左值的处理。

下面是一段验证的代码(无论是左值还是右值都会被折叠为左值):

  • 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
  • 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
  • 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
  • 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

2.2 完美转发

而完美转发会在传参过程中保留对象原生类型的属性。

添加完美转发之后的结果:

完美转发在实际中的使用场景:

例如在我们自己编写的 list 中,想让右值数组走专门处理右值的函数,就不得不使用完美转发,因为在将数据的传参过程中,万能引用会将其都退化为左值属性,所以我们可以将关键的传参处加上完美转发。

template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};
template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void PushBack(T&& x)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));
	}
	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};

效果如下:

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

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

相关文章

利用剪枝降低bfs算法的时空复杂度(一道OJ题目)

作者&#xff1a;非妃是公主 专栏&#xff1a;《算法》《刷题笔记》 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 《算法》专栏系列文章 算法设计与分析复习01&#xff1a;主方法求递归算法时间复杂度 算法设计与分析…

我写了一个脚本,实现了图片分类问题的全自动化训练

众所周知,图片分类问题属于计算机视觉中比较容易解决的问题之一 但 这几天被数据集的问题搞得焦头烂额, 照理说分类问题的数据集应该比较好制作 但 如果之前没有现成的数据集 也会变得比较麻烦 直到我偶然发现了一个HuggingFace的图片搜索API 无限次调用 而且不需要身份验证 真…

【手撕面试题】HTML+CSS(高频知识点一)

目录 面试官&#xff1a;给定一个元素&#xff0c;如何实现水平垂直居中&#xff1f; 面试官&#xff1a;padding与margin有什么不同&#xff1f; 面试官&#xff1a;vw和百分比有什么区别&#xff1f; 面试官&#xff1a;行内元素与块级元素有什么区别&#xff1f; 面试官…

MapReduce实现TopN

目录 1、先导知识 2、案例 2.1 需求 2.2 代码实现 FlowBean类 Mapper类 Reducer类 Driver类 3、总结 1、先导知识 TreeMap底层是根据红黑树的数据结构构建的&#xff0c;默认是根据key的自然排序来组织&#xff08;比如integer的大小&#xff0c;String的字典排序&…

一刷代码随想录——回溯算法

1.理论基础【1】本质回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。回溯是递归的副产品&#xff0c;只要有递归就会有回溯。因为回溯的本质是穷举&#xff0c;穷举所有可能&#xff0c;然后选出我们想要的答案&#xff0c;如果想让回溯法高效一些&#xff0c;可以…

线性DP与真题

目录 一、前言 二、最长公共子序列&#xff08;lanqiaoOJ题号1189&#xff0c;类似于1054&#xff09; 三、最长递增子序列 1、蓝桥骑士&#xff08;lanqiaoOJ题号1188&#xff09; 四、编辑距离 1、字符串转换&#xff08;lanqiaoOJ题号1507&#xff09; 五、网络图上的…

JavaScript两数之和

两数之和 两层for循环 // O(n^2) const twoNum function(nums,target){for(let i 0;i<nums.length;i){for(let ji1 ;j<nums.length;j){if(nums[i]nums[j]target){return[i,j]}}} }双指针 // 当数组为有序的时候O(n) const twoNum2 function(nums,target){let i 0 …

SpringCloud学习

由于Spring Cloud基于Spring Boot构建&#xff0c;而Spring Cloud Alibaba又基于Spring Cloud Common的规范实现&#xff0c;所以当我们使用Spring Cloud Alibaba来构建微服务应用的时候&#xff0c;需要知道这三者之间的版本关系。 目前Spring Cloud Alibaba的版本与Spring Bo…

1-1MySql复习

MySql复习 一 数据类型 数值 字符串 ​ char(5) 定长字符串 varchar(5) 可变长度字符串 日期 ​ timestamp 记录行数据的最后修改事件 二 基本查询 1 聚合函数 avg count sum max min 2 排序 order by ​ asc ​ desc 3 分组 group by … having … 分组通常跟…

Python语言的重要性(模式识别与图像处理课程作业)

Python语言的重要性&#xff08;模式识别与图像处理课程作业&#xff09;Python语言的重要性1 Python的优点主要有&#xff1a;1.1、简单1.2、易学1.3、速度快1.4、免费1.5、高层语言1.6、解释性1.7、面向对象1.8、可扩展性1.9、可嵌入性1.10、丰富的库1.11、规范的代码2 Pytho…

TCP/IP网络编程——套接字的多种可选项

完整版文章请参考&#xff1a; TCP/IP网络编程完整版文章 文章目录第 9 章 套接字的多种可选项9.1 套接字可选项和 I/O 缓冲大小9.1.1 套接字多种可选项9.1.2 getsockopt & setsockopt9.1.3 SO_SNDBUF & SO_RCVBUF9.2 SO_REUSEADDR9.2.1 发生地址分配错误&#xff08;B…

高效学 C++|编程实例之计算器

本节将实现一个能进行实数间加、减、乘、除运算的简易计算器。首先创建一个基于QWidget带界面的Qt项目&#xff0c;然后按照如下步骤进行操作&#xff1a; 01、计算器界面设计 在界面中拖入两个单行文本框和十七个按钮&#xff0c;按钮上显示的文字、按钮对象和单行文本框对象…

百分百拿捏offer的自动化测试面试题全套教程

最近很多咨询我&#xff0c;有没有软件测试方面的面试题&#xff0c;尤其是Python自动化测试相关的最新面试题&#xff0c;所以今天给大家整理了一份&#xff0c;希望能帮助到你们。 接口测试基础 1、公司接口测试流程是什么&#xff1f; 从开发那边获取接口设计文档、分析接口…

VUE3 指令 插槽

指令 指令是 Vue 模板语法里的特殊标记&#xff0c;在使用上和 HTML 的 data-* 属性十分相似&#xff0c;统一以 v- 开头&#xff08; e.g. v-html &#xff09;。 它以简单的方式实现了常用的 JavaScript 表达式功能&#xff0c;当表达式的值改变的时候&#xff0c;响应式地…

1x1卷积、Inception网络

目录1.1x1卷积(1x1 convolution)又称网络中的网络(network in network)池化层只能压缩图像的宽和高&#xff0c;1x1卷积能压缩通道数量&#xff0c;减少计算成本。如上图&#xff0c;输入维度的通道数为192&#xff0c;用32个1x1x192的filters&#xff0c;就能将输出的通道数压…

java基础—面试题一

文章目录1.和equals区别是什么&#xff1f;2.Java中的 <<、>>、>>> 是什么3.if-else-if-else与switch的区别4.while和do-while的区别5.switch 是否能作用在 byte 上&#xff0c;是否能作用在 long 上&#xff0c;是否能作用在String上6.&和&&…

大数据技术架构(组件)16——Hive:内置UDTF函数

1.4.11、内置UDTF函数1.4.11.1、explodeselect explode(array(100,200,300));Array<int> myCol[100,200,300][400,500,600]得到的结果如下&#xff1a;(int) myNewCol1002003004005006001.4.11.2、posexplodeselect posexplode(array(A,B,C));1.4.11.3、parse_url_tuples…

2023云原生安全值得关注的3个方向

如果说过去几年教会了我们什么的话&#xff0c;那就是云原生和开源环境中安全的重要性。 Log4j 等漏洞产生的重大影响&#xff0c;在无数的行业中浮现&#xff0c;对于云原生环境中的其他安全问题也越来越受到重视。 组织不再质疑是否要迁移到云端&#xff0c;而是在寻找最快、…

centos下安装docker 并通过docker安装gitlab

一:安装docker1、若之前安过docker&#xff0c;可以先卸载yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-selinux \docker-engine-selinux \docker-engine \docker-ce2、更新yum…

软件测试基础(四) 之 软件测试的覆盖率

一、什么是软件测试的覆盖率&#xff1f;软件测试覆盖率是软件测试技术有效性的一个度量手段&#xff0c;用来度量测试完整性。意思概括的说&#xff0c;软件测试的工作中会有非常非常多的item&#xff08;任务&#xff09;&#xff0c;执行过的任务和总任务数的一个比值&#…