c++中的引用

在c++中,为了改变c语言中指针使用的种种麻烦与困难,定义了一种新的复合类型---引用,引用也分为很多种,比如左值引用、右值引用、常量引用等等。

左值引用

一般情况下,大家见的最多的就是左值引用。右值引用通常与移动操作结合起来的比较多。其中左值引用使用&符号,右值引用使用&&

下面是左值引用的两个大类。 ## 普通引用 引用为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名。如下:

1
2
3
4
int i1 = 1024;
int &r1 = i1; //r1是i1的引用(也就是i1的别名)
int &r2; //错误!引用必须初始化,引用是无法复制的,因为对引用赋值其实是对引用绑定的变量赋值。
int &r3 = 1024; //错误!普通引用不能绑定常量

如下是几个注意问题: 1. 引用必须初始化!因为引用是无法赋值的,对引用赋值其实是对引用绑定的变量赋值。

  1. 普通引用(非常量引用)是不能绑定常量的!因为可以使用引用对绑定的变量进行赋值,所以如果引用绑定了常量,就会有改变常量的操作发生的可能。

  2. 没有引用的引用,但是可以通过引用绑定到一个对象(如int i=1; int &a=i; int &b=a;这样是可以的,但是b绑定的是i)。因为引用本身只是一个别名,不是一个对象,所以不能定义引用的引用。

  3. 引用的类型必须与绑定的对象严格匹配

  4. 引用只能绑定在对象上,不能与字面值或者某个表达式的计算结果绑定

拓展:定义内置数组的引用。

因为内置数组不可拷贝,所以内置数组的引用就会十分有用,并且高效。使用方法如下:

1
2
3
int arr[10] = {1};	//第一个元素为1,其他元素默认初始化为0
int (&arrRef)[10] = arr; //其实就是多取了一个名
cout<<arrRef[0]; //输出1

常量引用

科普一下,const可以分为顶层const与底层const两种。一般对象只会有顶层const,表示对象本身是常量不能修改;而对于指针与引用变量除了顶层const外(表示自身是常量,一般只对指针而言,引用一般只关心底层const),还有底层const,表示自身指向或者引用的对象是常量。

此处常量引用指的是底层const,即不能通过此引用改变绑定的对象。常量引用有几个特殊的地方:

  1. 常量引用可以绑定常量,也可以绑定非常量(但是不能通过此引用改变这个非常量的值);

  2. 常量引用的类型不必与绑定的对象严格一致,只要可以转换成常量引用的类型即可;

  3. 常量引用可与字面值或者某个表达式的计 算结果绑定

其中第一条很容易理解,第二条、第三条的原因可由下面例子知道原因:

1
2
3
4
5
double d1 = 3.14;
const int &r1 = d1; //正确!
//上式等价于如下:
const int temp = d1; //先生成一个临时变量(与常量引用类型一致)
const int &r1 = temp; //让r1绑定这个临时量
当所要绑定的对象与常量引用类型不一样时,编译器便会创建一个临时变量,并将原变量转换类型赋值给这个临时变量,常量引用最终绑定的是这个临时变量。当绑定对象是某个表达式时执行操作一样。注意!这种操作只在必要时发生,如果绑定的是一个类型相符的变量,则直接进行绑定操作,不会有什么临时变量。(因此这个引用其实与原变量不再相关,即使原变量值被修改了,这个引用指向的值不变,其实常量引用就相当于一个字面值常量)

右值引用

新标准中为了支持移动操作,有了右值引用的概念。顾名思义,右值引用就是绑定到右值的引用。

1
2
3
4
5
6
int i = 42;	
int &r = i; //正确:r是变量i的引用
int &&rr = i; //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i*42; //错误:不能将一个普通引用绑定到一个右值上
const int &r3 = i*42; //正确:可以讲一个常量引用绑定到一个临时变量上
int &&rr2 = i*42; //正确:将右值引用绑定到一个右值上

由上我们可以看出,左值一般有持久的状态,是一个稳定的对象;而右值要么是字面值常量,要么是表达式求值过程中创建的临时变量。

此处需要说明的一点是变量表达式都是左值,我们不能将一个右值引用绑定到一个右值引用上,因为右值引用本身是左值,是可以通过右值引用改变它指向对象的值的。如下:

1
2
3
int &&r1=42;	//正确:42是一个右值
int &&r2=r1; //错误:右值引用类型的变量表达式r1是一个左值
//注意:这样的操作在左值引用就是可以成立的。

std::move()

提到右值引用,就不得不提std::move()这个函数(头文件utility)。这个函数的作用就是将参数包装成一个右值

因为右值引用为左值,所以我们可以通过右值引用改变源对象的值:

1
2
3
4
5
6
int i = 1;
//int &&rr1 = i; 错误,不能绑定左值
int &&rr2 = std::move(i); //此处move返回一个右值类型的值被绑定到右值引用类型变量rr2
rr2 = 2; //正确;通过右值引用改变变量i的值
cout<<i<<endl; //输出2
//int &&rr3 = rr2; 错误,rr2是左值

通常肯定不会这样使用move,通常我们将move函数来辅助完成一个对象的移动操作,因此,往往调用move意味着就承诺:除了对源对象重新赋值或者销毁它外,我们将不再使用它。在对一个源对象做了move操作后,我们只能保证移后源对象结构依旧完好,可以正常销毁,但是它的值我们不能做任何假设。

万能引用

其实还有一种引用,那就是万能引用,万能引用形式和右值引用一样。万能引用只出现在型別推导的场景下,万能引用可以根据不同的语境选择是成为左值或者右值引用。

std::forward()

万能引用总是会搭配std::forward()使用,这个函数会根据语境选择是否将参数包装成一个右值。仅在实参原本是右值时才会。因此这中操作也叫做完美转发

forwardmove其实就是强制类型转换,只不过前者是有条件的转换,后者无条件转换。

参考资料

  • [1] C++ Primer(第5版)

c++中的引用
http://line.com/2018/10/09/2018-10-09-reference-cpp/
作者
Line
发布于
2018年10月9日
许可协议