目录
一、右值引用:
1、左值与右值:
2、左值引用和右值引用:
二、右值引用的使用场景:
1、左值引用的使用场景:
2、右值引用的使用场景:
移动构造
移动赋值
三、完美转发:
1、万能引用:
2、实际使用:
一、右值引用:
1、左值与右值:
在了解右值引用前,要先了解什么是左值引用(其实在之前已经使用过很多回了),那么要了解什么是左值引用,就先要了解什么是左值什么是右值
在等号左边的值叫左值吗?在等号右边的值叫右值吗?
显然定义不会这么简单的,但是左值可以出现赋值符号的左边,右值不能出现在赋值符号左边,也就是说在等号左边的值一定不是右值,在等号右边的可以是左值或右值
左值:
能够进行取地址的操作,也可以被修改
如下的a,b,c都是左值
int main()
{int a = 10;const int b = 10;int* c = new int(0);return 0;
}
右值:
不能够进行取地址的操作,一般不能够被修改
如下,10,x,fmin(x,y)的返回值就是右值
int main()
{double x = 1.1, y = 2.2;//如下就是右值10;x + y;fmin(x, y);return 0;
}
理解:
1、右值的本质是一个临时变量或者常量值
2、这些临时变量是没有被实际存储起来的,所以无法对右值取地址
3、像上述的fmin的返回值,其实际上就是一份临时拷贝,所以算作右值
2、左值引用和右值引用:
什么是左值引用:
左值引用就是给左值取别名
int main()
{int a = 10;const int b = 10;int* c = new int(0);//这就是左值引用int& pa = a;const int& pb = b;int*& pc = c;return 0;
}
什么是右值引用:
右值引用就是给右值取别名
int main()
{double x = 1.1, y = 2.2;//如下就是右值10;x + y;fmin(x, y);//这个就是右值引用int&& p1 = 10;int&& p2 = x + y;int&& p3 = fmin(x, y);return 0;
}
这里在给右值取别名后,右值会被存储到特定的位置,此时就能够取到该位置的地址了
左值引用可以引用右值吗 ----- 可以
但是,左值引用不能直接引用右值,因为右值不能够被修改,左值可以修改,如果直接引用的话权限会存在放大问题,所以如果想要左值引用右值就需要加上const修饰
像我们之前在函数参数传参的时候经常写const T& x,这就是保证既能够传左值,又能够传右值
template<class T>
void func(const T& val)
{cout << val << endl;
}
int main()
{string s("111");func(s); //s为左值func("222"); //"222"为右值return 0;
}
右值引用可以引用左值吗 ----- 可以
但是,右值引用也不能直接引用左值,如果想要引用左值,就需要加上move后的左值
int main()
{int a = 10;//右值引用给左值取别名int&& pa = move(a);return 0;
}
为什么加上move后才能让右值引用来引用左值呢?
我们首先要知道,左值引用或者右引用都是在给资源取别名,对于左值引用,就是直接指向原本的数据,对于右值引用,我们知道原本是没有空间资源的,那么右值引用引用右值就是首先开辟一块空间,然后将常量或者临时变量转移到开辟好的地方,然后在指向该地方
所以右值引用的本质是对右值进行资源的转移
此时就有空间资源了,此时就能够取地址了,并且能够对其进行修改了
对于常量,临时变量,表达式的结果这些右值,编译器在右值引用的时候会直接将这些右值进行转移资源,但是对于左值,编译器不敢直接转移,这个时候编译器就为用户提供了一个函数move,当进行move左值的时候,就能够让右值引用 引用左值了
二、右值引用的使用场景:
1、左值引用的使用场景:
左值引用既能够引用左值,又能够引用右值,但是还是存在短板,所以在C++11里面,引入了右值引用来弥补左值引用的短板
在左值引用中:
1、左值引用做参数,防止传参是的拷贝
2、左值引用做返回值,防止返回时对返回对象进行拷贝
首先,写一个自己的string类,在里面写上部分cout来方便打印观察
namespace ppr
{class string{public:typedef char* iterator;iterator begin(){return _str;//返回字符串中第一个字符的地址}iterator end(){return _str + _size;//返回字符串中最后一个字符的后一个字符的地址}//构造函数string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}//交换两个对象的数据void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷贝构造函数(现代写法)string(const string& s):_str(nullptr),_size(0),_capacity(0){cout << "string(const string& s) -- 深拷贝" << endl;string tmp;swap(tmp);}//移动构造函数(现代写法)string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}//赋值运算符重载(现代写法)string& operator=(const string& s){cout << "string& operator=(const string& s) -- 深拷贝" << endl;string tmp;swap(tmp);return *this;}//析构函数~string(){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}//[]运算符重载char& operator[](size_t i){assert(i < _size);return _str[i];}//改变容量,大小不变void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strncpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}}//尾插字符void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_str[_size + 1] = '\0';_size++;}//+=运算符重载string operator+=(char ch){push_back(ch);string tmp(*this);return tmp;}//返回C类型的字符串const char* c_str()const{return _str;}private:char* _str;size_t _size;size_t _capacity;};
}
接着看看左值引用的使用场景
//首先,看看左值引用的使用场景
//值传参
void func1(ppr::string s)
{cout << "void func1(ppr::string s)" << endl;
}
//左值引用传参
void func2(const ppr::string& s)
{cout << "void func2(const ppr::string& s)" << endl;
}int main()
{ppr::string ret1("1111111111111");func1(ret1);//这里采用深拷贝func2(ret1);ret1 += '0';//里面return *this 的时候,也会进行拷贝构造return 0;
}
其中,在func1的时候,传值传参会进行一次拷贝构造,在+=那里,返回* this的时候,也会进行一次拷贝构造这样的话会看到两次深拷贝
左值引用短板:
左值引用能够避免不必要的拷贝构造,但是并不能完全避免
左值引用做参数的时候,能够完全避免传参时的拷贝
左值引用做返回值的时候,不能完全避免拷贝
比如如果返回的是一个局部变量,在返回的时候局部变量被销毁了,此时如果使用左值引用进行返回,就会返回的野指针,此时就不能够用左值引用返回,需要老老实实地值拷贝
如下会进行两次拷贝操作,然后将ret返回
如果在新一点的编译器会进行优化,只需进行一次拷贝操作
如果是引用传参,此时局部变量的局部空间,在出了函数作用域之后就会被释放,此时就会出问题
所以,C++11为了解决这类问题,提出了右值引用来解决这种场景
2、右值引用的使用场景:
右值分为 纯右值 和 将亡值
纯右值:内置类型的右值
将亡值:自定义类型的右值
右值引用和移动语句解决上述问题的方式就是,给当前模拟实现的string类增加移动构造方法
移动构造
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
如果没加上述的移动拷贝,就会出现深拷贝
如果加上移动构造,那么就会走移动构造函数,这样就能更加减少拷贝
移动构造的本质就是将参数的右值窃取过来,占为己有,这样它就不用再深度拷贝了,所以叫做移动构造
移动构造和拷贝构造的区别:
1、在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数
2、增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数
3、string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小
左值引用:直接引用对象以减少拷贝
右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝
移动赋值
移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,之所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思
// 赋值重载
string& operator=(const string& s)
{cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this;
}
//移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string && s) -- 移动拷贝" << endl; swap(s); return *this;
}
string& operator=(const string& s) 和string& operator=(string&& s) 的区别:
1、在没有string& operator=(string&& s) 的时候,如果进行=操作,那么无论是左值还是右值传参都会调用string& operator=(const string& s) 这个函数
2、在增加移动赋值后,如果是左值就调用原来的函数,如果是右值就调用新加的移动赋值函数
3、移动赋值函数是通过swap函数进行资源的交换,而原来的operator=是通过深拷贝进行,因此,移动赋值的代价比原来的要小
三、完美转发:
1、万能引用:
template<class T>
void PerfectForward(T&& t)
{//...
}
这里函数中的参数并不是右值引用,如果传的模板是左值,这里的参数就是左值引用,相反如果传的模板是右值,那么这里的参数就是右值引用
void func(int& a)
{cout << "左值引用" << endl;
}
void func(const int& a)
{cout << "const 左值引用" << endl;
}
void func(int&& a)
{cout << "右值引用" << endl;
}
void func(const int&& a)
{cout << "const 右值引用" << endl;
}
template<class T>
void perfectForward(T&& val)
{func(val);
}int main()
{int a = 10;perfectForward(a); //左值const int b = 10; //const 左值perfectForward(b);perfectForward(move(a)); // 右值perfectForward(move(b)); //const 右值return 0;
}
如上,这就是通过func函数重载,来观察编译器会怎样进行函数调用
如上,这是运行结果,为什么会这样呢?难道是编译器做的不对吗,在实际调用中,4个函数没有一个是进入了右值引用,均匹配的是左值引用版本,这是为什么呢?
当对右值进行引用后,会导致右值被存储到特定的位置,此时就能够对这个引用后的右值进行取地址了,这样的话,这个右值就模版被识别成左值了
也就是说,在右值引用过一次后,会导致右值变成左值,但是如果想要继续保证其右值的属性,此时就需要用到完美转发
如上,在对右值引用进行传参的时候,在前面加上forward<T>,这样经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数
forward是一个模板函数,需要指定模板参数类型T,确保能正确推导并传递
2、实际使用:
首先实现一个建议的list,在其中实现左值引用的push_back和insert函数
namespace ppr
{template<class T>struct ListNode{T _data;ListNode* _next = nullptr;ListNode* _prev = nullptr;};template<class T>class list{typedef ListNode<T> node;public://构造函数list(){_head = new node;_head->_next = _head;_head->_prev = _head;}//左值引用版本的push_backvoid push_back(const T& x){insert(_head, x);}//右值引用版本的push_backvoid push_back(T&& x){insert(_head, x);}//左值引用版本的insertvoid insert(node* pos, const T& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}//右值引用版本的insertvoid insert(node* pos, T&& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:node* _head; //指向链表头结点的指针};
}
接着进行左值和右值的push_back版本的调用
int main()
{ppr::list<ppr::string> lt;ppr::string s1("111111111111111");//左值的push_backlt.push_back(s1);cout << endl << endl;ppr::string s2("111111111111111");//右值的push_backlt.push_back(move(s2));cout << endl << endl;lt.push_back("22222222222222222");//右值的push_backreturn 0;
}
但是会发现全部都是深拷贝,这和上述右值被引用后,就可以取地址了,就变成左值了,所以为了避免这种情况,就需要在右值版本的push_back和insert加上完美转发,让右值能够保存右值属性