C++整了这么一套花活,最终的目的就是:减少拷贝,节省内存,提高运行效率。
在看本文之前,如果你能清晰区分下面前两个函数的异同,并知道为啥第三个函数调用非法,你就可以继续阅读本文了。
void F1(int a){a++;}
void F2(int& a){a++;}
F2(100); //compile error
一、左值和右值
左值就是一个数据的表达式,这个表达式指向一块内存,并且允许我们对这块内存取地址,可以这么认为,可以取地址的对象或变量就是左值,左值通常可以被赋值,但被 const 修饰后的左值,不能给它赋值,但是仍然可以取它的地址。
右值不能取地址,也不能被赋值,即:右值不能出现在赋值表达式的左边。
根据上述定义,下面的a、p、b都是左值
int a = 3;
int* p = &a;
const int b = 2;
问:*p是左值还是右值。*p,又叫解引用,就是解释引用,或者说直接去寻找指针所指的地址里面的内容,于是这个表达式指向一块内存,内存的地址就是p的值,因此解引用也是个左值。
下面这些都是右值
double x = 1.3, y = 3.8;
10; // 字面常量
x + y; // 表达式返回值
fmin(x, y);
问下述函数是左值还是右值?
int a=10;
int& Incerease(int &b)
{
b++;
return b;
}
我们可以写出如下代码,因此int& Incerease(int &b)是个左值。
Incerease(a)=50;
二、左值引用和右值引用
记住一句话:无论左值引用还是右值引用,都是给对象取别名 。
左值引用:
左值引用就是对左值的引用,给左值取别名,左值引用使用一个&来表示。主要作用是避免对象拷贝。
- 左值引用只能引用左值,不能直接引用右值。
- 但是const左值引用既可以引用左值,也可以引用右值
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;
const double& d1=10.7
右值引用
右值引用就是对右值的引用,给右值取别名,具体表示就是在变量类型名称后加&&。
int&& n1=10;
double&& d1=x+y;
double&& d2=fmin(x,y);
右值引用
引用右值
,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)
- 右值引用只能引用右值,不能直接引用左值。
- 但是右值引用可以引用被move的左值。
无论是左值引用变量还是右值引用变量,都是左值。
三、使用左值引用和右值引用的意义
1. 不使用左值引用
看下图,如果在函数Func1中不使用引用的话,当str被传入Func1的时候,在Func内部首先产生一次拷贝构造,外部的str拷贝给函数内部的str,数字1所示。
在str被处理以后,该str将被传回到函数外部,于是在数字2处也产生一次拷贝构造,内部的str传出到外部的一个临时变量。
该临时变量也被调用一次拷贝构造,给外部的str1赋值,数字3,于是我们可以看到,一共产生了三次调用拷贝构造函数(不考虑编译器优化的情况)
2. 使用左值引用
如果传参使用左值引用,那么只有两次拷贝构造函数的调用
如果函数返回一个左值引用,那么就只有一次拷贝构造函数的调用
2. 使用右值引用
在讲右值引用之前,先来讲一下一个类中都有哪些函数。在C++ 11之前,编译器通常会为一个类准备4个函数,如果你不为这个类准备的话:
默认无参构造函数
拷贝构造函数
赋值运算符重载
析构函数。
在c++11之后,有增加了两个函数:移动构造函数和移动赋值运算符。这两个函数都比相应的拷贝构造函数和赋值运算符重载要高效的多。
看下面这个例子:
在Func1被调用的时候,1处有一个右值引用,右值引用只是给右值(str)起一个名字(str2),在新名字上的操作等同于对原始右值的操作。参见4处的两个对象的地址完全一致。
2处有一个右值返回,函数返回一个右值引用的对象(str2, 也就是str),是个右值。这个对象要赋值给一个新的对象str1,于是右值移动构造函数被调用。在3处有一个移动构造函数。当然新的对象str1除了把str的内容借过来,已经与str是两个完全不同的两个对象了,于是我们看到str内容为空了。
move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值,以便可以通过右值引用使用该值。看下面的例子,r是右值引用,就是x的别名,我们可以通过操作r来改变x。
我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数
四、万能引用
在此之前我们需要知道什么是万能引用:
确定类型的 && 表示右值引用(比如:int&& ,string&&),
但函数模板中的 && 不表示右值引用,而是万能引用,模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用。
注意区分右值引用和万能引用:下面的函数的 T&& 并不是万能引用,因为 T 的类型在模板实例化时已经确定。
template<typename T>
class A
{
void func(T&& t); // 模板实例化时T的类型已经确定,调用函数时T是一个确定类型,所以这里是右值引用
};
让我们通过下面的程序来认识万能引用:
template<typename T>
void F1(T& t1)
{
cout<<t1<<endl;
}
int main()
{
int n=1;
F1(n); // good
F1(1); //error
return 0;
}
嫩F1(1)导致编译错误。为了修正这个错误,我们重载另一个函数:
template<typename T>
void F1(T& t1)
{
cout<<t1<<endl;
}
template<typename T>
void F1(T&& t1)
{
cout<<t1<<endl;
}
int main()
{
int n=1;
F1(n); // good
F1(1); //error
return 0;
}
能否把两个函数合二为一,可以:
使用T&&
类型的形参既能绑定右值,又能绑定左值。
但是注意了:只有发生类型推导的时候,T&&才表示万能引用;否则,表示右值引用
五、完美转发
完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。
因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward(C++11),它在传参的过程中保留对象的原生类型属性。
这样右值引用在传递过程中就能够保持右值的属性
继续利用前面的一个例子,我们把函数的返回类型不使用右值引用,这次返回一个临时变量,这个临时变量会被拷贝构造函数赋值给str1. str2是一个右值引用,但str2本身是一个左值。因此return str2 就会通过拷贝构造str2,返回一个临时变量。这也就是我们说的右值引用失去了右值的属性,成为了一个左值,如何能保持str2的右值属性呢?这就引入了完美转发。
下面看如何使用完美转发来保持str2的右值属性,
总结:
右值引用和左值引用类似,都是为一个量(对象)起了个别名,操作右值引用和左值引用等价于操作被引用量本身。