欢迎来到博主的专栏:c++杂谈
博主ID:代码小豪
文章目录
- 左值和左值引用
- 右值和右值引用
- 右值
- 右值引用
- 右值引用的使用场景与意义
右值引用是c++11标准推出的新特性,在此之前,引用都是左值引用。为了弄清楚什么是右值引用,首先我们要知道什么是右值
左值和左值引用
左值是c语言就有的概念。在c++中,左值通常是一个变量,一个指针,对象,亦或者是一个函数的引用返回值。
int a;//变量
const int b=0;
int* p = &a;//指针
std::string str;//对象
str[0];//函数返回的引用
对于左值,左值引用便是左值的引用类型,左值引用只能引用左值。
//左值引用
int& ra = a;//变量的引用
const int& rb = b;
int*& rp = p;//指针的引用
std::string& rstr = str;//对象的引用
char& rch = str[0];//函数返回值的引用
那么关于左值,有没有更好的区分方法?答案是有的,一切的左值都能取地址。比如:
int* pa = &a;
int** ppa = &pa;
std::string* pstr = &str;
char* pch = &str[0];
右值和右值引用
右值
右值也是C语言时期提出来的概念了,可能有人会说了,赋值操作符(=)右边的值都是右值,左边的都是左值,这并不难区分。但是事实是右值是赋值操作符(=)右边的值,但是赋值操作符右边的值可以是左值。因此赋值操作符右边的值不一定都是右值。
在c++当中,右值通常是一个字面常量(常量并非都是右值,比如const修饰的变量就是左值,只有字面常量则是右值),函数传值返回的返回值,类型转换产生的临时变量,以及匿名对象。
int x = 10, y = 20;
10;//字面常量
x + y;//算数表达式的结果
fmin(x,y);//标准库的函数,其返回值是double类型,传值返回
(double)x;//类型转换产生的临时变量
std::string("11111");//匿名对象
左值引用不能引用右值。但是const修饰的左值引用可以引用右值。
int& r1 = 10;//error
double& r2 = (double)x;//error
std::string &= std::string("11111");//error
double& rd = fmin(x, y);//error
const int& r1 = 10;//ok
const double& r2 = (double)x;//ok
const std::string &r3= std::string("11111");//ok
const double& rd = fmin(x, y);//ok
那既然const修饰的左值引用可以引用右值,那为什么还要设计一个右值引用呢?首先,博主先给出一个结论。const修饰的左值引用!=右值引用。
要知道右值引用是c++11才推出的特性,而左值引用在c++设计之初就有的东西。因此右值引用推出的目的并非单纯的是引用右值。
说的有点远了,之所以const修饰的左值引用可以引用右值,是因为有些函数的参数会写成左值引用类型,因为这可以提高程序的运行效率。比如vector的push_back函数。
void push_back (const value_type& val);
c++的设计这希望这些以左值引用传参的函数,也能用右值来调用。因此允许const修饰的左值引用可以引用右值。
右值引用
右值引用就是在类型生命之后加上(&&),这表示是某个类型的右值引用。
int&& rr1 = 10;//ok,rr的意思是right reference(右值引用的缩写)
int&& rr2 = x + y;//ok
int&& rr3 = (double)x;//ok
std::string&& rr4 = std::string("1111");//ok
和左值引用一样,右值引用不允许引用左值。
int&& rrx = x;//error 右值引用不能引用左值
std::string&& rrstr = str;//error 右值引用不能引用左值
double&& rrd = fmin(x, y);//ok
但是标准库存在一个函数move(),能让左值变成右值引用的类型。为什么要这样做呢?博主在后面再谈吧。
std::string&& rrstr = move(str);//ok
int&& rrx = move(x);//ok
右值引用的使用场景与意义
首先我们要明白一点,右值不能取地址,不代表右值没有地址。在计算机硬件层面上讲,所有的数据都是二进制,竟然右值可以与左值一起参与计算,那么在硬件当中,右值一定也有其对应的二进制存储,所以右值不能取地址是语法方面的限制,而非右值不存在地址。
既然右值可以有地址,那么右值引用的作用给这个右值的地址起一个别名,使得右值可以被利用。那么问题来了,什么情况下需要用到右值引用呢?
无论是左值引用,还是右值引用,其核心目的只有一个,即减少拷贝。既然c++11推出了右值引用,那么右值引用一定解决了一些左值引用无法解决的情况。因此在此之间,我们先来看看左值引用的缺点是什么。
我们以string类为例(这里的string类是博主自己写的,因为使用标准库的string体现的不够明显)。该string类的拷贝构造函数和operator=函数如下:
mystring::mystring(const mystring& str)
{
mystring tmp(str._str);//深拷贝
swap(tmp);
}
mystring& mystring::operator=(mystring s)//参数s会导致深拷贝
{
swap(s);
return *this;
}
假如我们调用这个函数:
mystring makestring(const char* str)
{
mystring str(str);
return str;
}
int main()
{
mystring str = makestring("hello world");
return 0;
}
从上图的示意图可以看出,首先,由于返回值是str的拷贝,因此会调用以此拷贝构造函数,然后又会调用operator=进行以此拷贝,而string对象是一个需要深拷贝的类,因此这两次拷贝导致的时间开销特别大,如果将返回值改成string&(左值引用),那么又会因为str超出了生命周期,导致对象被销毁,数据不能成功被拷贝。
那么这也是左值引用不能解决的麻烦之一,那就是对传值返回的函数的优化还不够大。为什么这么说呢?
我们思考一个问题,那就是右值和左值的的区别是什么?或者说左值和右值的特性有什么区别?ok。右值和左值最大的区别在于,右值的生命周期比左值少多了,比如对于局部变量来说,其生命周期是函数的调用堆栈,或者for,if等语句的语句块,对于堆区的变量来说,其生命周期则是从new出来到delete掉该变量的整个区间。而右值的生命周期则仅仅只存在一行语句当中。换句话说,左值的生命周期远比右值要长。
因此对于左值引用,我们的程序就要采取对待左值的策略:
由于这个左值会在程序当中存在很长一段时间,因此如果拷贝一个左值(如拷贝构造。operator=),我们不会去做可能损毁这个左值的操作,因为我们不能确保这个被损坏的左值不会在后续的程序当中使用,如果该左值在后续的程序当中被使用了,那么造成的后果轻则逻辑错误,重则程序崩溃。
而右值由于其生命周期很短,后续的程序当中其不会再参与计算。所以右值对于我们而言,还有一个隐藏的价值,那就是右值当中存储的数据。比如我们可以设计一个针对右值的构造函数
mystring::mystring(mystring&& str)
{
char* tmp = str._str;
str._str = _str;
_str = tmp;
_capacity = str._capacity;
_size = str._size;
}
由于str是即将销毁的右值,出了表达式mystring str = makestring("hello world");
就要调用析构函数销毁的对象,也就是说str的生命周期只存在于短短一行代码当中。那么本着白用白不用,用了也白用,白用谁不用的态度。那么我们来一手卸磨杀驴吧,既然你str的生命短暂,将str进行魔改也不会影响后续的程序。于是这个右值引用的构造函数就来了一个很大胆的操作。
既然右值即将销毁,其当中存储的数据也要随之销毁,那么还不如将存储的数据直接给左值吧。左值会带着你的数据继续活下去的,这样做的好处在于,用移动数据来替代深拷贝,可以节省下大量的时间开销,提升程序的运行速率。这种操作称为移动。关于移动构造和移动赋值都是c++11推出的产物,与移动相关的函数博主放在其他文章。
拷贝左值引用的情况,是不敢这么做的,因为拷贝一个左值要考虑到这个左值还有很长的生命周期,我们要考虑到这个没有数据的左值会对程序造成什么样的破坏。因此拷贝左值引用是采取深拷贝的策略。
这也就是右值引用的价值所在了,既然右值命不久矣,那么不如直接以移动代替拷贝,以达到节省时间的目的。而对于某个不想再用的左值,则可以通过move函数来让这个左值发生移动构造。
ly::mystring str1 = makestring("hello world");
ly::mystring str2 = move(str1);//将str1强制转换为右值引用类型
因为程序员可以在控制某些左值在后面阶段不再使用,因此move()函数的作用实际上是让程序员决定一些左值的生杀大权,因为一旦左值被视作了右值引用,那么就会触发移动构造,将这个左值的数据给掏空,但是注意这个左值的生命周期不会被改变,因此被move过的左值就要注意不要在后续的程序当中被使用了。
那么我们最后在回到这个函数
mystring makestring(const char* str)
{
mystring str(str);
return str;
}
int main()
{
mystring str = makestring("hello world");
return 0;
}
由于存在了移动构造,因此str不需要再深拷贝makestring的返回值了,因为这个函数的返回值是一个右值,因此直接调用移动构造,节省了深拷贝的时间开销。
由于对右值进行利用可以提高程序的运行效率,因此STL中的大量函数也新增了大量右值引用的代码,比如序列式容器中的push_back,关联式容器中的insert,以及容器的move构造。