上一页 下一页

C语言编程基础

指针:理解内存地址

变量 f 在内存中占用四个字节的RAM。该位置有一个特定的地址,在本例中是 248,440。
© 2011 十万个为什么.com

如果你了解内存地址在计算机硬件中的工作原理,之前的讨论会更清晰一些。如果你还没读过,现在是阅读《位和字节如何工作》的好时机,以便充分理解位、字节和字。

所有计算机都拥有内存,也称为RAM随机存取存储器)。例如,你的计算机现在可能安装了 16、32 或 64 兆字节的RAM。RAM 存储着计算机当前运行的程序以及它们正在操作的数据(它们的变量和数据结构)。内存可以简单地看作一个字节数组。在这个数组中,每个内存位置都有自己的地址——第一个字节的地址是 0,然后是 1、2、3,依此类推。内存地址的作用就像普通数组的索引。计算机可以随时访问内存中的任何地址(因此得名“随机存取存储器”)。它还可以根据需要将字节组合起来,形成更大的变量、数组和结构。例如,一个浮点变量在内存中占用 4 个连续字节。你可以在程序中进行以下全局声明:

广告

float f;

这个语句表示:“声明一个名为 f 的位置,它可以存储一个浮点值。”当程序运行时,计算机在内存中为变量 f 预留空间。该位置在内存空间中有一个固定的地址,如下所示:

当你想到变量 f 时,计算机想到的是内存中的一个特定地址(例如,248,440)。因此,当你创建这样一个语句时:

f = 3.14;

编译器可能会将其翻译成:“将值 3.14 加载到内存位置 248,440。”计算机总是以地址和这些地址处的值来思考内存。

顺便说一下,计算机处理内存的方式有几个有趣的副作用。例如,假设你在程序中包含以下代码:

int i, s[4], t[4], u=0;

for (i=0; i<=4; i++)
{
	s[i] = i;
	t[i] =i;
}
printf("s:t\n");
for (i=0; i<=4; i++)
	printf("%d:%d\n", s[i], t[i]);
printf("u = %d\n", u);

你从程序中看到的输出可能如下所示:

s:t
1:5
2:2
3:3
4:4
5:5
u = 5

为什么 t[0]u 不正确?如果你仔细查看代码,会发现 for 循环写入了每个数组末尾之后的一个元素。在内存中,数组是彼此相邻放置的,如下所示:

数组彼此相邻放置

因此,当你尝试写入不存在的 s[4] 时,系统会转而写入 t[0],因为 t[0] 是 s[4] 应该在的位置。当你写入 t[4] 时,你实际上是在写入 u。就计算机而言,s[4] 只是一个地址,它可以写入其中。然而,如你所见,尽管计算机执行了程序,但它并不正确或有效。程序在运行过程中损坏了数组 t。如果你执行以下语句,会产生更严重的后果:

s[1000000] = 5;

位置 s[1000000] 很可能在你的程序内存空间之外。换句话说,你正在写入你的程序不拥有的内存。在具有受保护内存空间(UNIX、Windows 98/NT)的系统上,此类语句将导致系统终止程序的执行。然而,在其他系统(Windows 3.1、Mac)上,系统并不知道你在做什么。你最终会损坏另一个应用程序中的代码或变量。这种违规行为的影响范围从完全没有到整个系统崩溃。在内存中,i、s、t 和 u 都紧邻地放置在特定地址。因此,如果你写入超出变量边界的地方,计算机将按你说的做,但最终会损坏另一个内存位置。

由于 C 和 C++ 在你访问数组元素时不会执行任何类型的范围检查,因此作为程序员,你必须特别注意数组范围,并保持在数组的适当边界内。无意中读写超出数组边界总会导致程序行为错误。

再举一个例子,尝试以下代码:

#include <stdio.h>

int main()
{
    int i,j;
    int *p;   /* a pointer to an integer */
    printf("%d %d\n", p, &i);
    p = &i;
    printf("%d %d\n", p, &i);
    return 0;
}

这段代码告诉编译器打印出存储在 in p 中的地址,以及 i 的地址。变量 p 最初会带有一些随机值或为 0。 i 的地址通常是一个很大的值。例如,当我运行这段代码时,我收到了以下输出:

0   2147478276
2147478276   2147478276

这意味着 i 的地址是 2147478276。一旦语句 p = &i; 被执行, p 就包含了 i 的地址。你也可以尝试这个:

#include <stdio.h>

void main()
{
    int *p;   /* a pointer to an integer */

    printf("%d\n",*p);
}

这段代码告诉编译器打印 p 指向的值。然而, p 尚未初始化;它包含地址 0 或某个随机地址。在大多数情况下,会产生段错误(或其他运行时错误),这意味着你使用了指向无效内存区域的指针。几乎总是,未初始化的指针或错误的指针地址是段错误的原因。

说了这么多,我们现在可以从全新的角度来看待指针了。例如,看这个程序:

#include <stdio.h>

int main()
{
    int i;
    int *p;   /* a pointer to an integer */
    p = &i;
    *p=5;
    printf("%d %d\n", i, *p);
    return 0;
}

事情是这样的:

变量 i 占用 4 字节内存。指针 p 也占用 4 字节(在当今大多数使用的机器上,指针占用 4 字节内存。当今大多数 CPU 上的内存地址是 32 位的,尽管越来越趋向于 64 位寻址)。i 的位置有一个特定地址,在本例中是 248,440。一旦你说 p = &i;,指针 p 就持有该地址。因此,变量 *pi 是等价的。

当你在程序中说出这样的话时:

printf("%d", p);

输出的是变量 i 的实际地址。