【C++左值引用&&右值引用】

C++左值引用&&右值引用

在学习C++的过程中,对于左值引用和右值引用总是傻傻分不清楚,不知道右值引用到底有什么用。今天花了一天的时间弄懂了,在这里记录一下。
以下都是我写代码得到的一些感性和理性混合的认知,可以明确的说,我的认知不是完全正确的,但是相信懂得这些已经足够应付面试了。

什么是左值,右值,左值引用和右值引用?

左值

在表达式结束之后,依然存在的对象,可以被取地址的对象,可以被赋值的对象

右值

在表达式结束之后就不再存在的临时变量。通常是被赋值的临时变量和表达式的结果等。例如整数常量,字符串常量

左值引用

我们最常用的引用,是对象的别名,绑定在对象上,区分常量左值引用和非常量左值引用,其中,常量左值引用可以绑定左值和右值(不区分常量和非常量),而非常量左值引用只能绑定非常量左值。(有点绕,结合const,自己在编译器上多试试。)

右值引用

C++11的新东西,是只能绑定在右值和(左值变量的右值形态)上的引用。同时,右值引用也要区分常量右值引用和非常量右值引用。

注意,所有的引用,都是左值哦,因为尽管他们只是变量的别名,但是你使用他就是使用这个变量,这个变量是可以取地址的,所以所有的引用,不管是不是右值引用也不管是不是常量引用,都是左值。对右值引用进行二次赋值,就是对其绑定的对象赋值。上述说的都可以通过以下代码进行验证。

那么,右值引用到底有什么用?

右值引用据说至少有两个优势场景,但是我目前只弄清楚一个,就是实现移动构造,从而减少深拷贝的次数。
首先,要弄清楚,移动构造的目的是什么?

场景:在我使用已经创建好的临时变量去创建新的变量时,如果不使用移动构造,那么,我们会进行一次深拷贝,然后将临时变量的资源释放掉。注意,临时变量的资源被释放是无法停止的,是必定会进行的。

问题:那么,既然临时变量已经申请了资源,我基于临时变量构造的变量为什么还要重新申请一遍资源呢?这一步可不可以省去呢?

思考:当然是可以的,我只需要将新变量的指针指向临时变量的指针所指向的内存,然后将临时变量的指针置空,就可以实现了。

听起来左值引用就可以实现这件事了呀!真的是这样吗?

困难:如果使用左值引用来实现移动构造,那么我是使用常量左值引用还是非常量左值引用?

如果使用常量左值引用:我们可以不用担心参数是否是常量,但是,因为是常量引用,我们也就无法对参数做任何更改,也就无法实现将临时变量的指针置空!这样就会导致我们会对同一个内存释放两次!

如果使用非常量左值引用:我们可以实现对非常量左值的指针置空,而对于常量左值,则不能。但是好像有些奇怪,非常量左值,也就意味着他不是临时变量,也就意味着我后续还有可能需要使用它!移动构造反而成为了麻烦。

反思:哦!所以,移动构造针对的对象是临时变量,只有临时变量是在使用后会被立即释放的,是无法被二次利用的。移动构造是要让当前变量接管临时变量所申请的资源,从而减少深拷贝的次数!

那么右值引用可以实现我们想要的移动构造吗?

答案:可以!还记得吗,右值引用,可以绑定右值(也就是常量),也可以绑定左值的右值形态!(std::move)。先不管后面这个,前面这个就意味着我们可以实现移动构造了。

右值引用可以绑定常量,并对常量的值进行更改,从而实现移动构造。(不要惊讶右值引用可以对常量进行更改,下面有代码可以证明,也许真正的机理比我在这里说的要复杂,但至少我在描述的这个现象是存在的)

int&& a = 10;
cout << "当前值: " << a << endl;
cout << "当前地址: " << &a << endl;
a = 20;
cout << "当前值: " << a << endl;
cout << "当前地址: " << &a << endl;

我的输出结果:
当前值: 10
当前地址: 00000048252FFB64
当前值: 20
当前地址: 00000048252FFB64

那么,接下来,让我们写一个代码例子来检验一下。
我们需要检验三种情况:
一、不实现任何移动构造,我们在进行三种构造方式的时候,需要调用析构函数多少次
二、实现左值引用的移动构造,我们在进行三种构造方式的时候,需要调用析构函数多少次
三、实现右值引用的移动构造,我们在进行三种构造方式的时候,需要调用析构函数多少次

#include<iostream>
#include<thread>
#include<future>
using namespace std;

class mystring {
private:
	char* _data;
	size_t _len;

public:
	mystring() {
		_data = nullptr;
		_len = 0;
	}
	//拷贝构造
	mystring(const mystring& str) {
		//浅拷贝,会重复释放相同的内存
		//_data = str._data;
		//_len = str._len;
		//深拷贝
		this->_data = new char[str._len+1];
		memcpy(this->_data, str._data, str._len);
		this->_len = str._len;
		this->_data[_len] = '\0';
	}
	//重载=
	mystring& operator=(const mystring& str) {
		if (this != &str) {
			this->_data = new char[str._len + 1];
			memcpy(this->_data, str._data, str._len);
			this->_len = str._len;
			this->_data[_len] = '\0';
		}
		return *this;
	}
	mystring(const char* str) {
		_len = strlen(str);
		this->_data = new char[_len + 1];
		memcpy(this->_data, str, _len);
		this->_data[_len] = '\0';
	}
	~mystring() {
		if (_data!=NULL) {
			cout << "delete!" << endl;
		    free(_data);
		}
	}
};

int main() {
	mystring a;
	a = mystring("Hello world!");
	mystring b(a);
}

我的执行结果:
delete!
delete!
delete!

可以看到,调用了三次析构函数。在赋值过程中,分别用到了无参构造、有参构造、拷贝构造,=

接下来,我们实现一个左值引用的移动构造,这需要重载拷贝构造和=

#include<iostream>
#include<thread>
#include<future>
using namespace std;

class mystring {
private:
	char* _data;
	size_t _len;

public:
	mystring() {
		_data = nullptr;
		_len = 0;
	}
	mystring(const mystring& str) {
		//浅拷贝,会重复释放相同的内存
		//_data = str._data;
		//_len = str._len;
		//深拷贝
		this->_data = new char[str._len+1];
		memcpy(this->_data, str._data, str._len);
		this->_len = str._len;
		this->_data[_len] = '\0';
	}
	//非常量左值引用拷贝构造,使用移动构造的方式
	mystring(mystring& str) {
		this->_data = str._data;
		this->_len = str._len;
		str._data = nullptr;
		str._len = 0;
	}
	//非常量左值引用的=重载,使用移动构造的方式
	mystring& operator=(const mystring& str) {
		if (this != &str) {
			this->_data = new char[str._len + 1];
			memcpy(this->_data, str._data, str._len);
			this->_len = str._len;
			this->_data[_len] = '\0';
		}
		return *this;
	}
	mystring& operator=(mystring& str) {
		if (this != &str) {
			this->_data = str._data;
			this->_len = str._len;
			str._data = nullptr;
			str._len = 0;
		}
		return *this;
	}
	mystring(const char* str) {
		_len = strlen(str);
		this->_data = new char[_len + 1];
		memcpy(this->_data, str, _len);
		this->_data[_len] = '\0';
	}
	~mystring() {
		if (_data!=NULL) {
			cout << "delete!" << endl;
		    free(_data);
		}
	}
};
int main() {
	mystring a;
	a = mystring("Hello world!");
	mystring b(a);
	mystring c = b;
}

我的输出:
delete!
delete!

可以看到,这回,使用非常量左值进行拷贝构造,是用的移动构造的思路实现的,4个对象,只调用了两次析构函数。
但是也正如我们前面所说的,这没什么用,我们想要的是接管临时变量的资源。

好,重头戏来了!

#include<iostream>
#include<thread>
#include<future>
using namespace std;

class mystring {
private:
	char* _data;
	size_t _len;
public:
	mystring() {
		_data = nullptr;
		_len = 0;
	}
	mystring(const mystring& str) {
		this->_data = new char[str._len+1];
		memcpy(this->_data, str._data, str._len);
		this->_len = str._len;
		this->_data[_len] = '\0';
	}
	mystring& operator=(const mystring& str) {
		if (this != &str) {
			this->_data = new char[str._len + 1];
			memcpy(this->_data, str._data, str._len);
			this->_len = str._len;
			this->_data[_len] = '\0';
		}
		return *this;
	}
	mystring(const char* str) {
		_len = strlen(str);
		this->_data = new char[_len + 1];
		memcpy(this->_data, str, _len);
		this->_data[_len] = '\0';
	}
	~mystring() {
		if (_data!=NULL) {
			cout << "delete!" << endl;
		    free(_data);
		}
	}
	//右值引用的移动=
	mystring& operator=(mystring&& str) {
		_data = str._data;
		_len = str._len;
		str._data = nullptr;
		str._len = 0;
		cout << "move operator" << endl;
		return *this;
	}
	//右值引用的移动构造函数
	mystring(mystring&& str) {
		if (_data != str._data) {
			_data = str._data;
			_len = str._len;
			str._data = nullptr;
			str._len = 0;
			cout << "move constructor" << endl;
		}
	}
	void myprint() {
		cout << _data;
	}
};
int main() {
	mystring a;
	a = mystring("Hello world!");
	mystring b(move(a));
	mystring c = move(b);
}

我的输出结果是:
move operator
move constructor
move constructor
delete!
可以看到,只调用了一次析构函数!

最后,右值引用在减少深拷贝构造的领域里,还有一个更常用的场景,swap函数(注意,下面的左值引用版和右值引用版的区分在于拷贝构造的时候,而不是调用函数时传参的时候!)

//右值引用版
template <typename T>
void myswap(T& a, T& b) {
	T temp(move(a));
	a = move(b);
	b = move(temp);
}
//左值引用版
template <typename T>
void myswap(T& a, T& b) {
	T temp = a;
	a = b;
	b = temp;
}

接着我们调用下面的函数,记得两个模板要注释掉一个

#include<iostream>
#include<thread>
#include<future>
using namespace std;

class mystring {
private:
	char* _data;
	size_t _len;

public:
	mystring() {
		_data = nullptr;
		_len = 0;
	}
	mystring(const mystring& str) {
		this->_data = new char[str._len+1];
		memcpy(this->_data, str._data, str._len);
		this->_len = str._len;
		this->_data[_len] = '\0';
		cout << "Deep Copy!" << endl;
	}
	mystring& operator=(const mystring& str) {
		if (this != &str) {
			this->_data = new char[str._len + 1];
			memcpy(this->_data, str._data, str._len);
			this->_len = str._len;
			this->_data[_len] = '\0';
		}
		cout << "Left = Copy!" << endl;
		return *this;
	}
	mystring(const char* str) {
		_len = strlen(str);
		this->_data = new char[_len + 1];
		memcpy(this->_data, str, _len);
		this->_data[_len] = '\0';
	}
	~mystring() {
		if (_data!=NULL) {
			cout << "delete!" << endl;
		    free(_data);
		}
	}
	mystring& operator=(mystring&& str) {
		_data = str._data;
		_len = str._len;
		str._data = nullptr;
		str._len = 0;
		cout << "Right = Copy!" << endl;
		return *this;
	}

	mystring(mystring&& str) {
		if (_data != str._data) {
			_data = str._data;
			_len = str._len;
			str._data = nullptr;
			str._len = 0;
			cout << "Move Copy!" << endl;
		}
	}
	void myprint() {
		cout << _data;
	}
};


template <typename T>
void myswap(T& a, T& b) {
	T temp(move(a));
	a = move(b);
	b = move(temp);
}


/*
template <typename T>
void myswap(T& a, T& b) {
	T temp = a;
	a = b;
	b = temp;
}
*/

int main() {
	mystring a("Hello");
	mystring b("World");
	myswap<mystring>(a, b);
}

右值引用版的输出:
Move Copy!
Right = Copy!
Right = Copy!
delete!
delete!

左值引用版的输出:
Deep Copy!
Left = Copy!
Left = Copy!
delete!
delete!
delete!

这说明,我们的右值引用版,全程没有申请过新的资源,而左值引用版,是申请了新的资源的!

至此,完毕!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值