C++11右值引用:移动语义与右值引用

什么是右值(right value)

  1. 所有的具名变量或者对象都是左值,而右值不具名字,也不会开辟地址存储,比如x=y+z中,y+z的计算结果是一个右值

什么是引用

  1. 我们通常说的引用就是左值引用
  2. 左值引用的是有具体对象的,因为左值引用就是多个变量共享一段内存,换句话说就是别名
  3. 右值引用的是临时数据
    • 矛盾点:引用是指向一段空间的变量,临时变量是没有地址的,比如取地址&1这样的操作是不允许的,那么右值与引用怎么联系起来的?
    • 千万要注意,存储右值引用的变量是一个左值,并非临时对象,

右值引用

  • 符号&&,只能引用右值,不能引用左值
  • 右值引用是什么:笔者在看了各种解释之后,总结出右值引用的几个点
    • 右值并不是从来都不占用内存,而是会存在一个内存分配->变量析构->内存删除的过程
    • 右值引用其实就是省略了临时变量内存删除的过程。
    • 右值引用核心在于移动语义
  • 乍一看,这不就是左值赋值,就和int a = 2;一样,把右值赋予左值了吗?对于单纯的数字来说,可能真的是这样,但是对于一个对象来说,=赋值久相当于拷贝构造,和右值引用中的移动构造是两种实现,那么移动构造是什么呢?首先要知道移动语义

移动语义

  • 移动语义其实就是顾名思义,将一个对象的内容移动到另一个对象中
  • 我们可以类比一下,要将文件从A文件夹移动到B文件夹中,可以复制-粘贴-删除,也可以是剪切-粘贴,无疑,第二种方法能够更快速一些。
  • 按照笔者的理解,移动语义是为了提高临时变量拷贝的效率而出现的。
  • 曾经的对象拷贝往往就是为临时变量开辟一段内存,复制给目标对象,释放临时变量,可见这个临时变量的开辟和删除是没有意义的。
  • 移动语义希望能够实现让目标变量接替临时变量的内存空间,这种做法,如图所示,图中实线变成虚线表示对象被析构了:
    在这里插入图片描述

举例拷贝构造和移动构造

  • 首先我们介绍一下,如果没有移动语义,实现自动释放类中指针的做法,很简单,就是在析构函数中添加delete
	class HasPtrMem{
		public:
			int *d;
			HasPtrMem: d(new int(0)) {}
			~HasPtrMem(){delete d;}
	};
  • 这种实现会有什么问题? 浅拷贝会产生悬挂指针,比如有这么个main函数
	int main(){
		HasPtrMem a;
		HasPtrMem b(a);
	}//报错  
  • 这个函数中,创建了一个对象a,以及一个拷贝对象b,那么在结束的时候,会发现,a先delete了,b指向的是a删除的空间,当b调用析构函数的时候,就会出现经典的悬挂指针问题
  • 传统的做法是重写拷贝构造函数,实现深拷贝,让b也有属于自己的内存空间
	class HasPtrMem{
		public:
			int *d;
			HasPtrMem: d(new int(0)) {}
			HasPtrMem(HasPtrMem &h):d(new int(*h.d)) {}
			~HasPtrMem(){delete d;}
	};
  • 深拷贝的缺点
    • 缺点非常明显,就是当指针数据非常庞大的时候,中间涉及到的开辟,复制与最后的删除,都会带来空间和时间上大量的开销,尤其是有时候拷贝的意义不大,会导致大开销与小回报,比如get()函数
	class HasPtrMem{
		public:
			int *d;
			HasPtrMem: d(new int(0)) {}
			HasPtrMem(HasPtrMem &h):d(new int(*h.d)) {}
			~HasPtrMem(){delete d;}
	};
	HasPtrMem GetTmp(){return HasPtrMem();}
	int main (){
		HasPtrMem a = GetTmp();
	}
  • 如何改进这种无意义的拷贝呢?C++11给出了移动构造的解法,这一段一定要在自己编译器上输出结果,光看花花绿绿的代码很难看进去的,下面这段代码需要加上-fno-elide-constructors,从而让编译器不要省略构造函数的内容。
#include<bits/stdc++.h>
using namespace std;
class HasPtrMem{
public:
	 HasPtrMem():d(new int(3)){
		  cout << "Construct:" << ++n_cstr << endl;
	 }
	
	 HasPtrMem(HasPtrMem &h):d(new int(*h.d)){
		  cout << "Copy Construct:" << ++n_cptr << endl;
	 }
	 
	 HasPtrMem(HasPtrMem &&h):d(h.d){
		  h.d = nullptr;
		  cout << "Move Construct:" << ++n_mvtr << endl;
	 }
	 
	 ~HasPtrMem(){
		  delete d;
		  cout << "Destruct: " << ++n_dstr << endl;
	 }
	 int *d;
	 static int n_cstr;
	 static int n_cptr;
	 static int n_mvtr;
	 static int n_dstr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
int HasPtrMem::n_dstr = 0;

HasPtrMem GetTmp(){
	 HasPtrMem h;
	 cout << "Resource from: " << __func__ << ": " << hex << h.d << endl;
	 return h;
}

int main(){
	 HasPtrMem c(GetTmp());
	 HasPtrMem a = GetTmp();
	 cout << "Resource from: " << __func__ << ": " << hex << a.d << endl;
	 system("pause");
}
//输出
/*
Construct: 1
Resource from GetTmp: 0x603010
Destruct: 1
Move construct: 1
Destruct: 2
Move construct: 2
Resource from main: 0x603010
Destruct: 3
*/
  • 这段代码中,在GetTmp()调用了一次构造函数,在return h的时候调用了一次移动构造,在赋值给a的时候,又调用了一次移动构造,因此出现三次调用,以及三次析构,有趣的是,两次输出的变量地址实一样的,说明并没有新开辟空间,只是新变量接替了旧空间。

总结

  • 在构造时使得指针对象指向临时变量的堆内存资源

  • 保证临时对象不释放所指向的堆内存

  • 总的来说,其实就是浅拷贝临时变量,然后临时变量析构的时候不会释放内存。

  • 右值引用有什么用?

    • 给右值续命(,让右值的生命周期变为变量的生命周期,换句话说其实就是右指变左值,这一点和变量赋值似乎并无不同。
    • 与C++11其他新功能相关:智能指针自动删除空间
    • 在迁移数据的时候,实现直接移动,而不是开辟-拷贝-粘贴-删除三个过程。
      • 举个例子,比如在函数传递的时候,如果形参的是左值引用,那么在传递过程中,发生了拷贝,因此需要调用构造函数进行构造,但是如果形参是右值引用(或者常左值引用),那么该对象不会重新构造,也就是说,右值形参对象不需要调用构造函数
    • 右值引用只能引用右值,如下图所示,如果绑定了左值,将会产生错误,这有利于编译器区分传入参数是左值还是右值

容易混淆的点

  • std::move(lvalue), 将左值强制转换为右值
    • 该函数并非移动的意思,但是设计它最主要的目的就是为了配合移动语义
    • std::move(lvalue) 在转换之后,lvalue对象就会失效
    • move通常用于转换堆内存,fd等资源,因为这些内存空间通常不需要拷贝构造。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值