转自:C++ 编译器如何处理引用?

简介

我决定写一篇关于C++引用的文章是因为我觉得很多人都对引用这个东西有很多误解。之所以有这个感觉,是因为我曾经面试过很多C++程序员,但很少有人能给我完全正确的解释C++的引用。

那么,C++中的引用到底是什么呢?它通常被解释为它所引用的对象的一个别名(alias)。但我很讨厌别人用别名这个概念来解释C++中的引用。所以在这篇文章中,我首先就要尝试去解释,C++中根本就没有别名这种东西。

背景

在C/C++中,事实上只有两种方法可以让你访问、传递和检索一个变量。它们分别是:

  1. 通过值来访问/传递一个变量(变量名)
  2. 通过地址来访问/传递一个变量(指针)

根本没有第三种访问/传递变量的方法。一个引用变量其实就是一个在内存中实实在在占据空间的指针变量。但特殊之处在于,引用变量是一种会被自动解引用的指针变量(由编译器来做这件事)。很难理解?往后看吧

一个使用C++引用的小代码

#include <iostream.h>
int main()
{
    int i = 10;   // A simple integer variable
    int &j = i;   // A Reference to the variable i
    
    j++;   // Incrementing j will increment both i and j.

    // check by printing values of i and j
    cout<<  i  <<  j  <<endl; // should print 11 11

    // Now try to print the address of both variables i and j
    cout<<  &i  <<  &j  <<endl; 
    // surprisingly both print the same address and make us feel that they are
    // alias to the same memory location. 
    // In example below we will see what is the reality
    return 0;
}

引用其实就是C++中的指针常量。这个语句 int &i = j; 会被编译器转换成 int* const i = &j; 这也就能解释为什么引用必须初始化,因为指针常量必须被初始化。同样引用不能重新引用其他变量也是因为指针常量的性质。接下来我们尝试从编译器的视角来重写上面的代码。

一个使用C++引用的小代码(编译器视角)

#include <iostream.h>
int main()
{
    int i = 10;           // A simple integer variable
    int *const j = &i;       // A Reference to the variable i
    
    (*j)++;           // Incrementing j. Since reference variables are 
            // automatically dereferenced by compiler

    // check by printing values of i and j
    cout<<  i  <<  *j  <<endl; // should print 11 11
    // A * is appended before j because it used to be reference variable
    // and it should get automatically dereferenced.
    return 0;
}

可以看到,编译器将引用类型自动解释为指针常量类型。而之后每一次对引用变量的访问,编译器都会对对应的指针变量解一次引用再访问。

一个使用C++级联引用的小代码

让我们再看一个稍微复杂点的例子,这里我们会看到级联的引用变量是如何工作的。

#include <iostream.h>
int main()
{
    int i = 10; // A Simple Integer variable
    int &j = i; // A Reference to the variable
    // Now we can also create a reference to reference variable. 
    int &k = j; // A reference to a reference variable
    // Similarly we can also create another reference to the reference variable k
    int &l = k; // A reference to a reference to a reference variable.

    // Now if we increment any one of them the effect will be visible on all the
    // variables.
    // First print original values
    // The print should be 10,10,10,10
    cout<<  i  <<  ","  <<  j  <<  ","  <<  k  <<  ","  <<  l  <<endl;
    // increment variable j
    j++; 
    // The print should be 11,11,11,11
    cout<<  i  <<  ","  <<  j  <<  ","  <<  k  <<  ","  <<  l  <<endl;
    // increment variable k
    k++;
    // The print should be 12,12,12,12
    cout<<  i  <<  ","  <<  j  <<  ","  <<  k  <<  ","  <<  l  <<endl;
    // increment variable l
    l++;
    // The print should be 13,13,13,13
    cout<<  i  <<  ","  <<  j  <<  ","  <<  k  <<  ","  <<  l  <<endl;
    return 0;
}

一个使用C++级联引用的小代码(编译器视角)

#include <iostream.h>
int main()
{
    int i = 10;         // 一个普通的整型变量
    int *const j = &i;     // 一个引用变量
    // 变量j会存储着i的地址

    // 现在来建立一个引用变量的引用 
    int *const k = &*j;     // 一个引用变量的引用
    // 变量k同样存储的是i的地址,这是因为j是一个引用变量,所以编译器会对其自动解引用
    // 所以j前面的&和*会互相抵消,所以最终k存储的是j的值,即i的地址

    // 类似的,我们还可以为变量k建立一个引用
    int *const l = &*k;     // 一个引用变量的引用变量的引用
    // 变量l同样存储的是i的地址,因为&和*抵消,即等于k的值,即i的地址

    // 所以我们最终可以看到,所有的级联引用其实都存储的是同一个地址

    // 现在我们改变其中一个引用的值,在其他引用那里都可以看到结果的变化
    // 首先打印原始值。引用变量前面都有*是因为编译器对其作了自动解引用

    // 输出应该是 10, 10, 10, 10
    cout<<  i  <<  ","  <<  *j  <<  ","  <<  *k  <<  ","  <<  *l  <<endl;
    // 增加变量j
    (*j)++; 
    // 输出应该是 11,11,11,11
    cout<<  i  <<  ","  <<  *j  <<  ","  <<  *k  <<  ","  <<  *l  <<endl;
    // 增加变量k
    (*k)++;
    // 输出应该是 12,12,12,12
    cout<<  i  <<  ","  <<  *j  <<  ","  <<  *k  <<  ","  <<  *l  <<endl;
    // 增加变量l
    (*l)++;
    // 输出应该是 13,13,13,13
    cout  <<  i  <<  ","  <<  *j  <<  ","  <<  *k  <<  ","  <<  *l  <<endl;
    return 0;
}

引用在内存中也有自己的空间

我们可以通过求一个只有引用变量的类的大小来证明这一点。下面的代码证明了引用并不是什么别名而是拥有自己的空间。

#include <iostream.h>

class Test
{
    int &i;   // int *const i;
    int &j;   // int *const j;
    int &k;   // int *const k; 
};

int main()
{    
    // This will print 12 i.e. size of 3 pointers
    cout<<  "size of class Test = "  <<   sizeof(class Test)  <<endl;
    return 0;
}

总结

我希望这篇文章已经把引用解释的足够清楚了。而我同样要提到的是,C++标准并未规定编译器到底要如何实现引用的各种特性,这完全由编译器自行决定,但绝大多数编译器都是通过指针常量来实现引用的。

最后修改:2019 年 11 月 10 日
如果觉得我的文章对你有用,请随意赞赏