移动语义

c++11中引入了移动语义,可以避免无谓的复制,提高程序性能。让我们一起学习“移动语义

c++11 测试右值 临时对象的构造 编译器会自动优化导致有些流程未打印,下面先介绍一下RVO。

RVO(Return  Value Optimization)是一种编译器优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象返回,那么这个临时对象会消耗一个构造函数(Constructor)、一个拷贝构造函数(Copy Constructor)以及一个析构函数(Destructor)的调用的代价,RVO的目的就是消除为保存返回值而创建的临时对象,这样就可以将成本降低到一个构造函数的代价。更具体的请自行查阅资料。

言归正传,直奔主题,移动构造函数中为指针成员分配新的内存再进行内容拷贝的做法在C++编程中几乎被视为不可违背的,不过在一些时候,我么确实不需要这样的拷贝构造语义。我们看看下面的例子。

class HasPtrMem
{
public:
	HasPtrMem() : d(new int(0))
	{
		cout << "Construct:" << ++n_cstr << endl;
	};
	HasPtrMem(const HasPtrMem&h) : d(new int(*h.d))
	{
		cout << "Copy Construct:" << ++n_cptr << endl;
	}
	~HasPtrMem()
	{
		delete d;
		cout << "Destruct:" << ++n_dstr << endl;
	}
	int *d;
	static int n_cstr;
	static int n_cptr;
	static int n_dstr;
};

int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;

HasPtrMem GetTemp()
{
	HasPtrMem h;
	return h;
}

int main()
{
	HasPtrMem a = GetTemp();
}

在上例中,我们声明了一个返回一个HasPtrMem变量的函数。为了记录构造函数、拷贝构造函数,以及析构函数调用的次数,我们使用了一些静态变量。在main函数中,我们简单地声明了一个HasPtrMem的变量a,要求它使用GetTemp的返回值进行初始化。

编译程序,我们看到下面的输出:

优化前:

Construct:1
Copy Construct:1
Destruct:1
Copy Construct:2
Destruct:2
Destruct:3

 优化后:

Construct:1
Copy Construct:1
Destruct:1
Destruct:2

这里主要分析优化前的结果,这里构造函数被调用了一次,这是在GetTemp函数中HasPtrMem()表达式显式地调用了构造函数而打印出来的。而拷贝构造函数则被调用了两次。第一次是从GetTemp函数中HasPtrMem()生成的变量上拷贝构造出一个临时值,以用作GetTemp的返回值,而另外一次则是由临时值构造出main中变量a调用的。对应地,析构函数也就被调用3次。这个过程如下图。

最让人感到不安就是拷贝构造函数的调用。在我们的例子里,类HasPtrMem只有一个int类型的指针。而如果HasPtrMem的指针指向非常大的堆内存数据的话,那么拷贝构造的过程就会非常昂贵。

让我们把目光再次聚集在临时对象上,即在main函数的部分。按照C++的语义,临时对象将在语句结束后被析构,会释放它所包含的堆内存资源。而a在拷贝构造的时候,又会被分配堆内存。这样的一去一来似乎并没有多大的意义,那么我们是否可以在临时对象构造a的时候不分配内存,即不适用所谓的拷贝语义呢?

在C++11中,答案是肯定的。我们可以看看下图。

上半部分可以看到从临时变量中拷贝构造变量a的做法,即在拷贝时分配新的堆内存,并从临时对象的堆内存中拷贝内容。而构造完成后,临时对象将析构,因此其拥有的堆内存资源会被析构函数释放。而下半部分则是一种“新”方法,该方法在构造时使得a.d指向临时对象的内内存资源。同时我们保证临时对象不释放所指向的堆内存,那么在构造完成后,临时对象对象被析构,a就从中“偷”到了临时对象所拥有的堆内存资源。

在C++11中,这样的“偷走”临时变量中资源的构造函数,就被称为“移动构造函数”,而这样的“偷”的行为,则称之为“移动语义”。我们看看如何实现这种移动语义,如下图。

class HasPtrMem
{
public:
	HasPtrMem() : d(new int(0))
	{
		cout << "Construct:" << ++n_cstr << endl;
	};
	HasPtrMem(const 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_cptr = 0;
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_mvtr = 0;
int HasPtrMem::n_dstr = 0;

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

int main()
{
	HasPtrMem a= GetTemp();
	cout << "Resource from" << __func__ << ":" << hex << a.d << endl;
	return 0;
}

相比于之前的代码,上例HasPtrMem类多了一个构造函数HasPtrMem(HasPtrMem&&),这个就是我们所谓的移动构造函数。与拷贝构造函数不同的是,移动构造函数接受一个所谓的“右值引用”的参数。可以看到,移动构造函数使用了参数h的成员d初始化了本对象的成员d(而不是像拷贝构造函数一样需要分配内存,然后将内容依次拷贝到新分配的内存中),而h的成员d随后被置为指针空值nullptr。这就完成了移动构造的全过程。

这里所谓的“偷”对内存,就是指将本对象d指向h.d所指的内存这一条语句,相应地,我们还将h的成员d置为指针空值。这其实也是我们“偷”内存是必须做的。不然,在移动构造完成之后,临时对象会立即被析构。如果不改变h.d的话,则临时对象会析构掉本是我们“偷”来得堆内存。这样一来,就造成了悬挂指针。

为了看看移动构造函数的效果,我们让GetTemp和main函数分别打印h和变量a中的指针h.d和a.d,我们的结果如下:

优化前:

Construct:1
Resource fromGetTemp:0000015A20CF1FE0
Move Construct:1
Destruct:1
Move Construct:2
Destruct:2
Resource frommain:0000015A20CF1FE0
Destruct:3

优化后:

Construct:1
Resource fromGetTemp:0000015A20CF1FE0
Move Construct:1
Destruct:1
Resource frommain:0000015A20CF1FE0
Destruct:2

参考:深入理解C++11

  • 17
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值