【C++】C++11的新特性 --- 右值引用与移动语义

news2024/11/13 9:16:27

在这里插入图片描述

假如生活欺骗了你
不要悲伤,不要心急!
忧郁的日子里须要镇静
相信吧
快乐的日子将会来临
-- 普希金 《假如生活欺骗了你》

C++11的新特性

  • 1 左值与右值
  • 2 左值引用和右值引用
  • 3 引用的意义
  • 4 移动语义
    • 4.1 移动构造与移动赋值
    • 4.2 区分现代写法与移动语义
    • 4.3 实践中落实移动语义
  • 5 万能引用和完美转发
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 左值与右值

C++中,一个表达式不是右值就是左值。C语言中:左值可以位于赋值对象的左边,右值则不能。在C++中就没有这么简单了。在C++中的左右值可以通过是否可以取地址来区分:

  1. 左值表示一个占据内存中可识别位置的一个对象,有可能是一个表达式。更进一步地,可以对左值取地址
  2. 右值即不能进行取地址的值或表达式。包括常量,加减乘除等表达式,临时对象。

PS:左值和右值在内存中都是有地址的,只有左值可以取地址!

左值包括变量名,解引用的指针的等。下面是比较经典的左值,他们都可以进行取地址操作!
注意左值引用和右值引用都是左值

	int a = 1;
	int* p = &a;
	int** pp = &p;
	const int b = 2;

右值一般是常量,表达式,临时变量 ,对于一个常量肯定是无法取地址的!

&10;
&(1 + 1);
&string("111");

当一个对象被作为右值进行使用时,用的是对象的值(内容);用做左值时,实际使用的是对象的身份(在内存中的位置)

2 左值引用和右值引用

左值引用就是对左值进行取别名:

	int a = 1;    		int& ra = a;
	int* p = &a;		int*& rp = p

右值引用就是对右值进行取别名:

int&& rra = (1 + 1);
string&& rrb = string("111");
string&& rrb = to_string(111);

左值引用不可以给右值取别名,但是const左值引用可以

const int& ra = 10;
const string& rb = string("111");

右值引用不可以对左值取别名,但是可以给move后的左值取别名.move也是C++11新加入的特性,我们后面讲。

3 引用的意义

在之前,我们使用引用的目的是什么?是为了减少拷贝,提高性能。

void func1(const string& s);//传引用参数
string& func2(const string& s);//传引用返回

但是,引用返回也会出现一些问题,比如一个函数返回临时变量的引用,这时就会出错。临时变量的生命周期只在func2函数,func2函数返回一个临时变量的引用,在函数执行结束,临时变量就会进行销毁!右值引用也无法解决生命周期的问题!

那右值引用的意义在哪里呢???
我们来看一个情景:(bit::string是自己写的string类,方便看效果)

bit::string ret1 = bit::to_string(1234);

这个会进行几次深拷贝?
在这里插入图片描述
按理来说,应该会会进行两次拷贝构造,首先拷贝构造临时变量,然后ret1在拷贝构造。但是编译器进行了一个优化,实际上只进行了一次拷贝构造。

当我们把这个表达式分开写,就不会进行优化了,没有办法合二为一

bit::string ret1;
ret1 = to_string(1234);

这两种情况的拷贝的代价都挺大,有没有一种简单的解决办法来避免进行深拷贝?
这里可不能使用左值引用,因为临时变量在该行函数结束就销毁,在主函数里会直接挂掉!使用右值引用会直接报语法错误,因为在to_string中返回值是一个左值,左值是不能被右值引用的!
如果将该左值进行move()他就可以被右值引用。

bit::string&& to_string(int val)
{
	bit::string str;
	//...
	//处理
	//...
	return move(str);
}

这样运行依旧会挂掉,因为右值引用也是别名,**无法解决生命周期的问题!**这里左值引用和右值引用是没有区别的!
栈桢图是这样的:
在这里插入图片描述
编译器优化后会只进行一次拷贝构造ret1,但还是进行了深拷贝!

所以这个深拷贝的问题无法通过左值引用或者右值引用来解决!所以就有了移动语义!

4 移动语义

4.1 移动构造与移动赋值

C++11中就加入了一个针对右值引用的拷贝构造 — 移动构造!

PS:左值引用是拷贝构造 ,右值引用是移动构造!— 构成函数重载

//拷贝构造 --- const 左值可以接收左值和右值
string(const string& s) :
	_str(new char[s._capacity + 1])
	,_size(0)
	,_capacity(0)
{
	cout << "string(const string& s = "")  --- 深拷贝" << endl;
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}
//移动构造
string(string&& s)
	:_str(nullptr)
{
	cout << "string(string&& s) --- 移动构造" << endl;
	swap(s);
}

上面我们已经知道了右值引用的的一般类型:匿名对象 ,传值返回的临时对象

int&& rra = (1 + 1);
string&& rrb = string("111"); //匿名对象
string&& rrb = to_string(111);//传值返回的临时对象

这里进行一下跟细致的区分:纯右值和将亡值

  1. 纯右值:内置类型的右值引用是纯右值 :int&& a == 10 ;int&& b = (1 + 1);
  2. 将亡值:自定义类型的右值引用,生命周期只有一行。

分析一下左值的拷贝构造,因为传入的是一个左值,其有自己的内存中的位置,不能修改!必须进行深度的拷贝。但是如果是一个右值,是一个临时变量的右值引用(将亡值),其生命周期就一行,就可以对其进行修改!

string(string&& s)
	:_str(nullptr)
{
	swap(s);//直接将亡值的替换就可以
}

直接进行替换不就可以了?反正也是将亡的,不用他就直接销毁了,怪可惜的!
在这里插入图片描述

**移动构造就是用来解决这个问题的!**通过to_string返回的对左值的move的右值引用,就会调用到移动构造,就避免了深拷贝!(PS:不显式加上move也会调用到移动构造,编译器的优化很强,会强行识别成右值进而进行移动构造!)
在这里插入图片描述

移动构造的代价是很低的,因为是使用的同一块地址,没有开辟新的空间!注意移动构造只能对右值进行处理!必须是将亡值才有可以进行移动构造!
再来看个例子


int main()
{
	string s1("1111111111111111111");
	string s2 = s1;
	string s3 = move(s1);
}

s2是拷贝构造,开辟了新空间。s3进行移动构造,导致S1和空的s3进行了交换,s1变成了空!

对于分开书写的两行代码,编译器也无法进行优化,会进行两次深拷贝!而有了移动构造和移动赋值,都是对一块资源的移动,成本很低! 移动构造彻底解决传值返回的性能问题!!!
在这里插入图片描述
这里有点像拷贝的现代写法,但是拷贝的现代写法的本质还是开辟空间!

我们来看一个实际使用中的代吗:杨辉三角

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv(numRows) ;

        for(int i = 0;i < numRows ;i++){

            vv[i].resize(i + 1);
            if(i == 0) {
                vv[i][0] = 1;
            }
            else if(i == 1){
                vv[i][0] = vv[i][1] = 1;
            }
            else{
                vv[i][0] =  vv[i][i] = 1;
                for(int j = 1 ;j < i ;j++){
                    vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
                }
            }
        }
        return vv;
    }
};

int main()
{
	vector<vector<int>> ret;
	ret = Solution.generate(1000);
	return 0; 
}

这段代码是很坑的,我们来分析一下:

  1. 首先杨辉三角中的核心是二维数组:vector<vector<int>>,这就注定其深拷贝的时间复杂度是O(n^2)。
  2. 然后返回值是传值返回?这在调用的时候很明显会创建临时变量,就会进行深拷贝!

这就导致使用的之后传入一个较大数1000,会进行两次深拷贝,每次深拷贝的代价都很大!

所以实践中不能写这样的代码,可以改成:

class Solution {
public:
     void generate(int numRows , vector<vector<int>>& vv) {

    }
};

这样可以避免临时变量的深拷贝。在C++11之前都是这样保证效率,现在当然最好还是使用右值引用+移动赋值来解决。直接进行资源的转移,避免不必要的深拷贝!编译器会自动将返回值识别成右值,进而进行移动辅助!

4.2 区分现代写法与移动语义

我们先来看赋值重载的现代写法:

		string& operator=(const string& s) {
			//现代写法	
			string tmp(s);
			swap(tmp);
			return *this;
		}

现代写法并没有减小成本,本质还是进行了深拷贝,传入的还是左值。没有性能的提升。
再来看移动赋值

		string& operator=(string&& s) {
			swap(s);
			return *this;
		}

因为传入的是这里只进行了一次资源的交换,没有进行深拷贝,对性能有很大的提升!

4.3 实践中落实移动语义

前面我们说过:左值引用和右值引用都是左值。我们可以来验证一下:

using namespace std;
//函数重载
void func(string& s) 
{
	cout << "func(string& s)  --- 左值引用" << endl;
}
void func(string&& s)
{
	cout << "func(string& s)  --- 右值引用" << endl;
}

int main()
{
	string s("111");
	string& s1 = s;
	string&& s2 = string("111");
	func(s1);
	func(s2);
}

我们运行看看:
在这里插入图片描述
左值引用和右值引用都匹配到了左值引用的函数,证明了左值引用和右值引用都是左值。只有这样才能逻辑自洽!
回看移动赋值,在里面to_string的返回值成为string&& s,里面进行了一步swap(s)。我们又知道右值本身是不能被改变的,如果右值引用作为了一个右值的别名是右值的话,那么还是不能进改变,那如何进行资源的转移呢?
在这里插入图片描述
只有右值引用本身是左值,才能实现移动构造和移动赋值中的资源转移!!!
这实现了逻辑的自洽:右值引用的属性如果是右值,那么移动构造和移动赋值要进行转移资源的语法就是矛盾的,右值是不能进行改变的(可以理解为右值自带const属性),右值是有空间存储的,只是语法不允许取地址,但是是可以想办法取到的!
强转和再次引用(先转换为左值)都可以取到

	string&& s2 = string("111");
	string& s3 = s2;
	
	string& s4 = (string&)string("111");

理解上述内容,接下来我们就来看List中如何使用移动语义
我们来看push_back()
C++11中增添了右值引用版本的
在这里插入图片描述
我们来底层中来细细品味:
在这里插入图片描述
我们进行插入string是进行了一次构造和深拷贝,这是list内部push_back()中创建新节点中会创建一个string,然后进行拷贝。里面s1是左值,所以进行的是深拷贝!!!我们换一种方式
在这里插入图片描述
这样在节点创建中创建string进行了一次构造,然后就是移动构造了,因为这里是匿名对象,是一个将亡值,编译器自动匹配了移动构造!所以我们在平时写代码中使用匿名对象会使效率更高!!!

然后我们来自己实现一下:
在这里插入图片描述
首先我们看到我们初始化一个list会进行一次构造和深拷贝,这是因为头结点的缘故。而STL库中的是使用内存池(没有初始化),所以不会打印出来。
我们赶紧来看push_back,如果只有一个左值引用的版本,无论左值还是右值都只能调用这个。我们加一个右值版本:

		void push_back(const T& x = T())
		{
			//找尾
			insert(end(), x);
		}
		void push_back(T&& x)
		{
			//找尾
			insert(end(), x);
		}

我们试试
在这里插入图片描述
哎嘿!?怎么插入右值和插入左值都是深拷贝啊?其实我们看一下push_back中调用的insert就明白了。push_back支持右值了,可是insert还没有右值的版本啊!我们还要补充insert的右值版本

		//右值版本
		void insert(iterator pos = begin(), T&& x = T())
		{
			Node* prev = pos._node->_prev;
			Node* next = pos._node;

			Node* node = new Node(x);

			node->_prev = prev;
			node->_next = next;
			prev->_next = node;
			next->_prev = node;

			_size++;

		}

再来看看:
在这里插入图片描述
哎?还是这样?为什么呢?我们在分析分析insert内部,其中的Node* node = new Node(x);这一步会调用node的构造函数,而node还没有支持右值引用的移动构造啊!所以我们还要为节点增添一个移动构造

		ListNode(T&& x ) :
			_next(nullptr),
			_prev(nullptr),
			_data(x)
		{}

这样移动构造内部会调用_data的构造,而这里_datastring我们已经写好了移动构造,那这次应该就可以了吧?
在这里插入图片描述
答案是 NO!!!
还记得上面说过:左值引用和右值引用都是左值!

第一层的push_back()是可以调用到右值引用的版本,第二次的insert()的第二个参数无论是左值引用还是右值引用,都只会调用左值版本?这要怎么解决呢?我们可以进行move

		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		void insert(iterator pos = begin(), T&& x = T())
		{
			Node* prev = pos._node->_prev;
			Node* next = pos._node;

			Node* node = new Node(move(x));

			node->_prev = prev;
			node->_next = next;
			prev->_next = node;
			next->_prev = node;
			_size++;

		}
		//....
		ListNode(T&& x ) :
			_next(nullptr),
			_prev(nullptr),
			_data(move(x))
		{}

这样经过每一层的move保证了每次传递的都是右值:
在这里插入图片描述
我们就得到了想要的结果!!!

PS:对于内置类型或者Data来中,左值和右值是没有区别的,他们不会涉及到深拷贝的问题!使用涉及深拷贝的自定义类型才会涉及移动构造和移动赋值!

5 万能引用和完美转发

这里在介绍一个新语法:完美转发。它也可以上述做到move的效果,甚至更好!我们最好是使用完美转发!

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

上面的PerfectForward(T&& t)被叫做万能引用(引用折叠),虽然看上去是一个右值引用,但是他可以随机应变:传左值就是左值引用,穿右值就是右值引用。我们验证一下
在这里插入图片描述
可以看到,不是对应匹配的。因为还是那个原因:左值引用和右值引用都是左值!!!而引用折叠虽然会帮助我们实例化出:

void PerfectForward(int&& t)
{
	Fun((int&&)t);
}
void PerfectForward(int& t)
{
	Fun((int&)t);
}
void PerfectForward(const int&& t)
{
	Fun((const int&&)t);
}
void PerfectForward(const int& t)
{
	Fun((const int&)t);
}

但是由于右值引用是左值,所以根本调用不到右值版本!!!
所以这里如果使用move,行不行呢?我们试试:
在这里插入图片描述

move也无法解决这个问题,因为加上move之后,所以实例化的函数也都带有了func(move(t)),所有的都变成了右值,而我们想要的是该左值就是左值,该右值就是右值,所以就有了完美转发 — std::forward<T>(t)

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

完美解决:
在这里插入图片描述
完美转发就做到了:**完美转发在传参的过程中保留对象原生类型属性!**完美转发是在函数模版里面帮助辅助传参的!

  • 实参传左值,就推导成左值引用
  • 实参传右值,就推导成右值引用

完美转发本质上类似进行了一次强转!可以简单的这样理解:

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

上面的list也可以将move该成完美转发了!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

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

相关文章

【C++进阶学习】第七弹——AVL树——树形结构存储数据的经典模块

二叉搜索树&#xff1a;【C进阶学习】第五弹——二叉搜索树——二叉树进阶及set和map的铺垫-CSDN博客 目录 一、AVL树的概念 二、AVL树的原理与实现 AVL树的节点 AVL树的插入 AVL树的旋转 AVL树的打印 AVL树的检查 三、实现AVL树的完整代码 四、总结 前言&#xff1a…

开源模型应用落地-FastAPI-助力模型交互-进阶篇(三)

一、前言 FastAPI 的高级用法可以为开发人员带来许多好处。它能帮助实现更复杂的路由逻辑和参数处理&#xff0c;使应用程序能够处理各种不同的请求场景&#xff0c;提高应用程序的灵活性和可扩展性。 在数据验证和转换方面&#xff0c;高级用法提供了更精细和准确的控制&#…

旧系统的会员信息如何导入新系统?

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物等连锁店使用。 详细介绍请…

浙商之源——龙游商帮:天涯贾客李汝衡

龙游丝绸的历史可以追溯到古代&#xff0c;当地优越的自然环境和气候条件为蚕桑业的发展提供了得天独厚的条件。随着时间的推移&#xff0c;龙游地区的丝绸产业逐渐发展壮大&#xff0c;形成了自己独特的丝绸文化和技艺。 李汝衡&#xff0c;龙游人&#xff0c;其父鹤汀行贾远…

quantlab5.2代码更新,含本周所有策略集和数据集。

原创文章第592篇&#xff0c;专注“AI量化投资、世界运行的规律、个人成长与财富自由"。 一周一度更新代码的日子&#xff0c;我们发布本周积累的策略集——Quantlab5.2&#xff1a; 请大家前往星球下载更新&#xff1a;AI量化实验室——2024量化投资的星辰大海 quantlab…

Harmony 状态管理 @Local 和 @Param

Harmony 状态管理 Local 和 Param Local 背景 Local 是harmony应用开发中的v2版本中 对标**State**的状态管理修饰器&#xff0c;它解决了 State 对状态变量更改的检测混乱的问题&#xff1a; State 修饰的状态变量 可以是组件内部自己定义的State 修饰的状态 也可以由外部父…

实战:Eureka的概念作用以及用法详解

概叙 什么是Eureka&#xff1f; Netflix Eureka 是一款由 Netflix 开源的基于 REST 服务的注册中心&#xff0c;用于提供服务发现功能。Spring Cloud Eureka 是 Spring Cloud Netflix 微服务套件的一部分&#xff0c;基于 Netflix Eureka 进行了二次封装&#xff0c;主要负责…

逻辑门的题目怎么做?

FPGA语法练习——二输入逻辑门&#xff0c;一起来听~~ FPGA语法练习——二输入逻辑门 题目介绍&#xff1a;F学社-全球FPGA技术提升平台 (zzfpga.com)

adb查看网卡信息,并修改网卡mac地址

这种方法修改mac后&#xff0c;关机后会失效! 文章结尾有永久修改mac地址的方法! 1. 查看网卡的信息&#xff0c;以及mac地址&#xff0c;ip地址&#xff0c;子网掩码等 //查看所有网卡信息adb shell ifconfig//MAC地址&#xff1a; HWaddr 5e:2c:e9:58:3e:4f //IP地址&a…

数据库理论基础

1.什么是数据库 1.1数据 描述事物的符号记录&#xff0c; 可以是数字、 文字、图形、图像、声音、语言等&#xff0c;数据有多种形式&#xff0c;它们都可以经过数字化后存入计算机。 1.2数据库 存储数据的仓库&#xff0c;是长期存放在计算机内、有组织、可共享的大量数据…

(三)原生js案例之滚动到底部解锁按钮状态

业务主要是注册页面&#xff0c;有很长的条款需要用户去读&#xff0c;必须确认用户是不是看到全部的条款&#xff0c;这个场景下可以使用 效果 代码实现 必要的css <style>*{padding: 0;margin: 0;}ul{list-style: none;width: 330px;height: 100%;/* height: 200px;…

springboot3——项目部署

springboot的项目开发完了&#xff0c;怎么样把他放到服务器上或者生产环境上让他运行起来跑起来。就要牵扯到项目部署&#xff0c;打包的方式了。 springboot支持jar和war: 打jar包&#xff1a;默认方式&#xff0c;项目开发完打个jar包&#xff0c;通过命令把jar包起起来就…

应用层——HTTP

像我们电脑和手机使用的应用软件就是在应用层写的&#xff0c;当我们的数据需要传输的时候换将数据传递到传输层。 应用层专门给用户提供应用功能&#xff0c;比如HTTP,FTP… 我们程序员写的一个个解决我们实际的问题都在应用层&#xff0c;我们今天来聊一聊HTTP。 协议 协议…

使用 vue-element-plus-admin 框架遇到的问题记录

项目打包遇到的问题&#xff1a; 打包语句&#xff1a;pnpm run build:pro 报错信息&#xff1a; Error: [vite]: Rollup failed to resolve import "E:/workplace_gitee/xxx/node_modules/.pnpm/element-plus2.5.5_vue3.4.15/node_modules/element-plus/es/components…

最新 Docker 下载镜像超时解决方案:Docker proxy

现在Docker换源也下载失败太常见了&#xff0c;至于原因&#xff0c;大家懂得都懂。本文提供一种简洁的方案&#xff0c; 利用 Docker 的http-proxy&#xff0c;代理至本机的 proxy。 文章目录 前言Docker proxy 前言 这里默认你会安装 clash&#xff0c;然后有配置和数据库。…

使用Java -jar运行就jar包时报异常:org.yaml.snakeyaml.error.YAMLException异常

Java运行就 .jar包时出现的 YAMLException 异常 我在本地环境测试时&#xff0c;使用 java -jar 命令运行 Java 可执行 .jar 包时&#xff0c;遇到了 org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length 1 异常&#xff1b;这…

【Docker】Docker-compose 单机容器集群编排工具

目录 一.Docker-compose 概述 1.容器编排管理与传统的容器管理的区别 2.docker-compose 作用 3.docker-compose 本质 4.docker-compose 的三大概念 二.YML文件格式及编写注意事项 1.yml文件是什么 2.yml问价使用注意事项 3.yml文件的基本数据结构 三.Docker-compose …

Flink HA

目录 Flink HA集群规划 环境变量配置 masters配置 flink-conf.yaml配置 测试 Flink HA集群规划 FLink HA集群规划如下&#xff1a; IP地址主机名称Flink角色ZooKeeper角色192.168.128.111bigdata111masterQuorumPeerMain192.168.128.112bigdata112worker、masterQuorumPee…

VS C#类文件自动生成头部注释

VS C#类文件自动生成头部注释&#xff08;以VS2019为例&#xff09; 1、更新位置 E:\VS2019\vs_2019\Common7\IDE\ItemTemplates\CSharp\Code\2052\Class 2、替换Class 原始文件 using System; using System.Collections.Generic; $if$ ($targetframeworkversion$ > 3.5…

Grafana :利用Explore方式实现多条件查询

背景 日志统一推送到Grafana上管理。所以&#xff0c;有了在Grafana上进行日志搜索的需求&#xff0c;而进行日志搜索通常需要多条件组合。 解决方案 通过Grafana的Explore的方式实现多条件查询。 直接看操作步骤&#xff1a; 在主页搜索框中输入“Explore” 进入这个界面…