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

右值:

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
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<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 的返回值等。

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

 

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
右值引用和move语义是C++ 11中重要的特性之一,可以提高程序的效率和性能。右值引用是一种新的引用类型,其绑定到临时对象或将要销毁的对象上,而不是左值对象。move语义则是利用右值引用,将一个对象的资源所有权从一个对象转移到另一个对象,避免了不必要的内存拷贝,提高了程序的效率。 下面是一个使用右值引用和move语义的例子: ```c++ #include <iostream> #include <vector> using namespace std; vector<int> getVector() { vector<int> v = {1, 2, 3, 4}; return v; } int main() { vector<int> v1 = getVector(); // 拷贝构造函数 vector<int> v2 = move(v1); // 移动构造函数 cout << "v1 size: " << v1.size() << endl; // 输出 0 cout << "v2 size: " << v2.size() << endl; // 输出 4 return 0; } ``` 在上面的例子中,getVector函数返回一个临时对象vector<int>,该临时对象是一个右值。在主函数中,我们使用拷贝构造函数将临时对象的值拷贝到v1中,然后使用move函数将v1中的值移动到v2中。由于move函数使用右值引用,将v1中的资源所有权转移到了v2中,避免了不必要的内存拷贝,提高了程序的效率。最后,我们输出v1和v2的大小,可以看到v1的大小为0,v2的大小为4,说明资源已经成功转移。 需要注意的是,使用move语义之后,原对象的值会被移动到新对象中,并且原对象的值会被置为默认值(例如,对于vector而言,原对象的大小为0)。如果需要保留原对象的值,则需要在移动之前先进行一次拷贝操作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值