引用
在讲解面向对象相关的知识前,我不得不补充一个非常重要且有用的概念——左值引用(lvalue reference),现阶段简称引用。
引用这个名字听上去很奇怪,但是实际上,引用就是给 C++ 中的变量起一个别名。典型的引用的声明语句是这样写的:
类型说明符& 引用名{变量名};
其中 {变量名}
是初始化器。举个例子如:
这是什么意思呢?意思是,为 类型说明符
类型的变量 变量名
起一个别名,这个别名就叫做 引用名
。比如刚才的例子中,我们给 int
类型变量 a
起了一个别名叫做 r
。这个过程就好比给孙悟空起了一个别名叫”齐天大圣“——我说孙悟空怎样怎样,齐天大圣就怎样怎样。比如:
r = 56;
cout << a << endl; // 输出 56
这里,我令 r
的值更改为 56
,但 r
实际上就是 a
的别名,所以刚刚的赋值语句完全就是对 a
赋值。所以最后输出 a
的值是 56
。
总结下来就是,当声明引用时,引用名
将成为初始化器中 变量名
的一个别名。这个行为可以称作“绑定引用到一个变量上”。之后,对引用进行各种操作完全就是在对其所绑定的变量进行操作。
那么读者可能会感到疑惑了:我为什么要给一个变量起别名,用原来的名字不好吗?实际上,引用主要的作用并不是单纯地起别名,而是通过“起别名”这个工作原理让函数传参更加灵活好用。
我们来看这个例子:
这个 change
函数我们已经在之前见过了,但这次的不同是参数类型变成了引用。当你编译运行之后你就能发现结果不一样了:change
函数确实改变了 main
函数中 a
和 b
的值。这是为什么呢?
首先我们回顾一下函数传参的细节:用实参去初始化形参。所以,如果把调用 change
这个传参过程具体写出来的话就是:
int& c{main 函数中的 a}; int& d{main 函数中的 b};
这时问题已经浮出水面了。引用在进行初始化时,是将引入的名字绑定到用于初始化的变量上去,使得这个变量拥有一个别名。所以,这里是让 main
函数中的 a
拥有了一个别名 c
,main
函数中的 b
拥有了一个别名 d
。而 c
和 d
这两个名字的作用域是 change
函数,所以我们可以通过在 change
函数中更改别名 c
和 d
的值来更改 main
函数中 a
和 b
的值。
这也就是第三种用来“跨函数”更改变量值的方法。(第一种是全局变量,第二种是运用指针。)这时,如果你在 change
函数调用语句中传入的实参不是一个左值的话,会导致编译错误:
引用仅仅是一个别名而已,它不是变量,不占用任何存储空间。所以不存在指向引用的指针,不存在引用构成的数组,不存在引用的引用。
这里
s
并不是引用r
的引用。r
已经是a
的别名了,所以int& s{r};
就等价于int& s{a};
。所以s
不过是a
的另外一个别名罢了。
绑定到只读类型的引用
先看下面的代码:
这里,引用 r
是绑定到 const int
类型的引用,而用于初始化它的变量 a
是 int
类型的。这种写法是被允许的,它的效果是这样的:
cout << r << endl; // 等价于 cout<<a<<endl;
r = 56; // 错误,因为引用绑定到只读类型
a = 56; // OK
就是说,如果引用绑定到了 const
限定的类型,那么就不能通过引用更改其所绑定变量的值。但是直接更改原来变量的值仍然是被允许的。这一通操作看上去会显得很迷惑——但是形如 const T&
的声明在将来会非常常见。这里暂且先不讨论其意义,留到后面再作具体解释。
引用无法在顶层被
const
限定(如int& const r{a};
是错误的)。
非常神奇地,形如
const T&
的引用可以绑定到右值上,如const int& r{42};
。
将引用作为返回值类型
接下来,我们看这样一段代码:
重点在 getGlobalVar
这个函数。这个函数的返回值类型是 int&
,也就是说,它返回了一个别名……?这是什么意思呢?
当函数返回值类型为引用时,表明整个函数调用表达式的结果相当于绑定到 return 表达式;
中的 表达式
的引用。比如在这里,getGlobalVar()
这个表达式就相当于绑定到变量 glbVar
的引用;因为函数中的 return 语句返回了它。正因如此,getGlobalVar()
就是 globalVar
的一个别名,因此它可以被赋值或修改(即这里的 += 2
)。这段代码的输出是 44
。
类似的写法还有:
由于 chooseVar
函数返回的是引用类型,所以 chooseVar(x)
相当于某个变量的别名。因此它可以被修改或者赋值,例如这里的 chooseVar(x) += 1
这个函数根据传入的实参,决定将整个调用表达式绑定到全局变量 a
还是 b
。因此总结下来,这段代码可以根据输入来选择对 a
自增还是对 b
自增。
虽然这种语法可以写出很神奇的代码,但错误使用反而会导致危险。比如下面的函数
注意 f
的返回值类型是一个引用,但是它 return
的是局部变量 x
,也就是说返回的那个引用实际上是绑定到了局部变量 x
上去。而 x
这个局部变量在函数返回之后就消亡了,也就是说 f
的返回值绑定到了一个已经不存在的变量上!进一步地,如果
这样写,引用 r
会绑定到 f()
上(也就是绑定到已消亡的 x
上),会导致未定义行为。像 r
这种绑定到已消亡变量的引用称为悬垂引用(Dangling reference),是最常见的错误之一。它本质上和悬垂指针的错误是类似的,但更难发现和调试。我在这里给出一个建议:在返回引用时,检查这个引用:
- 要么绑定到全局变量(或静态局部变量);
- 要么绑定到引用类型的形参;
- 要么来自
this
(还没讲,之后再说), - 否则很可能是一个悬垂引用错误。
C++2b 将返回绑定到局部变量的左值引用视为编译错误;但语法上仍然允许返回绑定到局部变量的右值引用(尽管这是错误的)。