右值:
C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。在介绍右值引用类型之前先要了解什么是左值和右值:
1. lvalue 是locator value的缩写,rvalue 是 read value的缩写
2. 左值是指存储在内存中、有明确存储地址(可取地址)的数据;
3. 右值是指可以提供数据值的数据(不可取地址);
通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。
下面的一段代码讲述了左值引用和右值引用的初始化方式:
#include<iostream>
using namespace std;
int main()
{
// 左值
int num = 9;
// 左值引用
int& a = num;
// 右值引用
int&& b = 8;
//常量右值引用
const int&& d = 6;
// 常量左值引用
const int& c = num;
const int& f = b;
const int& g = d;
const int& h = a;
// 由此可以看出常量左值引用是万能的引用类型
// 可以用同类型的各种引用来初始化的左值引用
#if 0
const int&& e = b;// error
int&& f = b;// error
#endif
return 0;
}
右值引用:
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
举一个例子:
A a = 临时;
要基于这个临时对象给a对象初始化,假设这个a对象是非常庞大的。将这个临时对象构建出来需要时间,将数据拷贝给a对象也需要时间,然后就被析构了。也就是说这个临时对象从创建到被销毁存活的时间是非常短的,虽然存活时间短,但是耗费了大量的系统资源。这时就有一种方法让这个临时对象不销毁,直接使用他。这个时候就需要使用右值引用了,延长存活周期。这时候这个a对象就不是拷贝临时对象了,而是引用了这个临时对象。
关于右值引用的使用,参考代码如下:
#include<iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct:my name is jerry" << '\n';
printf("m_num 地址:%p\n", m_num);
}
// 拷贝构造
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct:my name is tom" << '\n';
}
~Test()
{
cout << "destruct Test class ... " << '\n';
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
// t对象会被getObj返回的对象实例化,但是函数中的对象t就会自动析构
// 等主函数中的t对象生命周期结束的时候,t对象也会自动析构
Test t = getObj();// 拷贝构造
return 0;
}
上述代码的运行结果为:
construct: my name is jerry
m_num 地址:0x7ffca2c02790
copy construct: my name is tom
destruct Test class...
destruct Test class...
输出结果与上述代码分析的一样,这就验证了我们的分析是正确的。
但是现在的编译器可能会进一步优化,使输出结果变为:
construct:my name is jerry
m_num 地址:000000ABB6DDFA28
destruct Test class ...
优化的部分:getObj()调用Test t的默认构造函数,return t隐式调用复制构造函数创建一个临时对象(此步骤被编译器优化了),main中Test t = getObj()又调用了复制构造函数
使用一个右值引用的构造函数来优化,这个右值引用的构造函数也称为移动构造函数。
下面是添加移动构造函数的示例:
#include<iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct:my name is jerry" << endl;
printf("m_num 地址:%p\n", &m_num);
}
// 拷贝构造
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct:my name is tom" << endl;
}
// 移动构造函数的作用:复用其他对象中的资源(堆内存)
// 因为这个堆内存已经在另一个对象中被申请出来了,并且已经被初始化了
// 所以就没有必要在新的对象中再去申请新的资源了,并且还要对这个新对象做相同的初始化
Test(Test&& a) : m_num(a.m_num)// 让当前对象的指针指向a对象的m_num指针
{
// 所以通过移动构造做的是一个浅拷贝
// 不能让a对象析构的时候将这个块内存析构掉了
// 让指针指向空就好了
// 这样当前对象就可以继续使用a对象中的m_num这个指针了
a.m_num = nullptr;
cout << "move construct ... " << endl;
}
~Test()
{
cout << "destruct Test class ... " << endl;
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
// t对象会被getObj返回的对象实例化,但是函数中的对象t就会自动析构
// 等主函数中的t对象生命周期结束的时候,t对象也会自动析构
Test t = getObj();// 拷贝构造
// getObj()调用Test t的默认构造函数,
// return t隐式调用复制构造函数创建一个临时对象(此步骤被编译器优化了),
// main中Test t = getObj()又调用了复制构造函数
return 0;
}
上述代码的运行结果为:
construct:my name is jerry
m_num 地址:0x7ffcb9c02790
move construct ...
destruct Test class ...
destruct Test class ...
注意: 这个移动构造函数调用的并不是getObj()对象t中的所有的资源,而是某一部分资源(堆内存资源)。这样就没有必要拷贝了。
接下来来思考:为什么添加了移动构造后,拷贝构造就不调用了呢?
在进行赋值操作的时候,编译器就会判断,右边的这个是不是临时对象。如果是临时对象就会优先调用移动构造。若不是临时对象,那么调用的还是拷贝构造函数。
临时对象也可以用右值引用来接收:
int main()
{
// t对象会被getObj返回的对象实例化,但是函数中的对象t就会自动析构
// 等主函数中的t对象生命周期结束的时候,t对象也会自动析构
Test t = getObj();// 拷贝构造
// getObj()调用Test t的默认构造函数,
// return t隐式调用复制构造函数创建一个临时对象(此步骤被编译器优化了),
// main中Test t = getObj()又调用了复制构造函数
cout << endl;
Test&& t1 = getObj();
printf("m_num 地址:%p\n", &t1.m_num);
return 0;
}
输出结果为:
construct:my name is jerry
m_num 地址:000000C0172FF648
copy construct:my name is tom
destruct Test class ...
destruct Test class ...
construct:my name is jerry
m_num 地址:000000C0172FF688
move construct ...
m_num 地址:000000C0172FF688
destruct Test class ...
由上述输出结果可以看出,移动构造就是用的同一个地址。
使用右值引用续命:
#include<iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct:my name is jerry" << endl;
printf("m_num 地址:%p\n", &m_num);
}
// 拷贝构造
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct:my name is tom" << endl;
}
~Test()
{
cout << "destruct Test class ... " << endl;
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
Test getObj1()
{
return Test();// 返回临时的匿名对象
}
int main()
{
// t对象会被getObj返回的对象实例化,但是函数中的对象t就会自动析构
// 等主函数中的t对象生命周期结束的时候,t对象也会自动析构
Test t = getObj();// 拷贝构造
// getObj()调用Test t的默认构造函数,
// return t隐式调用复制构造函数创建一个临时对象(此步骤被编译器优化了),
// main中Test t = getObj()又调用了复制构造函数
cout << endl;
Test&& t1 = getObj();
printf("m_num 地址:%p\n", &t1.m_num);
// 如果没有移动构造函数,使用右值引用初始化的要求要更高一些
// 要求右侧是一个临时的不能取地址的对象
cout << endl;
Test&& t2 = getObj1();
printf("m_num 地址:%p\n", &t2.m_num);
return 0;
}
输出结果为:
使用右值引用t2给这个匿名对象续命,因为输出的地址一致。我们并没有创建t2,而是使用了即将释放的这个对象里面的所有的资源。
注意:移动构造函数中是复用了即将释放的对象里面的部分资源(堆内存),而在没有移动构造函数,使用右值引用续命是复用了即将释放的对象里面全部资源。
getObj函数也可以这样写,这个返回的就是右值引用类型
Test&& getObj2()
{
return Test();
}
通过这些方式得到的对象都称为将亡值。将亡值就是即将被释放的对象。
C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):
1. 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等。
2. 将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等。
综上,使用移动构造,返回即将释放的对象,或者返回右值引用的对象都称之为将亡值。