Skip to content

C++ 引用

引用定义

在 C 语言中,如果使用指针变量作为函数参数,那么就可以将函数外变量的地址传递到函数内部,使得函数内可以操作函数外的数据;如果要给函数传递外部数组变量,那么就需要使用数组指针作为函数参数来传递数组的首地址。

在 C++ 中,保留了 C 语言的指针类型,但引入了一种比指针更加便捷的方式:引用(reference)。引用可以看做是该变量的别名,语法格式为:

type &ref_name = ori_name;

引用在定义的时候必须同时初始化,并且在之后不可再修改为引用其他变量。引用 ref_nameori_name 都指代同一块内存地址,对 ref_name 的修改,自然会反应到 ori_name 上。如果不希望通过引用修改原始数据,可以在定义的时候添加 const 修饰符,即常引用

const type &ref_name = ori_name;
type const &ref_name = ori_name;

引用的应用

函数参数

引用可以用来作为函数的参数,这样函数内外的实参和形参将指代同一份数据。如下代码通过引用传递交换了 num1num2 的值。可以看到,使用引用传递,在形式上会比指针更加美观。

#include <iostream>
using namespace std;

void swap(int &a, int &b) {
    int tmp = a;
    a = b;
    b = tmp;
}

int main() {
    int num1 = 2, num2 = 3;
    cout << num1 << ", " << num2 << endl;
    swap(num1, num2);
    cout << num1 << ", " << num2 << endl;
}

函数返回值

引用也可以作为函数的返回值,需要注意的是,这种情况下,返回值不能是函数的局部变量,否则,当函数调用结束,局部数据销毁,再次访问这个引用时,访问的数据就不存在了(见如下代码的 add_2)。

#include <iostream>
using namespace std;

int& add_1(int &a,int &b) {
    a += b;
    return a;
}

int& add_2(int &a,int &b) {
    int c = a + b;
    return c;
}


int main() {
    int num1 = 2, num2 = 3;
    int &num3 = add_1(num1, num2);
    cout << "add_1: " << num1 << ", " << num2 << ", " << num3 << endl;
    int &num4 = add_2(num1, num2);
    cout << "add_1: " << num1 << ", " << num2 << ", " << num4 << endl;    
}

引用的本质

当我们在引用前使用 & 符号,将获得原变量的地址,那么是不是意味着引用只是一个标签,并不占用内存呢?下面的例子说明,并不是这样的。

#include <iostream>
#include <iomanip>
#include <cstdint>
#define PRINT_VAR(x) cout << #x ": " << (x) << endl

using namespace std;

class A {
    public:
        uintptr_t m_n;
        int &m_r;
        A(int n, int &r): m_n(n), m_r(r) {}
};

int main() {
    int a = 100;
    A obj(0, a);

    PRINT_VAR(sizeof(obj));

    PRINT_VAR(&a);
    PRINT_VAR(&obj.m_r);
    PRINT_VAR(obj.m_r);

    cout << hex << showbase;
    PRINT_VAR(*(&obj.m_n + 1));
}

在 64 位机上,上述代码输出(不同次执行内存地址会不一样,但是每次执行第 2、3、5 行的值都是相同的)

sizeof(obj): 16
&a: 0x7fffcc0fed0c
&obj.m_r: 0x7fffcc0fed0c
obj.m_r: 100
*(&obj.m_n + 1): 0x7fffcc0fed0c

从输出可以知道,class A 的对象占用 16 字节,而 uintptr_t 类型的成员变量 m_n 和地址长度一样,即 64 位(8 字节),因此引用 int &m_r 占用了 8 字节的大小。如果直接读取 &obj.m_r 将会得到原变量 a 的地址;为了知道 obj.m_r 所占的 8 字节空间中到底存放了什么,我们可以通过 &obj.m_n + 1 获得 obj.m_r 的真正地址,然后对这个地址解引用以获取其存放的真正内容。运行结果表明,obj.m_r 中存放的是 a 的地址,也就是说引用本质上是被 C++ 封装过的指针。在使用 & 取引用的地址时,编译器会自动对代码进行隐式的转换,以获取到其所指向的原变量的地址。引用虽然基于指针,但是比指针更加易用。

常引用的特殊性

因为引用的本质是指针,因此当引用作为函数参数时,一般来说不能传递常量或者临时变量(临时变量对象会存放在内存上,因此将它绑定到引用上,某些编译器并不会报错)。但是当加上 const 关键字以后,引用就可以绑定临时数据了,这是因为将常引用绑定到临时数据时,编译器会在内存空间中为临时变量创建一个新的无名变量,然后将引用绑定到该无名变量。

同指针变量的使用一样,引用使用时需要保证变量类型的严格一致(如下代码中的 float 类型的引用 r1 尝试绑定 int 类型的变量 n,这是不正确的)。但是如果是加了 const 关键字的常引用,那么就可以绑定到可自动类型转换的变量,这时编译器会在内存中自动创建一个临时变量来存储自动类型转换后的值(r2 使用是正确的,编译器会在内存中自动创建一个 float 变量来存储 (float)n )。

int n = 100;
float &r1 = n; // 错误
const float &r2 = n; // 正确

因此为了避免一些意料之外匪夷所思的 bug,如果需要使用引用来作为函数参数,且该值并不需要在函数调用期间修改,请尽量使用 const 关键字限定为常引用。