C++ 11 特性 之 右值引用

左值与右值

左值与右值的本质区别:能不能对表达式取地址;

非引用返回的临时变量,原始字面量, lambda表达式等都属于右值。

rvalue reference

右值引用的声明方式:

T && k = GetStr();

GetStr函数返回一个非引用的临时变量。与普通返回临时变量不同,有了右值引用,使得该临时变量不会在函数调用结束后被销毁,而是将生命周期跟变量k绑定。

自定义一个String类测试:

class String {
	public:
		String():m_pData(new char('\0')) {
			cout<<"Constructor: "<<hex<<this<<endl;
		}

		String(const char *pData) {
			m_pData = new char[strlen(pData) + 1];
			strcpy(m_pData, pData);
			cout<<"string Constructor: "<<hex<<this<<endl;
		}

		String(const String &str) {
			m_pData = new char[strlen(str.m_pData) + 1];
			strcpy(m_pData, str.m_pData);
			cout<<"copy Constructor: "<<hex<<this<<endl;
		}

		String &operator = (const String &str) {
			if(this != &str) {
				String sTemp(str);
				char *mTemp = sTemp.m_pData;
				sTemp.m_pData = m_pData;
				m_pData = mTemp;
			}
			cout<<"Valuation: "<<hex<<this<<endl;
			return *this;
		}
		
		virtual ~String() {
			delete []m_pData;
			m_pData = nullptr;
			cout<<"Destuctor: "<<hex<<this<<" ("<<++Des<<")"<<endl;
		}
	private:
		char *m_pData;
		static int Des;      //记录构造对象的次数
};

int String::Des = 0;

设置一个函数,返回类的临时对象,

String GetStr() {
	return String();        //两种方式等价
	/*String s;
	return s;*/
}

然后用以下代码进行测试,

String str = GetStr();

//输出结果:
Constructor: 0x6ffdd0			//默认构造函数,构造临时对象s
copy Constructor: 0x6ffe30		//返回临时对象s的副本,由s拷贝构造函数
Destuctor: 0x6ffdd0 (1)
copy Constructor: 0x6ffe20		//构造对象str
Destuctor: 0x6ffe30 (2)
Destuctor: 0x6ffe20 (3)
//(注:编译时设置编译选项`-fno-elide-constructors`防止编译器优化。)

可以看出,在没有优化的情况下,拷贝构造函数调用了两次。为了避免对象的多次拷贝构造和析构,才有了右值引用的优化。

String &&str = GetStr();

//输出结果:
Constructor: 0x6ffdd0
copy Constructor: 0x6ffe20			//返回临时变量的副本,不是构造str
Destuctor: 0x6ffdd0 (1)
Destuctor: 0x6ffe20 (2)

使用了右值引用,改变了返回的临时变量的生命周期,少了一次构造的过程。值得一提的是,如果我们使用常量左值的方式,也可以达到与右值引用一样的效果。

const String &str = GetStr();

因为,

I. 常量左值引用是一个万能的类型:可以接受左值,右值,常量左值,常量右值。
II. 普通左值引用则会引起编译报错,非常量左值引用只能接收左值。


移动构造函数

众所周知,默认拷贝构造函数的拷贝方式是浅拷贝,会引发“指针悬挂”问题。因此,显式定义拷贝构造函数,实现深拷贝不可或缺。但是,深拷贝虽然可以保证正确性,在返回临时变量方面却也造成了额外的性能损耗。

因此,借助右值引用,实现移动构造函数,完美解决该问题。(同理可实现移动赋值运算符函数)

//添加实现移动构造函数和移动赋值函数
String(String &&str):m_pData(str.m_pData) {
	str.m_pData = nullptr;
	cout<<"move Cosntructor: "<<hex<<this<<endl;
}

String &operator = (String &&str) {
	if(this != &str) {
		m_pData = str.m_pData;
		str.m_pData = nullptr;
	}
	cout<<"move Valuation: "<<hex<<this<<endl;
	return *this;
}

//输出结果:
Constructor: 0x6ffdd0
move Cosntructor: 0x6ffe20
Destuctor: 0x6ffdd0 (1)
Destuctor: 0x6ffe20 (2)

可以看出这里并没有调用拷贝构造函数,而是调用了移动构造函数,避免了临时变量的深拷贝问题,仅仅是将对象指向字符串的指针移动到了另一个对象。

为了保证代码的安全性,即使定义了移动构造函数,拷贝构造函数也不可或缺。


universal reference

个人理解,universal引用只是引用折叠的一种产物,

template<typename T>
T Funtional(T &&);

T &&的注意点:

I. 当T是确定的类型时,如int &&,表示右值引用;
II. 当T被推导为T &或者T &&等引用类型时,发生引用折叠;
III. 当T存在类型推导时(作为函数参数或者使用关键字auto),表示universal引用。如果被右值会初始化,则为右值;若被左值初始化,则为左值;
IV. const T &&为右值引用;

引用折叠

I. 所有右值引用叠加到右值引用仍然是一个右值引用;
II. 所有其他包括其他类型引用的叠加都将变成左值引用;


移动语义

移动语义的本质作用:将资源的所有权转移,将左值转换为右值引用,然后调用移动构造函数,避免拷贝,优化性能。

如:

vector<T> v1;                   //省略初始化
vector<T> v2 = v1;             //存在拷贝

vector<T> v3;
vector<T> v4 = move(v3);        //没有拷贝, std::move

若不使用std::move,拷贝的代价大,性能低。

完美转发

#include<iostream>
using namespace std;

void processValue(int &i){
	cout<<"left"<<endl;
}

void processValue(int &&i){
	cout<<"right"<<endl;

template<typename T>
void forwardValue(T &&val){
	processValue(forward<T> val);    //按照传入类型转发, std::forward
}

int main(){
	int i = 0;
	forward(i);     //传入左值
	forward(0);     //传入右值	
}

//输出结果:
left
right

附:

参考:从4行代码看右值引用.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值