左值引用和右值引用
我们常见的&引用,即左值引用,不带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的浅拷贝:
-
带const的左值引用虽然可以引用右值对象,但无法进行修改,也就解决不了double free的问题
-
直接的左值引用虽然可以更改对象,但无法引用右值对象,从根本上就无法进行对右值对象的浅拷贝
现在我们需要一个可以引用右值的同时,还能对其进行修改的方案,来实现对右值对象的浅拷贝——这正是右值引用登场的时候了
使用右值引用来实现浅拷贝构造函数:
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;
}