引用

在讲解面向对象相关的知识前,我不得不补充一个非常重要且有用的概念——左值引用(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 函数中 ab 的值。这是为什么呢?

首先我们回顾一下函数传参的细节:用实参去初始化形参。所以,如果把调用 change 这个传参过程具体写出来的话就是:

int& c{main 函数中的 a};
int& d{main 函数中的 b};

这时问题已经浮出水面了。引用在进行初始化时,是将引入的名字绑定到用于初始化的变量上去,使得这个变量拥有一个别名。所以,这里是让 main 函数中的 a 拥有了一个别名 cmain 函数中的 b 拥有了一个别名 d。而 cd 这两个名字的作用域是 change 函数,所以我们可以通过在 change 函数中更改别名 cd 的值来更改 main 函数中 ab 的值。

这也就是第三种用来“跨函数”更改变量值的方法。(第一种是全局变量,第二种是运用指针。)这时,如果你在 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 类型的引用,而用于初始化它的变量 aint 类型的。这种写法是被允许的,它的效果是这样的:

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 将返回绑定到局部变量的左值引用视为编译错误;但语法上仍然允许返回绑定到局部变量的右值引用(尽管这是错误的)。

最近更新:
代码未运行