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!
这说明,我们的右值引用版,全程没有申请过新的资源,而左值引用版,是申请了新的资源的!
至此,完毕!