引用是 C++ 中一个比较神奇的东西。在这之前或者说 C 语言中,一般是使用指针来减少传参所带来的不必要的开销。如函数传递的参数是数组或结构体时,使用指针会省很多事,毕竟传递的是地址。而 C++ 中引用变量的主要用途也是函数传参,子函数直接操作原始数据,而不是其副本,这样处理大型数据结构也会佷便捷。
指针
一维数组
以一维数组为例。众所周知数组名是是数组首元素的地址。因此调用子函数时,主函数传递的是数组首元素的地址,所以子函数接收的是地址,无法预知数组的长度,需要增加额外的参数指明数组元素的数量。
对于函数,一般用 int arr[]
这样的形式指明 arr
接收的是数组,这样的可读性强;换一种方法,因为传递的是数组首元素的地址,而数组首元素为 int
类型,地址是 int*
类型,因此可以用 int* arr
来接收一个数组,但是这样表意不明确。
1 |
|
二维数组
升级到二维数组,二维数组的类型本质就是指向『多个 int
组成的数组』的指针,因此参数的形式为 int (*arr)[4]
,而不是 int* arr[4]
。
- 前者是一个『由 4 个指向 int 的指针』组成的数组;即一个数组,数组元素是四个 int 指针;
- 后者是一个指向『由 4 个 int 组成数组』的指针;即一个指针,指向 4 个 int 的数组。为了更好的可读性,一般声明如下:
1 |
|
引用
二维数组的指针是不是感觉有点晕?先来看一下简单的引用:
1 | int a{11}; |
这样 b
和 a
就指向了相同的值和内存单元,只是名字不一样。此外,引用必须在声明的时候进行初始化,否则这个变量不知道指向哪个内存单元和值,但是指针可以先声明在赋值。
此外,声明一旦绑定,就无法在修改。可以通过初始化声明来设置引用,不能通过赋值来设置。如下所示的程序,只是对引用 b
进行了赋值,而不是修改引用。
1 | int a{11}; |
引用传参
回到主题,一般将引用用做函数传参时。主函数中的变量名是被调用函数中对应变量的别名,在调用时用实参初始化形参,因此引用参数被初始化为:函数调用时传递过来的实参。如下所示:
1 | void swap(int& a, int& b) { |
此外,传递引用时对类型的限制更加严格,以求和函数为例:
1 | void sum(double n) { |
换句话说,当实参和形参的类型不匹配时,将会生成临时变量传给形参。但是引用则不行,限制相对严格,sum(a + 6.3)
会报错,传递的实参是表达式不是变量,而引用不能绑定到表达式上,且此时不会生成临时变量。
但是当参数为 const 引用时,会创建一个临时的无名变量,临时变量的值初始化为 a + 6.3
,而后再将无名变量赋给引用:
1 |
|
也许你会有疑问,这个时候为什么会生成临时变量?const
为什么合理呢?如果引用参数是 const
,两种情况会生成临时变量:
- 实参类型正确,但不是左值,如
a + 6.3
这样的表达式 - 实参类型不正确,但可以转为正确类型,如
int
隐式转换为double
;double
到int
则错误
左值:左值是可以被引用的数据对象,变量、数组、元素等,非左值有字面常量,多项的表达式等。或者说,可以放在赋值语句左侧 and 能访问地址的就是左值,也就是说,赋值语句左侧是可修改的内存块,const 变量也是左值,只是不可修改。
回到原问题,如果形参加上 const
修饰,意思是函数只使用这个值,不修改这个值。即使因类型不匹配生成了临时变量,引用参数引用这个临时变量,都不会造成任何不好的副作用。但此时就是值传递而不是地址传递,因为要用临时变量来存储数值。所以也推荐尽可能使用 const
:
- 避免无意修改数据造成结果错误
- 能更好的接收实参,生成并使用临时变量
返回值为引用
对于传统的调用函数而言,返回结果的这个值被复制到临时位置,也就是产生值的副本,调用程序将使用这个值。如:
1 | int sum(int len) { |
而返回引用的函数实际上是返回被引用变量的别名。返回引用值时,并不产生值的副本。而是将返回值直接复制给接收函数的变量或对象,言简意赅,当函数返回引用类型时,没有复制返回值创建临时变量,相反,返回的是对象本身,并复制到接收变量那里。
对于一个大型的数据结构如结构体,将结构体复制到额外的地址的开销会很大;如果返回引用,将返回的引用的结构体直接赋值给接收值,避免额外的开销。
但是,避免返回指向临时变量的引用,临时变量在执行完毕后会消失,引用会指向乱七八糟的地址,就跟避免指向临时变量的指针一样。有两种解决方法:
- 使用 new,将数据放到堆区,不过内存模型的坑准备后续开
- 传递一个额外的参数,传递给函数的引用,将该参数返回。因此返回引用时,要求在函数的参数中,包含有以引用方式需要被返回的参数。
1 |
|
此外,非引用函数的返回值类型是右值,这种语句位于表达式的右侧,也无法通过地址访问这个值,也无法放到复制语句的左侧。因为返回值的地址在执行完毕后就消失了,也就是说无法引用。如果一定要引用返回值,将返回值类型声明为引用,这样返回的就是左值,就可以引用。
总结一下:当返回结果需要做为左值时,就要用引用返回。即重载函数的返回结果需要出现在赋值语句左边时,必须用引用返回。如果不用引用返回,那么重载函数的返回结果会是一个临时变量,临时变量是不能放在赋值语句左边的。
1 | // 错误,右值不能在赋值语句左侧 |
如果不想返回的引用被修改,就加 const
修饰:
1 |
|