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