传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
4.右值引用
4.1 左值引用和右值引用
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址并且可以对它赋值,左值可以出现赋值符号的左边(也可以出现在赋值符号的右边),右值不能出现在赋值符号左边(只可以出现在符号的右边)。定义时const
修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用(给左值取别名)。
int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0;
}
什么是右值?什么是右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回,如函数返回值,这个返回值在函数表达式中存在,但是出了函数作用域这个值就会被销毁,所以不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
右值被分为
1.纯右值(内置类型表达式的值)
2.将亡值(自定义类型表达式的值)
- 案例1
#include<iostream>// 一个函数模板
template<class T>
T fmin(T x, T y)
{if (x < y){return x;}return y;
}int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值// 字面常量:10;// 表达式的返回值 x + y;// 函数返回值 fmin(x, y);10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1; x + y = 1;fmin(x, y) = 1;return 0;
}
- 案例2
int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;// ra1为a的别名,a是左值int& ra1 = a; // 编译失败,因为10是右值,右值是不可以被左值引用的//int& ra2 = 10; // const修饰的左值引用,既可引用左值,也可引用右值。const int& ra3 = 10; const int& ra4 = a; // 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;// int&& r2 = a; // 报错// move()左值之后,编译器会将其识别为右值// 右值引用可以引用move()以后的左值int&& r3 = std::move(a);return 0;
}
- 案例3
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1
引用后,可以对rr1
取地址,也可以修改rr1
。如果不想rr1
被修改,可以用const int&& rr1
去引用,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{double x = 1.1, y = 2.2;// 右值引用之后,会导致右值被存储到特定位置(此时我们就可以对rr1进行修改,不过修改的是特定位置的变量,此时我们可以认为rr1就是一个左值),且可以取到该位置的地址int&& rr1 = 10;// const修饰rr2之后,则rr2不可以被修改了const double&& rr2 = x + y;rr1 = 20;// rr2 = 5.5; // 报错return 0;
}
4.2左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是
const
左值引用既可引用左值,也可引用右值
int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0;
}
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
int main()
{// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;int&& r2 = a;// 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}
4.3 右值引用使用场景和意义
左值引用的意义是什么?
1.函数传参或者是函数传返回值,使用左值引用,可以减少参数的拷贝
但是左值引用并没有完全解决问题,例如以下场景
- 场景1
// 场景1:左值引用可以解决函数传参,减少参数的拷贝
// const引用,既可以接收左值,也可以接收右值
template<class T>
void func1(const T& x)
{}int main()
{// v1为左值vector<int> v1(10, 0);func1(v1);// vector<int>(10,0) 是一个匿名对象,也就是一个右值(属于将亡值)// 出了当前这一行,这个匿名对象就会被释放func1(vector<int>(10, 0));return 0;
}
- 场景2:
// 场景2:函数传返回值,
// x的声明周期是直到main()返回,才会被销毁
// 因此我们才可以使用左值引用返回,如果x出func2()就被销毁,那么是不可以使用左值引用返回的
template<class T>
const T& func2(const T& x)
{// ...假设中间还做了许多操作return x;
}int main()
{// v1为左值vector<int> v1(10, 0);func2(v1);return 0;
}
- 场景3
// 场景三:左值引用尚未解决的问题场景
// 当ret出func3()的函数作用于就会被销毁,那么我们是不可以使用左值引用返回的(这是因为引用的变量已经被销毁了)
// 因此,此时ret返回时,就会产生临时变量,就会增加参数ret的拷贝
// 右值引用的价值之一:就是补齐这个最后一块短板,传值返回的拷贝问题
template<class T>
T func3(const T& x)
{T ret;// ...return ret;
}int main()
{// v1为左值vector<int> v1(10, 0);func3(v1);return 0;
}
- 场景4
// 但是其实也可以使用左值引用来解决场景三的问题
// 就是使用输出型参数,但是这样使用起来是很别扭的
// 假设ret的类型就是int
// 使用了输出型参数就不需要进行返回了
template<class T>
void func4(const T& x, int& ret)
{// ...//return ret;
}int main()
{// v1为左值vector<int> v1(10, 0);int ret = 10;// ret是一个输出型参数func4(v1,ret);return 0;
}
右值引用是怎样解决左值引用的短板的?
// 首先,我们先来看下面代码运行时,其底层的拷贝原理
#include<iostream>
#include<assert.h>
using namespace std;namespace qwy
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}// 构造函数string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};// 将整数转化为字符串string to_string(int value){bool flag = true;if (value < 0){flag = false;// 将value变为正数value = 0 - value;}qwy::string str;while (value > 0){// 获取value的个位int x = value % 10; // 获取value的十位及以上value /= 10;str += ('0' + x); // 将x转化为对应的ascll值,并放入str}if (flag == false){str += '-';}// 逆转string对象中字符串的顺序std::reverse(str.begin(), str.end());return str;}
}
场景一
int main()
{// 场景1:// to_string()的返回值str,会先将其拷贝给一个临时变量,再将临时变量拷贝给ret// 但是编译器会自动对其进行优化,将两次拷贝简化为一次拷贝// 如下图所示qwy::string ret = qwy::to_string(-1234);return 0;
}
- 打印结果为:
string(const string& s)
– 深拷贝 - 通过打印结果我们可知,只调用了一次深拷贝,符合我们预期的结果
场景二
// 场景2:
int main()
{// 对于场景二:编译器不敢将两次深拷贝优化为一次深拷贝// 优化的规定一般为:传参或传返回值过程中,存在连续的构造、拷贝构造、就会被优化// 但是具体是取决于编译器的// 由于编译器对于如下这种情况是不进行优化的,// 因此to_string()的返回值str会先拷贝给临时变量,再由临时变量赋值给ret,但是赋值重载的过程中,会调用拷贝函数qwy::string ret;// 在上面创建string对象ret和下面对ret进行赋值之间,可能对ret进行了其他操作,因此编译器不敢进行优化ret = qwy::to_string(-1234);return 0;
}
注:编译器没有进行优化
第一步:拷贝给临时变量,调用了一次拷贝构造,由临时变量赋值给ret,调用了赋值重载,赋值重载的函数内部调用了一次拷贝构造
打印结果为:
string(const string& s)
– 深拷贝
string& operator=(string s)
– 深拷贝
string(const string& s)
– 深拷贝
4.4移动构造和移动赋值
// 移动构造
// 所谓的移动构造就是将string对象的右值引用s与将要构造的对象进行资源交换
// 这样是不需要在移动构造内部创建新的对象
string(string&& s)
{cout << "string(const string& s) -- 移动拷贝" << endl;swap(s);
}// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string s) -- 移动赋值" << endl;swap(s);return *this;
}
#include<iostream>
#include<assert.h>
using namespace std;namespace qwy
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}// 构造函数string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}// 移动构造// 所谓的移动构造就是将s与将要构造的对象进行资源交换string(string&& s){cout << "string(const string& s) -- 移动拷贝" << endl;swap(s);}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string s) -- 移动赋值" << endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};// 将整数转化为字符串string to_string(int value){bool flag = true;if (value < 0){flag = false;// 将value变为正数value = 0 - value;}qwy::string str;while (value > 0){int x = value % 10; // 获取value的个位value /= 10; // 获取value的十位及以上str += ('0' + x); // 将x转化为对应的ascll值,并放入str}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}
场景一:右值引用解决了拷贝构造问题(只是对于右值传参拷贝的问题)
// 使用移动拷贝进一步提升构造的效率(移动构造的效率的大于拷贝构造的)
// 这就是右值引用解决了拷贝构造问题(只是对于右值传参拷贝的问题)
int main()
{qwy::string s1("hello world");// 用s1来构造s2// s1是左值,因此编译器会默认调用左值引用传参的拷贝构造qwy::string s2(s1);// 左值被move()之后,编译器会将其看作是右值// move(s1)是右值,编译器会默认调用右值引用传参的移动构造// 如果没有右值引用传参的移动构造,move(s1)也是可以被左值引用传参的拷贝构造调用qwy::string s3(move(s1));return 0;
}
打印结果为:
string(const string& s)
– 深拷贝
string(const string& s)
– 移动拷贝
场景二:利用右值引用解决赋值问题
int main()
{// 根据优化的规则:传参或传返回值过程中,存在连续的构造、拷贝构造、就会被优化// 因此对于如下的情况,编译器并不会进行优化// 1.编译器会将to_string()的返回值默认识别为右值,并将其移动拷贝给临时变量,// 2.再将临时变量移动赋值给retqwy::string ret;ret = qwy::to_string(-1234);return 0;
}
打印结果:
string(const string& s)
– 移动拷贝
string& operator=(string s)
– 移动赋值
场景三:
int main()
{list<qwy::string> lt;qwy::string s1("111111");// s1是左值,调用拷贝构造lt.push_back(s1);// "222222" 是右值(字面常量),调用移动拷贝// 如果我们没有实现移动拷贝,那么就会调用拷贝构造lt.push_back(qwy::string("222222"));// "333333" 会先构造一个string的对象,再调用移动拷贝// 如果我们没有实现移动拷贝,那么就会调用拷贝构造lt.push_back("333333");return 0;
}
打印结果
string(const string& s)
– 深拷贝
string(const string& s)
– 移动拷贝
string(const string& s)
– 移动拷贝
总结
右值引用和左值引用减少拷贝的原理不太一样
1.左值引用是取别名,直接起作用。
2.右值引用是间接起作用,实现移动构造和移动赋值,在拷贝的场景中,如果是右值(将亡值),转义资源
4,5右值引用引用左值及其一些更深入的使用场景分析
- 场景1:
#include<iostream>
using namespace std;void func1(int& x)
{ cout << "void func1(int& x)" << endl;
}void func2(int&& x)
{ cout << "void func2(int&& x)" << endl;
}int main()
{int x = 1;// 左值调用对应的func1()func1(x);// 右值调用对应的func2()func2(2);return 0;
}
// 打印结果为:
// void func1(int& x)
// void func2(int&& x)
- 场景二
void func2(int&& x)
{ cout << "void func2(int&& x)" << endl;
}int main()
{int x = 1;func2(2);func1(x); // 此处会报错,右值引用无法引用左值return 0;
}
- 模板中的&&万能引用
// 使用模板中的&&万能引用就可以解决场景二中:左值不可以被右值引用的问题了// 模板的右值引用我们可以认为是万能引用,
// 既可以引用右值,也可以引用左值
template<typename T>
void PerfectForward(T&& t)
{// t是否可以被加加,详情请看下图// t++;
}int main()
{perfectforward(10); // 右值int a;perfectforward(a); // 左值perfectforward(std::move(a)); // 右值// 且万能引用既可以引用const的左值,也可以引用const的右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
- 万能引用存在的问题
// 万能引用还存在如下的问题:
#include<iostream>
using namespace std;void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }// 万能引用
template<typename T>
void PerfectForward(T&& t)
{// T&& t 不论是对左值引用,还是对右值引用,t本身都是一个左值// 因此调用Fun()函数,始终会调用参数为左值引用的Fun()函数Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
// 打印结果为:
// 左值引用
// 左值引用
// 左值引用
// const 左值引用
// const 左值引用
- 解决万能引用存在的问题
// 使用完美转发来解决上述的问题
// 具体如下:
#include<iostream>
using namespace std;void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }// 万能引用
template<typename T>
void PerfectForward(T && t)
{// 完美转发,保持t的本源属性,如果t是对左值的引用,那么就保留t为左值的属性,反之保留t为右值的属性Fun(std::forward<T>(t));
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
// 打印结果为:
// 右值引用
// 左值引用
// 右值引用
// const 左值引用
// const 右值引用
对list类的改造(移动构造、移动赋值)(内部使用了完美转发)
// 关于list的封装
namespace qwy
{// 链表节点的类模板template<class T>struct list_node{list_node* _next;list_node* _prev;T _data;list_node(const T& x):_next(nullptr), _prev(nullptr), _data(x){}// 此处右值传参之后,x是左值,为了需要保证x的原生类型属性,因此我们需要使用完美转发// 来保证x的原生类型属性(此处使用了完美转发)list_node(T&& x):_next(nullptr), _prev(nullptr), _data(std::forward<T>(x)){}};// 迭代器的类模板template<class T, class Ref, class Ptr>struct __list_iterator{typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> Self;node* _pnode;__list_iterator(node* p):_pnode(p){}Ptr operator->(){return &_pnode->_data;}Ref operator*(){return _pnode->_data;}Self& operator++(){_pnode = _pnode->_next;return *this;}Self operator++(int){Self tmp(*this);_pnode = _pnode->_next;return tmp;}Self& operator--(){_pnode = _pnode->_prev;return *this;}Self operator--(int){Self tmp(*this);_pnode = _pnode->_prev;return tmp;}bool operator!=(const Self& it) const{return _pnode != it._pnode;}bool operator==(const Self& it) const{return _pnode == it._pnode;}};template<class T>class list{typedef list_node<T> node;public:typedef __list_iterator<T, T&, T*> iterator;iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}void empty_initialize(){_head = new node(T());_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_initialize();}void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}// 拷贝构造list(const list<T>& lt){empty_initialize();list<T> tmp(lt.begin(), lt.end());swap(tmp);}size_t size() const{return _size;}bool empty() const{return _size == 0;}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){it = erase(it);}}// 左值引用的push_backvoid push_back(const T& x){insert(end(), x);}// 右值引用的push_backvoid push_back(T&& x){// 此处右值传参之后,x是左值,为了需要保证x的原生类型属性,因此我们需要使用完美转发// 来保证x的原生类型属性(此处,使用了完美转发)insert(end(), std::forward<T>(x));}// 左值引用的insertiterator insert(iterator pos, const T& x){node* newnode = new node(x);node* cur = pos._pnode;node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}// 右值引用的insertiterator insert(iterator pos, T&& x){// 此处右值传参之后,x是左值,为了需要保证x的原生类型属性,因此我们需要使用完美转发// 来保证x的原生类型属性(此处使用了完美转发)node* newnode = new node(std::forward<T>(x));node* cur = pos._pnode;node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}iterator erase(iterator pos){assert(pos != end());node* prev = pos._pnode->_prev;node* next = pos._pnode->_next;prev->_next = next;next->_prev = prev;delete pos._pnode;--_size;return iterator(next);}private:node* _head;size_t _size;};
}
使用list::push_back
int main()
{qwy::list<qwy::string> lt;qwy::string s1("111111");lt.push_back(s1);lt.push_back(qwy::string("222222"));lt.push_back("333333");return 0;
}
打印结果为
string(const string& s)
– 移动拷贝
string(const string& s)
– 深拷贝
string(const string& s)
– 移动拷贝
string(const string& s)
– 移动拷贝