C++工程代码中经常会看见这样的函数入参:
void fun(int & a, int* b){
...
return;
}
上面的函数使用了引用作为函数的入参;
左值引用和右值引用
有时候还会见到更加奇怪的变量定义:
int a = 10;
int &b = a;
int &&c = 10;
上面代码内b就是左值引用了,而c就是右值引用;
c++左值与右值
int a = 10;
c++代码原本就有左值和右值之分,其中上面代码第一行变量a就是左值,而数字10就是右值;
一般来说左值在内存中是有分配空间的,而右值很有可能因为只是中间量,可能只存在cpu某个寄存器内而没有具体内存地址;
简单来说,在‘=’左边的是左值,而在‘=’右边的就是右值。
回到引用的说明,引用是变量的别名,它使用不同名称“指向”了内存的某一具体地址;显然根据上面左值右值的说明可以发现,如果某个变量没有在内存内(右值),而我们又想对他定义引用,怎么办?这就有了右引用的用武之地;
int a = 10;
int &&c = 10;
正常情况下上面的代码中,第一行的右值“10”的生存周期只在当行有效。在执行第二行时,对应的寄存器或者内存会释放掉。程序会申请一个内存地址叫做‘a’,地址内存放10。
对于第二行,右值“10”在定义c后,会转化为左值被c引用,并且在执行完当前行后,“10”继续存在在内存中(有地址被c记录)直到c被释放,显然c解决了右值生存周期的问题。
那为什么需要右值引用这种看似很无聊的操作呢:
减少不必要的拷贝,同时完成必要的数据转换!
来个例子
#include <iostream>
using namespace std;
int fun(int &v1, const int &v2){
v1 = v2 + 1;
cout << "-------入参引用--------" << endl;
cout << " v1 =" << v1 <<" "<<" &v1 = "<<&v1<< endl;
cout << " v2 =" << v2 <<" "<<" &v2 = "<<&v2<< endl;
cout << "-----------------------" << endl;
return 0;
}
int main()
{
int v1 = 1;
int v2 = 1;
cout << "-------入参定义--------" << endl;
cout << " v1 =" << v1 <<" "<<" &v1 = "<<&v1<< endl;
cout << " v2 =" << v2 <<" "<<" &v2 = "<<&v2<< endl;
cout << "-----------------------" << endl;
fun(v1, v2);
return 0;
}
-------入参定义--------
v1 =1 &v1 = 0x7fff062557dc
v2 =1 &v2 = 0x7fff062557d8
-----------------------
-------入参引用--------
v1 =2 &v1 = 0x7fff062557dc
v2 =1 &v2 = 0x7fff062557d8
-----------------------
const 引用的特殊性
上面例子中const修饰函数入参,除了不让修改和普通入参引用似乎没什么区别。
如果把变量v2修改为float,我们发现编译并不会报错,修改后的代码如下:
#include <iostream>
using namespace std;
int fun(int &v1, const int &v2){
v1 = v2 + 1;
cout << "-------入参引用--------" << endl;
cout << " v1 =" << v1 <<" "<<" &v1 = "<<&v1<< endl;
cout << " v2 =" << v2 <<" "<<" &v2 = "<<&v2<< endl;
cout << "-----------------------" << endl;
return 0;
}
int main()
{
int v1 = 1;
float v2 = 1;
cout << "-------入参定义--------" << endl;
cout << " v1 =" << v1 <<" "<<" &v1 = "<<&v1<< endl;
cout << " v2 =" << v2 <<" "<<" &v2 = "<<&v2<< endl;
cout << "-----------------------" << endl;
fun(v1, v2);
fun(v1,2);
return 0;
}
代码执行结果如下:
-------入参定义--------
v1 =1 &v1 = 0x7fff7782a864
v2 =1 &v2 = 0x7fff7782a860
-----------------------
-------入参引用--------
v1 =2 &v1 = 0x7fff7782a864
v2 =1 &v2 = 0x7fff7782a868
-----------------------
-------入参引用--------
v1 =3 &v1 = 0x7fff7782a864
v2 =2 &v2 = 0x7fff7782a86c
-----------------------
发现编译器通过const int & v2 自动帮我们完成了数据类型转换。(个人理解是const关键字说明该引用不会影响函数外的变量且会申请新的内存地址变为左值,所以自动完成内存转换不会有任何风险)。
这里可以尝试修改v1的类型为float 或者在fun函数的第一个入参为右值均无法编译通过。
当然其实在复杂的工程代码上,有些更加变态的代码会有如下需求:
int fun(const T &a); // 函数申明
/**/
int b1 = fun(new T);
T t;
int b2 = fun(t);
其中的差别需要积累经验慢慢体会。
其他
写法上const关键字可以放在任何位置;常引用随便绑;
int &a = 2; // 左值引用绑定到右值,编译失败
int &&a = 2; // 右值引用绑定到右值,编译ok
int b = 2; // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值,编译ok
const int d = 2; // 常量左值
const int &e = c; // 常量左值引用绑定到常量左值,编译ok
const int &f =2; // 常量左值引用绑定到右值,编程ok
int const &g =2; // 和上一句等价,编程ok
右值引用不能直接绑定到左值,如果要绑定可以使用std::move()函数;
int a;
int &&r1 = a; // 编译ok
int &&r2 = std::move(a); //编译ok