……显然,我们能通过名字使用对象。然而在 C++ 中,大多数对象都“有身份”;也就是说对象位于内存的某个地址中,如果我们知道对象的地址和类型,就能访问它…… 翻译自 Bjarne Stroustrup 《The C++ Programming Language》(Fourth Edition),Chapter 7.
在学习 C 与 C++ 语言时,指针与引用类型(C++)总是同学们最头疼的知识点之二。事实上,很多学习过包含指针和/或引用概念程序设计语言的同学也仍然对于为什么会存在这些语言元素的原因缺乏思考。下述代码以 C++ 为例。本文首发于 DGideas 的博客。
感谢 @szouc 对于数组声明与数组类型隐式转换方面的指正。
为什么要有指针?
现代通用电子计算机在程序运行时将所需数据存储于内部存储器(内存)中。通过机器可理解的语言,我们能令电子计算机存取内存中某一指定位置数据,就比如编写一个统计字符串字面量中含有多少个英文小写字母 a 的程序一样,这个程序将用于计数的数据存储于内存空间中的某个位置中。像这样的操作使用是如此频繁,以至于高级程序设计语言专门为程序员提供一种被称为“变量”的语言概念。
变量(Variable)是一块具名(Named)地址空间。在高级程序设计语言中,我们不再令计算机程序访问某一个特定位置的数据,而只需指出我们需要一块在内存中名为 a
的,存储默认长度整型数据的空间。就像在大多数编程语言中我们的如下定义一样:
int a;
这样的名称 – 地址一对一的方式使程序员不再关注变量存储的具体位置,这也为兼容于不同内存寻址方式提供了更多方便。然而,并不是所有类型的变量都与计算机为存储该变量所分配的地址空间之间存在一对一的关系,就比如:
int b[10];
上述 C++ 代码声明了一组连续的,能够存储 10 个默认长度整型变量的空间块,其中标识符(Identifier) b
是数组名称。C++ 标准(conv.array、expr.unary.op 与 expr.sub)指导我们,在某些语境下,包含数组名称 b
的表达式中,标识符 b
可以隐式转换为所分配的空间块中第一块的地址。习惯上,我们使用类似 *b
的包含间接寻址运算符的表达式计算第一块地址所对应的内容。
我们如何读取其余几块的内容?我们并没有一种方式能够直接访问这些空间的内容,但因为我们知道第一块的地址,(编译器)也知道每块默认长度整型的空间有多长,所以编程语言能够提供类似 *(b+2)
或者 b[2]
的方式,允许我们使用首块地址+偏移量(Offset)的方式间接地访问其他块的内容。
这种通过地址+偏移量间接地访问其他块内容的方式,被称为间接寻址(Indirection)与数组下标(Subscript)运算。像这种保存空间中某一个块的地址的变量,被称为指针。这样,我们很容易接受指针这个概念。
要注意,由于不同数据类型每块所占用的空间各有不同,所以指针是具有类型的。类型系统有助于指导程序以何种方式去解释内存中某块位置的数据,也能够正确处理类似 *(b+2)
的偏移量操作。
其他编程语言,比如 Python ,并没有提供指针数据类型。但也通过下标操作(类似 b[2]
)提供访问非具名数据的能力。
那……引用呢?
想象我们有以下函数,完成一项具有超凡成就的工作:
void nicejob(int a)
{
a = a * 2;
}
这段代码意义非凡。它尝试将传入的变量加倍后,将自身改为加倍后的新值。可惜,这段代码不能正常工作。当你尝试执行下列代码时,你就会发现它并不能如期运行:
#include <iostream>
int main()
{
int x = 2;
std::cout << x << std::endl;
nicejob(x);
std::cout << x << std::endl;
return 0;
}
输出:
2
2
发生了什么?在 nicejob
中对于变量 a
的更改并不会影响 x
的值。包括 C/C++ 在内的多种语言中,当按值传递参数到函数时,函数中获得的变量 a
仅仅是 x
的拷贝——你一直在对拷贝进行操作,当然不会影响到原来的 x
值。
显然我们有两种方法解决这个问题。我们可以将变量 x
的地址传递给函数 nicejob
,在函数中修改内存对应位置的 x
值,简单且粗暴。我们还可以指示我们的程序将 x
值本身(而非拷贝!)传入函数 nicejob
中,这样在函数中操作 a
就相当于直接操作 x
一样—— a
是 x
的别名。
void nicejob(int& a)
{
a = a * 2;
}
我们只需在函数形式参数列表中将变量 a
的传递类型由 int
改为 int&
—— 引用类型即可。这样我们的 nicejob
函数就能如期工作了。
当我们将一个很大的变量传递给函数时,为了避免在变量拷贝过程中的大量开销(比如,我们传递一个大小为 1GB 的图片给图片处理函数时),我们也使用引用类型。为防止函数对传入变量的误修改,我们可以将函数形参列表(Parameter-list)中的变量类型设为常量引用,就比如 const Image&
。
指针提供了一种(直接或间接)访问非具名数据的能力; 引用是一种程序变量在构造过程中初始化的方式; DGideas