指针
指针
指针是一种极其强大的编程工具。它们可以让某些事情变得更容易,帮助提高程序的效率,甚至允许你处理几乎无限量的数据。例如,使用指针是让函数修改传递给它的变量的方法之一。还可以使用指针动态分配内存,这意味着你可以编写能够实时处理几乎无限量数据的程序——你不需要在编写程序时知道需要多少内存。哇,这有点酷。实际上,它非常酷,正如我们将在接下来的教程中看到的那样。目前,我们只需了解指针是什么以及如何使用它们。
Why
指针的命名非常贴切:它们“指向”内存中的位置。想象一下当地银行里一排大小不一的保险箱。每个保险箱都会有一个编号,方便你快速查找。这些编号就像变量的内存地址。在保险箱的世界里,指针可以简单地理解为任何存储其他保险箱编号的东西。也许你有一个富有的叔叔,他在自己的保险箱里存放了贵重物品,但决定将真实位置放在另一个较小的保险箱里,这个小保险箱只存放一张写着大保险箱编号的卡片,而大保险箱里存放着真正的珠宝。存放着卡片的保险箱就存储着另一个保险箱的位置;它相当于一个指针。在计算机中,指针只是存储内存地址的变量,通常是其他变量的地址。
有趣的是,一旦你能谈论变量的地址,你就能去那个地址并获取其中存储的数据。如果你恰好有一大块数据想要传递给函数,传递它的位置比复制数据的每个元素要容易得多!此外,如果你需要更多内存来运行程序,你可以向系统申请更多内存——那么你如何“收回”这些内存呢?系统会告诉你它在内存中的位置;也就是说,你会得到一个内存地址。你需要指针来存储内存地址。
关于术语的说明:指针这个词可以指内存地址本身,也可以指存储内存地址的变量。通常,这种区别并不那么重要:如果你将指针变量传递给函数,你传递的是指针中存储的值——即内存地址。当我想要谈论内存地址时,我会称其为内存地址;当我想要一个存储内存地址的变量时,我会称其为指针。当一个变量存储另一个变量的地址时,我会说它“指向”那个变量。
C指针语法
指针需要一些新的语法,因为当你有一个指针时,你需要能够请求它存储的内存位置以及该内存位置存储的值。此外,由于指针有些特殊,你需要在声明指针变量时告诉编译器该变量是一个指针,并告诉编译器它指向什么类型的内存。
指针的声明如下所示:
变量类型 *名称;例如,你可以使用以下语法声明一个存储整数地址的指针:
int *points_to_integer;注意这里的*使用。这是声明指针的关键;如果你直接在变量名前加上它,它将声明该变量为指针。小技巧:如果你在同一行声明多个指针,你必须给每一个都加上一个*:
/* 一个指针,一个普通的整型 */
int *pointer1, nonpointer1;
/* 两个指针 */
int *pointer1, *pointer2;正如我提到的,使用指针访问信息有两种方式:可以让它将实际地址传递给另一个变量。为此,只需使用指针的名称而不加*。然而,要访问实际内存位置及其存储的值,则使用*。这种做法的技术名称是解引用指针;本质上,你是在获取某个内存地址的引用并跟随它,以获取实际值。记住何时应该添加*可能会很棘手。记住指针的自然用途是存储内存地址;因此,当你使用指针时:
/* 需要内存地址(指针)的函数调用 */
callpointer(pointer);它将评估为地址。你必须添加额外的符号*,才能获取存储在地址中的值。你会频繁进行这样的操作。尽管如此,指针本身应该存储一个地址,所以当你使用裸指针时,你会得到该地址。
指向某个东西:获取地址
为了让指针真正指向另一个变量,需要获取该变量的内存地址。要获取变量的内存地址(它在内存中的位置),在变量名前放置&符号。这会使它返回其地址。这被称为取地址运算符,因为它返回内存地址。
例如:
#include <stdio.h>
int main()
{
int x; /* A normal integer*/
int *p; /* A pointer to an integer ("*p" is an integer, so p
must be a pointer to an integer) */
p = &x; /* Read it, "assign the address of x to p" */
printf("Please enter a number: ");
scanf( "%d", &x ); /* Put a value in x, we could also use p here */
printf( "%d\n", *p ); /* Note the use of the * to get the value */
getchar();
}printf 输出存储在 x 中的值。为什么会出现这种情况呢?让我们看看代码。这个整数叫做 x。然后定义了一个指向整数的指针,命名为 p。接着它使用取地址运算符(&)来获取变量的地址,并将 x 的内存地址存储在指针 p 中。使用取地址运算符有点像查看保险箱的标签以获取其编号,而不是打开箱子查看其存储的内容。用户输入一个数字,并将其存储在变量 x 中;记住,这就是 p 所指向的地址。事实上,由于我们使用取地址运算符将值传递给 scanf,因此很明显 scanf 是将值放入 p 所指向的地址中。(实际上,scanf 之所以能工作,是因为指针!)
下一行代码将 *p 传递给 printf。 *p 对 p 执行“解引用”操作;它查看 p 中存储的地址,然后去该地址并返回该地址的值。这类似于打开一个保险箱,发现里面是另一个(据推测是钥匙)保险箱的编号,然后你打开那个保险箱。
请注意,在上面的例子中,指针在使用前被初始化为指向特定的内存地址。如果情况不是这样,它可能会指向任何地方。这可能导致程序出现非常糟糕的后果。例如,操作系统可能会阻止你访问它知道你的程序不拥有的内存:这将导致你的程序崩溃。如果它让你使用内存,你可能会干扰任何正在运行程序的内存——例如,如果你在 Word 中打开了一个文档,你可以更改文本!幸运的是,Windows 和其他现代操作系统会阻止你访问该内存,并导致你的程序崩溃。为了避免你的程序崩溃,你应该在使用它们之前始终初始化指针。
也可以使用空闲内存来初始化指针。这允许动态分配内存。这对于设置链表或数据树等结构很有用,在这些结构中,你不知道编译时需要多少内存,因此必须在程序执行期间获取内存。我们稍后会查看这些结构,但眼下,我们将简单地考察如何从操作系统请求内存以及如何将内存返回给操作系统。
位于 stdlib.h 头文件中的函数 malloc,用于使用来自自由存储区(所有程序可用的内存区域)的内存来初始化指针。malloc 的工作方式与其他函数调用完全相同。malloc 的参数是请求的内存量(以字节为单位),malloc 获取该大小的内存块,然后返回指向已分配内存块的指针。
由于不同变量类型具有不同的内存需求,我们需要获取 malloc 应返回的内存大小。因此,我们需要知道如何获取不同变量类型的大小。这可以通过使用关键字 sizeof 来实现,它接受一个表达式并返回其大小。例如,sizeof(int) 将返回存储一个整数所需的字节数。
#include <stdlib.h>
int *ptr = malloc( sizeof(int) );这段代码将 ptr 设置为指向一个 int 大小的内存地址。被指向的内存将无法被其他程序使用。这意味着谨慎的程序员应该在用完后释放这块内存,以免在程序运行期间内存被操作系统占用(这通常被称为内存泄漏,因为程序没有跟踪其所有内存)。
请注意,通过直接使用指针获取所指向变量的内存大小来编写 malloc 语句稍微更简洁:
int *ptr = malloc( sizeof(*ptr) );这里发生了什么?sizeof(*ptr)将评估我们通过解引用 ptr 得到的大小;由于 ptr 是一个指向 int 的指针,*ptr 将给我们一个 int,所以 sizeof(*ptr)将返回一个整数的大小。那么为什么要这样做呢?嗯,如果我们后来重写 ptr 的声明如下,那么我们只需要重写它的第一部分:
float *ptr = malloc( sizeof(*ptr) );我们不必回去更正 malloc 调用以使用 sizeof(float)。由于 ptr 将指向一个 float,*ptr 将是一个 float,所以 sizeof(*ptr)仍然会给出正确的大小!
当你最终在声明变量之后很远的点分配内存时,这变得更加有用:
float *ptr;
/* 数百行代码 */
ptr = malloc( sizeof(*ptr) );free 函数将内存返回给操作系统。
free( ptr );释放指针后,最好将其重置为指向 0。当 0 被赋值给指针时,该指针变成一个空指针,换句话说,它不指向任何地方。这样做的好处是,当你用指针做一些愚蠢的操作时(这种情况经常发生,即使是经验丰富的程序员也会犯),你可以立即发现问题,而不是等到造成严重损害之后才发现。
空指针的概念经常被用作表示问题的方法——例如,当 malloc 无法正确分配内存时,它会返回 0。你需要确保正确处理这种情况——有时候你的操作系统可能会真的会溢出内存并给你这个值!
指针一开始可能会让人觉得非常困惑,但我认为任何人都可以逐渐欣赏并理解它们。如果你感觉没有完全掌握关于指针的一切,那就深呼吸几次,重新阅读课程内容。虽然你可能不会完全理解何时以及为何需要使用指针的每一个细节,但你应该对它们的一些基本用途有所了解。
