右值引用
在 C++语言中,按照能否放在赋值操作符“=”的左边或者右边,数值或变量被分成左值或者右值。比如某个数字常量,就只能放在等号右边给其它左值赋值而无法放在等号左边被赋值。 C++中的右值主要指的是数字常量(例如, 1、 2.3 等)和匿名变量(例如,函数的返回值变量、构造函数的返回的对象等)。
定义
右值引用就是与这些右值相关联的引用。
在 C++中,可以通过将“&&”
符号放到数据类型的后面来定义一个相应类型的右值引用,相对应地,原来只使用一个“&”
符号定义的引用被称为左值引用,简称引用。
// 定义一个 int 类型的变量,这个变量可以放在等号左边被赋值,
// 所以是一个左值
int nInt = 1;
// 定义一个左值引用,将它指向一个左值
int& lrefInt = nInt;
// 定义一个右值引用,将它指向一个直接使用构造函数创建的右值 int(0)
int&& rrefInt =int(0);
// 显然我们无法将它放在等号左边对它赋值
int(0) = 1; // 错误
关联变量
左值引用和右值引用对变量的关联是一一对应专属的,换句话说,也就是左值引用只能关联到左值,而右值引用也只能关联到右值,否则,将会产生编译错误。
// 正确:左值引用 lrefInt1 关联到左值变量 nInt
int& lrefInt1 = nInt;
// 错误:左值引用 lrefInt2 不可以关联到右值 int(0)
int& lrefInt2 = int(0);
// 正确:右值引用 rrefInt1 关联到右值 int(0)
int&& rrefInt1 = int(0);
// 错误:右值引用 rrefInt2 不可以关联到左值 nInt
int&& rrefInt2 = nInt;
使用
关联完成之后,左值引用和右值引用的使用就没有什么区别了,它们都可以当成普通数据变量进行左右值的操作。例如:
// 对右值引用 rrefInt1 赋值
rrefInt1 = 1;
// 利用右值引用对左值引用赋值
lrefInt1 = rrefInt1;
右值引用是如何提高性能的
最常见的右值就是函数(包括普通函数和构造函数)的返回值。当一个函数调用完成
后,这些没有变量名的返回值通常会被赋值给等号左边的一个左值变量,在没有右值引用的时代这其实是一个极其消耗性能浪费资源的过程。首先,需要释放左值变量原有的内存资源,然后根据返回值的大小重新申请内存资源,接着才是将返回值的数据复制到左值变量新申请的内存中,最后还要释放掉返回值的内存资源。经过这样四个步骤,才能最终完成一个函数返回值的赋值操作。
使用前
例如,用 CreateBlock()函数创建一个用于管理内存的 MemoryBlock对象,并将其保存到另外一个 MemoryBlock 类型变量中:
#include <iostream>
#include <cstring> // 为了使用内存复制函数 memcpy()
using namespace std;
// 用于管理内存的类
class MemoryBlock
{
public:
// 构造函数,根据参数申请相应大小的内存资源
MemoryBlock(const unsigned int nSize)
{
cout<<"创建对象,申请内存资源"<<nSize<<"字节"<<endl;
m_nSize = nSize;
m_pData = new char[nSize];
}
// 析构函数,释放管理的内存资源
~MemoryBlock()
{
cout<<"销毁对象";
if(0 != m_nSize) // 如果拥有内存资源
{
cout<<", 释放内存资源"<<m_nSize<<"字节";
delete[] m_pData; // 释放内存资源
m_nSize = 0;
}
cout<<endl;
}
// 赋值操作符,完成对象的复制
// 这里的参数是一个左值引用
MemoryBlock& operator = (const MemoryBlock& other)
{
// 判断是否自己给自己赋值
if(this == &other)
return *this;
// 第一步,释放已有内存资源
cout<<"释放已有内存资源"<<m_nSize<<"字节"<<endl;
delete[] m_pData;
// 第二步,根据赋值对象的大小重新申请内存资源
m_nSize = other.GetSize();
cout<<"重新申请内存资源"<<m_nSize<<"字节"<<endl;
m_pData = new char[m_nSize];
// 第三步,复制数据
cout<<"复制数据"<<m_nSize<<"字节"<<endl;
memcpy(m_pData,other.GetData(),m_nSize);
return *this;
}
public:
// 获取相关数据的成员函数
unsigned int GetSize() const
{
return m_nSize;
}
char* GetData() const
{
return m_pData;
}
private:
unsigned int m_nSize; // 内存块的大小
char* m_pData; // 指向内存块的指针
};
// 根据大小创建相应的 MemoryBlock 对象
MemoryBlock CreateBlock(const unsigned int nSize)
{
// 创建相应大小的对象
MemoryBlock mem(nSize);
// 给内存中填满字符'A'
char* p = mem.GetData();
memset(mem.GetData(),'A',mem.GetSize());//p[0] = 'A';
// 返回创建的对象
return mem;
}
int main()
{
// 用于保存函数返回值的 block 变量
MemoryBlock block(256);
// 用函数创建特定大小的 MemoryBlock 对象
// 并赋值给 block 变量
block = CreateBlock(1024);
cout<<"创建的对象大小是"
<<block.GetSize()<<"字节"<<endl;
return 0;
}
在这段代码中,我们利用 CreateBlock()函数创建了一个特定大小的 MemoryBlock 对象并将其保存到本地的一个 block 变量中。从表面上看,这是一个非常简单的动作,可是在背后,程序是经过千辛万苦的四个步骤才完成了这一“简单”动作。从程序的输出中,我们可以清楚地看到这四个步骤:
创建对象,申请内存资源 256 字节
创建对象,申请内存资源 1024 字节
释放已有内存资源 256 字节 <-第一步
重新申请内存资源 1024 字节 <-第二步
复制数据 1024 字节 <-第三步
销毁对象,释放内存资源 1024 字节 <-第四步
创建的对象大小是 1024 字节
销毁对象,释放内存资源 1024 字节
使用后
函数的返回值其实是一个右值,通过在 MemoryBlock 类当中提供可以接受其右值引用为参数的移动构造函数和赋值操作符,我们就可以直接利用函数返回值这个右值作为我们的 block 变量:
// …
// 用于管理内存的类
class MemoryBlock
{
// …
public:
// 可以接收右值引用为参数的移动构造函数
MemoryBlock(MemoryBlock&& other)
{
cout<<"移动资源"<<other.m_nSize<<"字节"<<endl;
// 将目标对象的内存资源指针直接指向源对象的内存资源
// 表示将源对象内存资源的管理权移交给目标对象
m_pData = other.m_pData;
m_nSize = other.m_nSize; // 复制相应的内存块大小
// 将源对象的内存资源指针设置为 nullptr
// 表示这块内存资源已经归目标对象所有
// 源对象不再拥有其管理权
other.m_pData = nullptr;
other.m_nSize = 0; // 内存块大小设置为 0
}
// 可以接收右值引用为参数的赋值操作符
MemoryBlock& operator = (MemoryBlock&& other)
{
// 第一步,释放已有内存资源
cout<<"释放已有资源"<<m_nSize<<"字节"<<endl;
delete[] m_pData;
// 第二步,移动资源,也就是移交内存资源的管理权
cout<<"移动资源"<<other.m_nSize<<"字节"<<endl;
m_pData = other.m_pData;
m_nSize = other.m_nSize;
other.m_pData = nullptr;
other.m_nSize = 0;
return *this;
}
// …
}
这里的移动构造函数和赋值操作符都以一个右值引用为参数。这就意味着这个参数所关联的右值对象在函数调用完成后就会被销毁,其管理的内存资源也会被释放。既然这个右值对象即将被销毁,而我们同时又要创建或者复制一个与之完全相同的对象,那么很自然地,我们会想到“废物再利用”,直接用这个即将被销毁的右值对象作为我们想要创建或复制的目标对象。内存资源依旧是那块内存资源,只不过其管理者由原来的作为参数的右值对象,换成了我们想要创建或复制的目标对象。
从程序的输出中,我们也可以看到这个“移交”过程:
创建对象,申请内存资源 256 字节
创建对象,申请内存资源 1024 字节
释放已有资源 256 字节 <-第一步,释放已有内存资源
移动资源 1024 字节 <-第二步,移交内存资源的管理权
销毁对象
创建的对象大小是 1024 字节
销毁对象,释放内存资源 1024 字节
在这个过程中,没有内存资源的重新申请,也没有数据的复制,完全就像一场和平友好的内存资源管理权移交仪式,只是简单地让目标对象的内存指针指向右值对象的内存资源,从而将内存资源的管理权从右值对象移交( move)到了目标对象。
为了与传统的可以接收左值引用( &)为参数的构造函数和赋值操作符相区别,这里的可以接收右值引用( &&)为参数的构造函数也被称为移动构造函数,相应地赋值操作符也被称为移动赋值操作符。
总结
为类提供可以接收右值引用为参数的移动构造函数和移动赋值操作符,是提高保存复制函数返回值这一常见动作性能的一个有效途径。
- 对于标准库中的内容而言,比如 vector 容器,已经使用右值引用进行了改写。
- 对于我们自己创建的类,如果它会作为函数返回值被赋值给另外的左值对象时,为它提供移动构造函数和移动赋值操作符,则可以将函数返回值这个原先被“废弃”的右值再次利用起来,在一定程度上提高程序的性能