C++11右值引用和移动构造函数示例

左值引用和右值引用

我们常见的&引用,即左值引用,不带const的左值引用只能引用可以取地址的变量,比如说:

#include <iostream>

using namespace std;

int get_self(int &x)
{
    return x;
}
int main()
{
    int a = 3; 
    int b = 5;
    int &c = a; //成功
    int &d = 3; //错误
    int &e = a + b; //错误
    
    get_self(a); //正确
    get_self(3); //错误
    get_self(a + b); //错误 
    
    int &f = get_self(a); //错误
    
	return 0;
}

对于上述这些不能被&直接引用的值,或者说不能取地址的值,我们统称为右值(因为其放不到等号左边)。

有没有什么办法可以引用右值呢?——显然,加const即可。

然而,带const的左值引用虽然可以引用右值,却不能对其进行更改:

const int &g = 3; //正确
const int &h = a + b; //正确
g = 10; //错误

那么,有没有什么办法能够可以引用右值的同时,还能对其进行修改呢?——右值引用恰好可以解决这个问题。

右值引用的写法是&&,如:

int &&j = 3; //成功
int &&k = a + b; //成功
int &&l = get_self(a); //成功
j = 10; //成功

到这我们总结出右值引用的一个重要特性:可以引用右值的同时,还能对其进行修改

了解了左值引用和右值引用,我们通过如下一个示例来说明右值引用一种用途,以及移动构造函数:

移动构造函数

考虑一个数据包类,其包含一个指针成员变量data,三个成员函数(构造、析构和拷贝构造函数):

#include <iostream>
#include <cstring>

#define PKT_SZ 4096
using namespace std;

class Packet
{
public:
	char *data;
	Packet() //构造函数
	{
		data = new char[PKT_SZ];
	}
	~Packet() //析构函数
	{
		if(data != nullptr) 
			delete[] data;
	}
	Packet(const Packet &src) //拷贝构造函数
	{
		cout << "copy constructor" << endl;
		data = new char[PKT_SZ];
		memcpy(data,src.data,PKT_SZ); //深拷贝
	}
};	

现在我们想实现一个加密数据包的功能,写了一个能简单加密后生成新数据包的函数,如下:

Packet encrpyt_packet(Packet &src)
{
	Packet pkt;
	for(int i = 0;src.data[i] != '\0';i++)
		pkt.data[i] = src.data[i] + 1; //加密方式:把原数据每个字符+1,生成新的数据包
    
	return pkt;
}

我们测试一下:

int main(int argc, const char *argv[])
{
	Packet pkt1;
	strcpy(pkt1.data,"hello world");
	cout << pkt1.data << endl;

	Packet pkt2 = encrpyt_packet(pkt1);
	cout << pkt2.data << endl;

	return 0;
}

编译运行(-fno-elide-constructors参数用于禁止对构造函数的自动优化):

请添加图片描述
会发现出现了两次copy constructor,即两次拷贝构造函数,分别是:

(1) encrpyt_packet()函数最后return pkt时,会把pkt作为参数构造(拷贝构造)临时对象,然后再返回

(2) Packet pkt2 = encrpyt_packet(pkt1)这一步,会把(1)返回的临时对象作为参数构造(拷贝构造)pkt2

要知道,我们最开始定义的data数组大小为4096,而这两次拷贝构造都是深拷贝,所以会浪费很多资源,尤其是如果我们data数组大小更大,比方说为400M之类时,深拷贝造成的资源浪费就会更多。

出于效率原因,自然会去思考该如何进行优化。

我们想:如果是因为深拷贝造成了资源浪费,那么改成浅拷贝应当可以解决资源浪费的问题

原来的拷贝构造函数是深拷贝:

Packet(const Packet &src) //拷贝构造函数
{
	cout << "copy constructor" << endl;
	data = new char[PKT_SZ];
	memcpy(data,src.data,PKT_SZ); //深拷贝
}

我们改成浅拷贝:

Packet(const Packet &src) //拷贝构造函数
{
	cout << "shallow copy constructor" << endl;
	data = src.data; //浅拷贝
}

编译看一下:
请添加图片描述
出错,提示出现了“double free”错误,也就是对同一资源进行了两次释放

显然,问题出在浅拷贝的构造函数上,因为我们直接把原数据包的data指针的值拷贝给了新数据包,导致原数据包,新数据包,有两个data指针指向了同一块内存,自然再析构时就会对同一块内存进行两次delete,从而产生double free的错误

那么,如果我们在浅拷贝后,把原数据包的指针置空,只留下新数据包指向数据内存,应该就不会发生double free错误了,例如:

Packet(const Packet &src) //拷贝构造函数
{
	cout << "shallow copy constructor" << endl;
	data = src.data; //浅拷贝
    src.data = nullptr; //置空原数据包的指针
}

但这么做的话,编译依旧会出错,因为src是一个const引用,是不能进行更改的。

那如果去掉const呢,例如:

Packet(Packet &src) //拷贝构造函数
{
	cout << "shallow copy constructor" << endl;
	data = src.data; //浅拷贝
    src.data = nullptr; //置空原数据包的指针
}

编译看一下:
请添加图片描述出错,错误说,不能将Packet&类型的左值引用与Packet类型的右值相绑定。

看到报错的Packet pkt2 = encrpyt_packet(pkt1);这一行,encrpyt_packet(pkt1)返回的是一个不能取地址的临时对象,还记得最初介绍的右值吗,这种不能取地址的临时对象,正是一个右值。

而该行生成pkt2时会自动调用拷贝构造函数,也就是Packet(Packet &src),但其形参是一个左值引用,我们知道,不带const的左值引用是无法引用右值对象的,因此编译器自然会报错

至此我们的问题可以总结为——如何实现对右值对象无double free的浅拷贝:

  1. 带const的左值引用虽然可以引用右值对象,但无法进行修改,也就解决不了double free的问题

  2. 直接的左值引用虽然可以更改对象,但无法引用右值对象,从根本上就无法进行对右值对象的浅拷贝

现在我们需要一个可以引用右值的同时,还能对其进行修改的方案,来实现对右值对象的浅拷贝——这正是右值引用登场的时候了

使用右值引用来实现浅拷贝构造函数:

Packet(Packet &&src) //移动构造函数
{
	cout << "move constructor" << endl;
	data = src.data; //浅拷贝
	src.data = nullptr; //置空原数据包的指针
}

编译运行一下:
请添加图片描述

看到两次move constructor,说明原先的两次深拷贝已经成功替换成浅拷贝,并且没有再报double free的错误,至此,我们成功利用右值引用实现了性能的优化

这种浅拷贝的构造函数,由于最后原对象数据指针置空,也就说原对象已经没有资源了,相当于其资源被移动到了新对象上,基于这种特性,这种构造函数被统称为移动构造函数

注意:移动构造函数和原来的拷贝构造函数并不冲突,二者可以构成重载,在接收右值时调用移动构造函数,接收左值时调用拷贝构造函数

本示例最终代码如下:

#include <iostream>
#include <cstring>

#define PKT_SZ 4096
using namespace std;

class Packet
{
public:
	char *data;
	Packet()
	{
		data = new char[PKT_SZ];
	}
	~Packet()
	{
		if(data != nullptr) 
			delete[] data;
	}
	Packet(const Packet &src)
	{
		cout << "copy constructor" << endl;
		data = new char[PKT_SZ];
		memcpy(data,src.data,PKT_SZ);
	}
	Packet(Packet &&src)
	{
		cout << "move constructor" <<endl;
		data = src.data;
		src.data = nullptr;
	}
	
};

Packet encrpyt_packet(Packet &src)
{
	Packet pkt;
	for(int i = 0;src.data[i] != '\0';i++)
	{
		pkt.data[i] = src.data[i] + 1;
	}
	return pkt;
}

int main(int argc, const char *argv[])
{
	Packet pkt1;
	strcpy(pkt1.data,"hello world");
	cout << pkt1.data << endl;

	Packet pkt2 = encrpyt_packet(pkt1);
	cout << pkt2.data << endl;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值