简介
我决定写一篇关于C++引用的文章是因为我觉得很多人都对引用这个东西有很多误解。之所以有这个感觉,是因为我曾经面试过很多C++程序员,但很少有人能给我完全正确的解释C++的引用。
那么,C++中的引用到底是什么呢?它通常被解释为它所引用的对象的一个别名(alias)。但我很讨厌别人用别名这个概念来解释C++中的引用。所以在这篇文章中,我首先就要尝试去解释,C++中根本就没有别名这种东西。
背景
在C/C++中,事实上只有两种方法可以让你访问、传递和检索一个变量。它们分别是:
- 通过值来访问/传递一个变量(变量名)
- 通过地址来访问/传递一个变量(指针)
根本没有第三种访问/传递变量的方法。一个引用变量其实就是一个在内存中实实在在占据空间的指针变量。但特殊之处在于,引用变量是一种会被自动解引用的指针变量(由编译器来做这件事)。很难理解?往后看吧
一个使用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++标准并未规定编译器到底要如何实现引用的各种特性,这完全由编译器自行决定,但绝大多数编译器都是通过指针常量来实现引用的。