传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
左值与左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),
1.左值可以获取它的地址。
2.左值可以对它赋值,但定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
3.左值可以出现赋值符号的左边,也可以右边。
左值引用就是给左值的引用,给左值取别名。
int a = 0;
int& b = a;
右值与右值引用
右值也是一个表示数据的表达式(如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等)
1.右值不能取地址。(与左值最重要的区别)
2.右值不可以对它赋值,不可以改变它。
3.右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,
右值引用就是对右值的引用,给右值取别名。
double func()
{return 1.3;
}
int main()
{double x = 1.1;double y = 1.2;//常见的右值10;//字面常量x + y;//表达式返回值func();//函数返回值//右值引用int&& a1 = 10;double&& a2 = x + y;double&& a3 = func();return 0;
}
底层上
总结:
语法上,引用都是取别名,不开空间,左值引用是给左值取别名,右值引用是给右值取别名。
底层上,本质上都是指针,
1.左值引用是存当前左值的地址。
2.右值引用是把当前右值拷贝到栈上的一个临时空间,存储这个临时空间的地址。
右值引用与左值引用特殊使用情况
左值引用不能直接给右值取别名,但是const 左值引用可以给右值取别名。
右值引用不能直接给左值取别名,但是move(左值)可以用右值引用进行取别名。
double func()
{return 1.3;
}
int main()
{//左值引用给右值取别名//int& r1 = func();错误//int& r2 = 10;错误const int& r1 = func();const int& r2 = 10;//右值引用给左值取别名int x = 0;//int&& rr1 = x;错误int&& rr1 = move(x);//需要move一下return 0;
}
右值引用所解决的问题(移动语义)
左值引用解决了什么问题?
1.传参拷贝的问题全部解决
传参时,代替c语言中指针的作用。
2.传返回值的问题解决了一部分
解决了传返回值时候产生拷贝的问题,减少了一次拷贝(当返回值是一个大的类对象时,减少拷贝能减少很大开销)。
但是局部对象返回(出了作用域销毁)的拷贝问题未解决。
在C++98中对传左值和右值都会被视为const类型的对象
例如:
void func(const int& i)
{cout << "void func(const int& i)" << endl;
}
//void func(int&& i)
//{
// cout << "void func(int&& i)" << endl;
//}
int main()
{int a = 0;func(a);//传左值func(10);//传右值return 0;
}
而在C++11中,有了右值引用的概念后,我们可以对函数写出右值引用的版本 ,这时传右值就会调右值引用版本的函数,传左值就会调左值引用版本的函数。
例如:
void func(const int& i)
{cout << "void func(const int& i)" << endl;
}
void func(int&& i)
{cout << "void func(int&& i)" << endl;
}
int main()
{int a = 0;func(a);//传左值func(10);//传右值return 0;
}
当然也可以使用move将左值属性转变成右值属性,来当成右值传参。
void func(const int& i)
{cout << "void func(const int& i)" << endl;
}
void func(int&& i)
{cout << "void func(int&& i)" << endl;
}
int main()
{int a = 0;func(a);//传左值func(10);//传右值func(move(a));//使用move将左值转成右值return 0;
}
本质上是对传入参数的类型(左值or右值)进行识别。
C++11对右值概念的解释可以形象地理解为以下两种:
1.纯右值(内置类型的右值)如: 1,a+b
2.将亡值(自定义类型的右值)如:匿名对象,传值返回函数/to_string(上面的例子)
场景一
string拷贝构造的场景——拷贝构造的移动构造版本
拷贝构造
//拷贝构造——左值string(const string& tmp){cout << "string(const string& tmp)——深拷贝" << endl;_str = new char[tmp._capacity + 1];strcpy(_str, tmp._str);_size = tmp._size;_capacity = tmp._capacity;}
如果传入的对象tmp是将亡值,深拷贝是一种浪费资源的行为,因为深拷贝后tmp对象就销毁了。
移动构造
使用右值引用进行移动构造。
//移动构造——右值(将亡值)string(string&& tmp){cout << "string(string&& tmp)——移动拷贝" << endl;swap(tmp);}
本质上是识别传入的值是右值(将亡值),然后函数内部对该右值(将亡值)进行特殊处理。
注:右值被右值引用后,右值引用的属性是左值
总结:
浅拷贝的类不需要移动构造。
原因:通常是一些内置类型,直接拷贝代价不大。
深拷贝的类需要移动构造。
原因:传入的是将亡值时,重新开空间构造对于一些大对象代价过大,不如直接将 将亡值 的资源转移给要构造的对象。
场景二
在这个场景中,C++11有了右值引用和移动语义,大大减少了传值返回的拷贝。
考虑这样一个场景,在string容器中有个成员函数是 to_string ,它的功能是将一个数值转换成string类型的字符串,以下是其简要模拟实现。
string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
C++98中,需要两次拷贝,但被编译器优化成一次拷贝。
C++11后加入右值引用,需要一次拷贝,但被编译器优化成无拷贝。
场景三
拷贝赋值重载的移动赋值版本(强行去掉编译器优化)
赋值拷贝
//赋值拷贝(现代写法)string& operator=(string& tmp){cout << "string& operator=(string& tmp)——深拷贝" << endl;string s(tmp);swap(s);return *this;}
int main()
{string s;s = to_string(10);return 0;
}
相比于上一个场景,先定义后赋值,可以避开编译器优化,这样我们直接可以看到拷贝过程。
结果是两次拷贝,刚好是如下图这样的情况。
移动赋值
//移动拷贝string& operator=(string&& tmp){cout << "string& operator=(string&& tmp)——移动赋值" << endl;swap(tmp);return *this;}
int main()
{string s;s = to_string(10);return 0;
}
这样就是一次深拷贝,一次移动赋值不产生拷贝。
移动将亡值的资源,并且把不要的空间给将亡值,让将亡值释放时刚好释放不要的空间,一举两得。
总结:左值引用没有解决的问题,右值引用解决了,深拷贝对象传值返回只需要移动资源,代价很低
C++11以后,所有的容器都增加了移动构造,移动赋值和插入。有值转移的地方都可以考虑右值引用版本。
场景四
int main()
{//右值被右值引用以后,右值引用r是左值。int&& r = 10;r++;//左值可以被修改
}
以list容器中的复用insert的push_back为例。
iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return newnode;}
void push_back(const T& x){insert(end(), x);}
以下是右值引用版本
iterator insert(iterator pos, const T&& x){//右值引用版本代码……return newnode;}
//右值引用版本void push_back(T&& x){insert(end(), x);}
//右值引用版本void push_back(T&& x){insert(end(), move(x));//将左值move成右值}
法二:完美转发
完美转发
模版中的&&——万能引用
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
例子:
template<typename T>
void PerfectForward(T&& t)//这里是万能引用,传右值就是右值引用,传左值就是左值引用
{//代码
}
测试一下
void Func(int& x)
{cout << "void Func(int& x)----左值" << endl;
}
void Func(int&& x)
{cout << "void Func(int&& x)----右值" << endl;
}
void Func(const int& x)
{cout << "void Func(const int& x)----const左值" << endl;
}
void Func(const int&& x)
{cout << "void Func(const int&& x)----const右值" << endl;
}template<typename T>
void PerfectForward(T&& t)
{Func(t);
}
int main()
{int a = 10;PerfectForward(a);//传左值PerfectForward(10);//传右值return 0;
}
但是结果
原因是传右值时,右值被右值引用,右值引用属性是左值,导致再去调Func函数时传的是左值。
如何解决这个问题?
前文中的一种方式是move一下,将左值move成右值,但是对于这样的场景是不适用的,这里是要测试传的是左值还是右值,而move统统把所有值变成右值,这样就失去了测试的意义(传左值结果被move成右值,导致测试结果是右值)。
这里C++11提供了一个 std::forward 完美转发在传参的过程中保留对象原生类型属性,它的作用是保持属性:如果本身是左值就不变,如果本身是右值,右值被右值引用后,右值引用是左值,转成右值,相当于move一下。
template<typename T>
void PerfectForward(T&& t)
{Func(std::forward<T>(t));
}
这样也就弥补之前场景四的特殊细节的情况
总结:move与forward的区别
一、move 左值属性——>右值属性
二、forward 保持属性
1.如果本身是左值就不变,
2.如果本身是右值,右值被右值引用后,右值引用是左值,转成右值,相当于move一下