c++11特性:右值引用的作用以及使用

右值:

C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。在介绍右值引用类型之前先要了解什么是左值和右值:

1. lvalue 是locator value的缩写,rvalue 是 read value的缩写

2. 左值是指存储在内存中、有明确存储地址(可取地址)的数据;

3. 右值是指可以提供数据值的数据(不可取地址);

通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。

下面的一段代码讲述了左值引用和右值引用的初始化方式: 

#include
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
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
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
move construct ... 
destruct Test class ...

construct:my name is jerry
m_num 地址:000000C0172FF688
move construct ... 
destruct Test class ...
m_num 地址:000000C0172FF688
destruct Test class ...
destruct Test class ...

 由上述输出结果可以看出,移动构造就是用的同一个地址。

 使用右值引用续命:(下面的代码没有移动构造函数)

#include
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;
}

输出结果为:

c++11特性:右值引用的作用以及使用_第1张图片  

 使用右值引用t2给这个匿名对象续命,因为输出的地址一致。我们并没有创建t2,而是使用了即将释放的这个对象里面的所有的资源。

注意:移动构造函数中是复用了即将释放的对象里面的部分资源(堆内存),而在没有移动构造函数,使用右值引用续命是复用了即将释放的对象里面全部资源。

getObj函数也可以这样写,这个返回的就是右值引用类型 

Test&& getObj2()
{
	return Test();
}

 通过这些方式得到的对象都称为将亡值。将亡值就是即将被释放的对象。

C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):

1. 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等。
2. 将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等。

综上,使用移动构造,返回即将释放的对象,或者返回右值引用的对象都称之为将亡值。

 

你可能感兴趣的:(c++,算法,开发语言,笔记)