在 cpp 中,引用是一个别名,它是一个对象的另一个名字。引用和指针的区别在于,引用不是一个对象,它只是一个对象的别名。引用一旦初始化之后,就不能再绑定到另一个对象,这一点和指针不同。

我们在使用的过程中,可以简单地将其理解为 “引用即别名”。对于某个变量,我们为它起一个另外的名字,使用它就是在使用原变量。

# 引用的定义

引用的定义格式如下:

type &name = object;

其中, type 表示引用的类型, name 表示引用的名字, object 表示引用的对象, & 表示引用符号。这样定义之后, name 就是 object 的一个别名, nameobject 共享 同一块 内存空间。

对于同一个对象 / 变量,可以定义多个引用,这些引用都是这个对象的别名,它们共享同一块内存空间,但是最初的那个对象 / 变量的名字仍然是它的名字。因此,我们可以对某个变量创建多个引用,分别用于不同的用途。

值得注意的是,引用的使用有一定的约束条件:

  1. 引用在定义时必须初始化。
  2. 引用一旦初始化后,就不能更改,引用必须初始化为合法的内存空间。

# 引用的使用

以下是一个引用的使用示例:

int main()
{
  int a = 10; // 定义一个整型变量 a,初始化为 10。cpp 会为其分配内存空间。
  int &b = a; // 定义一个整型引用 b,初始化为 a。b 和 a 共享同一块内存空间。
  int c = 20;
  b = c; // 将 c 的值赋给 b,由于共享内存空间,此时 a 的值也会变为 20。
  cout << "a = " << a << endl;
  cout << "b = " << b << endl;
  cout << "c = " << c << endl;
  return 0;
};

引用最常见的使用场景是作为函数的参数。我们都知道,在 cpp 中对于函数的传参,有两种主流方式:

  1. 值传递:将实参的值拷贝一份传递给形参,在函数体内部对形参的修改不会影响实参。通常只将计算结果返回给调用者,本质偏向于 纯函数
  2. 指针传递:将实参的地址传递给形参,通过指针可以修改实参的值。通常用于需要修改实参的情况。

引用传递,则是将实参的引用传递给形参, 通过引用可以修改实参的值 。引用传递的优点是既能够避免数据拷贝,又能够修改实参的值,主要就是得益于引用的特性 —— 共享内存空间。

以下是一个引用传递的示例:

#include <iostream>
using namespace std;
// 交换函数,用于交换两个整型变量的值
void swap3(int &a, int &b)
{
  int temp = a;
  a = b;
  b = temp;
}
int main()
{
  int a = 10;
  int b = 20;
  cout << "原值:a = " << a << endl;
  cout << "原值:b = " << b << endl;
  swap3(a, b);
  cout << "现值:a = " << a << endl;
  cout << "现值:b = " << b << endl;
  return 0;
};

在上述代码中,我们定义了一个 swap3 函数,用于交换两个整型变量的值。在函数体内部,我们通过引用的方式传递了两个整型变量的引用,这样在函数体内部对这两个变量的修改会影响到实参。

看起来和指针传递很像,但是引用传递的 语法更加简洁,而且不需要在函数体内部进行解引用操作。因此,引用传递是 cpp 中常用的一种传参方式。

当然,引用本身也可以作为函数的返回值。在上述说明中,我们知道了引用本身是一个别名,它是一个对象 / 变量的另一个名字,我们对其调用,实际上就是拿到了这个别名而已。也正是基于这个特性,引用函数本身可以作为 左值,并对其进行赋值操作。

#include <iostream>
using namespace std;
int &test02()
{
  static int a = 10; // 静态变量保存在全局区,函数执行完后不会被释放
  return a;
}
int main()
{
  int &b = test02(); // 引用作为函数的返回值
  cout << "b = " << b << endl; //b 的值为 10
  b = 1000; // 通过引用修改静态变量的值
  cout << "b = " << b << endl; //b 的值会变为 1000
  test02() = 10000; // 通过引用修改静态变量的值
  cout << "b = " << b << endl; //b 的值也会变为 10000
  return 0;
};

如果函数的返回值为一个引用,返回的必须是定义在全局区的变量。因为函数执行完后,局部变量会被释放,返回的引用就会指向一个无效的内存空间,这样会导致程序崩溃。

# 引用的本质

引用的本质是 指针常量 ,它是一个指针,只是在定义时不需要使用 * 符号。也就是说,当 cpp 解析代码遇到 & 符号后,会进行如下的转换操作:

int a = 10;
int &b = a;
// ↓转换为↓
int a = 10;
int *const b = &a;
b = 20;
// ↓转换为↓
*b = 20;

const 操作符表示常量,修饰指针时表示指针的指向不可变,但是指针指向的值是可以改变的。因此,引用在定义时必须初始化,且初始化后不能更改。

# 常量引用

常量引用的主要作用是 防止被调用函数修改实参的值,防止误操作。在函数的参数列表中,如果我们不希末修改实参的值,可以将形参定义为常量引用。

#include <iostream>
#include <windows.h>
using namespace std;
// 打印数据函数
void showValue(const int &val)
{
  //val = 1000; // 对传进来的引用进行修改,会影响到实参!!!
  // 可以使用 const 修饰形参,防止误操作
  cout << "val = " << val << endl;
}
int main()
{
  SetConsoleOutputCP(CP_UTF8);
  // 常量引用:修饰形参,防止误操作
  int a = 10;
  showValue(a);
  //int &ref = 10; // 非常量引用的初始值必须为左值
  // 加上 const 之后,编译器将代码修改为:int temp = 10; const int &ref = temp;
  const int &ref = 10;
  //ref = 20; // 表达式必须是可修改的左值,加入 const 变为只读、不可修改
  return 0;
};