目录
一、左值引用 && 右值引用
1.1什么是左值?什么是左值引用?
1.2什么是右值?什么是右值引用?
1.3左值引用与右值引用比较
二、左、右值引用使用场景(为什么要有右值引用?)
2.1左值引用使用场景:
2.2右值引用使用场景(移动语义):
2.2.1移动构造
2.2.2移动赋值
2.2.3右值引用左值(move)
在上一篇章,我们讲了C++11提供的新接口,但是用的上的又很少,所以这些接口的意义又何在?实则,是为了这一篇章做准备,我们来继续学习
一、左值引用 && 右值引用
想必这个大家都很熟悉,在编译器上有时会看到一些左、右值这样的报错,左值引用虽然并不是c++11新增的,但是我们并没有真正去了解它,而右值引用是c++11新增的语法特性。不管是左值引用还是右值引用,都是给对象取别名。
1.1什么是左值?什么是左值引用?
左值:是一个表示数据的表达式(变量名或解引用的指针等),但是它有一定的规则:左值是可以被取地址+左值可以出现在赋值符号的左边。定义时const修饰后的左值,不能给它赋值,但是可以取它的地址。
注意:左值,并不代表它一定是在赋值符号的左边,它只是一个表示数据的表达式,通常出现在左边,要判断是不是左值,得判断是否可以取地址即可。
左值引用:对左值进行引用,即对左值取别名
通过代码来演示、验证:
#include <iostream>
using namespace std;
int main()
{//一下a、b、p、c都是左值int a = 1;int b = 2;int* p = new int(0);const int c = 3;//对上面左值的引用,可以看出左值在赋值符号的右边,要判断是否为左值,就判断是否可以取地址int*& ptr = p;int& ra = a;int& rb = b;const int& rc = c;int& pp = *p;cout << p << endl << a << endl << b << endl << c << endl;cout << ptr << endl << ra << endl << rb << endl << rc << endl << pp << endl;return 0;
}
输出结果:
1.2什么是右值?什么是右值引用?
右值:也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值等,同样有一定的规则:右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,且右值不能被取地址。右值就相当于本身是不可被修改的值,类似于常量,既然不可修改,就不能够被取地址。
右值引用:对右值的引用,即给右值取别名。这里就要引入新的引用符号,右值引用符号:&&
注意:虽然右值本身不能被取地址,但是当右值被取别名时,也就是赋值给了引用变量,这个引用变量是可以被取地址的,不要混淆了。
通过代码演示、验证:
#include <iostream>
using namespace std;
int fun(int a, int b)
{return 3;
}
int main()
{int a = 1;int b = 2;//右值,不可修改10;a + b;//对右值的引用int&& ra = 10;int&& sum = a + b;int&& ab = fun(a, b);//右值引用变量是可以被取地址的,可以被修改。int* aa = &ra;*aa = 5;//与此,ra也被改成了5//这些都是会报错的,右值是不可别修改的,=的左操作数必须是左值。/*10 = 1;x + y = 3;fun(a,b)=3*/cout << ra << endl << sum << endl << ab << endl << *aa << endl;return 0;
}
输出结果:
1.3左值引用与右值引用比较
左值引用总结:
1.左值引用只能引用左值,不能引用左值
2.但是const+左值引用既可引用左值,也可引用右值。
右值引用总结:
1.右值引用只能引用右值,不能引用左值
2.但是右值引用可以move以后引用左值(待后续讲解)
二、左、右值引用使用场景(为什么要有右值引用?)
2.1左值引用使用场景:
作参数和返回值都可以提高效率,当我们传参时,以及返回对象时,可以减少对象的拷贝从而提高了效率。例如:
#include <iostream>
using namespace std;
string fun(string& s)//使用左值引用做参数减少对象的拷贝
{s = "i love you too";return s;
}
int main()
{string str = "i love you";cout << str << endl;const string s = fun(str);cout << s << endl;return 0;
}
左值引用的缺陷:
通过一组代码来演示
#include <iostream>
#include <assert.h>
using namespace std;
namespace bit
{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){std::swap(_str, s._str);std::swap(_size, s._size);std::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=(const string& s) -- 深拷贝" << endl;/*string tmp(s);swap(tmp);*/if (this != &s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}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)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};bit::string to_string(int x){bit::string ret;while (x){int val = x % 10;x /= 10;ret += ('0' + val);}reverse(ret.begin(), ret.end());return ret;}
}int main()
{bit::string s = bit::to_string(1234);//拷贝构造(ret返回给to_string)+拷贝构造(to_string拷贝给s)优化为拷贝构造return 0;
}
当函数返回对象是一个局部变量,出了函数作用域就销毁了,就不能使用左值引用返回,只能传值返回,上述代码亦如此,ret是一个局部变量,只能传值返回,传值返回会导致至少1次的拷贝构造,如图:
当然新的编译器可能会优化一次拷贝构造,但这都是编译器做的事,并没有解决本质上的问题:对于拷贝构造而言,其实现是一种深拷贝。那么对此问题,右值引用便体现出了价值,c++11新增了两个默认成员函数(移动构造、移动赋值)如下
2.2右值引用使用场景(移动语义):
2.2.1移动构造
// 移动构造string(string&& s){cout << "string(string&& s) -- 移动拷贝" << endl;swap(s);}
对于移动构造而言,采用的是右值引用,可以直接将资源取过来(取别名),而不用进行深拷贝,所以,移动构造,就是窃取别人的资源来构造自己。如图:
注意:如果没有移动构造,调用就会匹配调用拷贝构造!
同理,赋值重载也是深拷贝,所以除了移动构造,还有移动赋值
2.2.2移动赋值
// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s)-- 移动赋值" << endl;swap(s);return *this;}
移动赋值也是采用右值引用,直接把资源取过来,而不用深拷贝。如图:
注意: 如果没有移动赋值,调用就会匹配调用赋值重载!
在c++11中,对于STL容器都是增加了移动构造和移动赋值的
2.2.3右值引用左值(move)
前面提到了,右值引用可以通过move实现引用左值,那么现在来具体讲讲它的用法。std::move()函数位于头文件中,该函数作用就是将一个左值强制转换成右值引用,然后实现移动语义。
如下,main函数前面的代码还是跟之前一样(加了移动构造和移动赋值)
int main()
{bit::string s = bit::to_string(1234);//移动构造bit::string s1("hello world");//由于s1是左值引用,可以被取地址,所以这是一个拷贝构造bit::string s2(s1);//对s1进行move处理以后,s1被强制转换成右值引用,那么该表达式就会调用移动构造//但是并不建议这样用,因为移动构造会将s3,s1的资源交换,s1就会被置空了。bit::string s3 = move(s1);return 0;
}
对于STL容器的插入接口也增加了右值引用。
总结一句话是右值移动语义针对的是深拷贝问题,对于浅拷贝没有任何意义。