上一章已提到过指针,还给出使用指针的提示。本章深入探索这个主题,了解指针的功用。
本章将介绍许多新概念,所以可能需要多次重复某些内容。本章很长,需要花一些时间学习其内容,用一些例子体验指针。指针的基本概念很简单,但是可以应用它们解决复杂的问题。指针是用C语言高效编程的一个基本元素。
本章的主要内容:
● 指针的概念及用法
● 指针和数组的关系
● 如何将指针用于字符串
● 如何声明和使用指针数组
● 如何编写功能更强的计算器程序
7.1 指针初探
指针是C语言里最强大的工具之一,它也是最容易令人困惑的主题,所以一定要在开始时正确理解其概念,在深入探讨指针时,要对其操作有清楚的认识。
第2和第5章讨论内存时,谈到计算机如何为声明的变量分配一块内存。在程序中使用变量名引用这块内存,但是一旦编译执行程序,计算机就使用内存位置的地址来引用它。这是计算机用来引用“盒子(其中存储了变量值)”的值。
请看下面的语句:
int number = 5;
这条语句会分配一块内存来存储一个整数,使用number名称可以访问这个整数。值5存储在这个区域中。计算机用一个地址引用这个区域。存储这个数据的地址取决于所使用的计算机、操作系统和编译器。在源程序中,这个变量名是固定不变的,但地址在不同的系统上是不同的。
可以存储地址的变量称为指针(pointers),存储在指针中的地址通常是另一个变量,如图7-1所示。指针P含有另一个变量number的地址,变量number是一个值为5的整数变量。存储在P中的地址是number第一个字节的地址。

图7-1 指针的工作原理
首先,知道变量P是一个指针是不够的,更重要的是,编译器必须知道它所指的变量类型。没有这个信息,根本不可能知道如何处理它所指的内存的内容。char类型值的指针指向占有一个字节的值,而long类型值的指针通常指向占有4个字节的值。因此,每个指针都和某个变量类型相关联,也只能用于指向该类型的变量。所以如果指针的类型是整数,就只能指向int类型的变量,如果指针的类型是float,就只能指向float类型的变量。一般给定类型的指针写做type*,其中type是任意给定的类型。
类型名void表示没有指定类型,所以void*类型的指针可以包含任意类型的数据项地址。类型void*常常用做参数类型,或以独立于类型的方式处理数据的函数的返回值类型。任意类型的指针都可以传送为void*类型的值,在使用它时,再将其转换为合适的类型。例如,int类型变量的地址可以存储在void*类型的指针变量中。要访问存储在void*指针所指地址中的整数值,必须先把指针转换为int *类型。本章后面介绍的malloc()库函数返回void*类型的指针。
7.1.1 声明指针
以下语句可以声明一个指向int类型变量的指针:
int *pointer;
pointer变量的类型是int *,它可以存储任意int类型变量的地址。这条语句创建了pointer,但没有初始化它。未初始化的指针是非常危险的,所以应总是在声明指针时对它进行初始化。重写刚才的声明,初始化pointer,使它不指向任何对象:
int *pointer = NULL;
NULL是在标准库中定义的一个常量,对于指针它表示0。NULL是一个不指向任何内存位置的值。这表示,使用不指向任何对象的指针,不会意外覆盖内存。NULL在头文件<stddef.h>、<stdlib.h>、<stdio.h>、<string.h>、<time.h>、<wchar.h>和<locale.h>中定义,必须在源文件中至少包含这些头文件中的一个,编译器才能识别NULL。
如果用已声明的变量地址初始化pointer变量,可以使用寻址运算符&,例如:
int number = 10;
int *pointer = &number;
pointer的初值是number变量的地址。注意,number的声明必须在pointer的声明之前。否则,代码就不能编译。编译器需要先分配好空间,才能使用number的地址初始化pointer变量。
指针的声明没有什么特别之处。可以用相同的语句声明一般的变量和指针,例如:
double value , *pVal, fnum;
这条语句声明了两个双精度浮点数变量value和fnum,以及一个指向double的变量pVal。从该语句中可以看出,只有第2个变量pVal是指针,考虑如下语句:
int *p, q;
上述语句声明了一个指针p和一个变量q,两者都是int类型。把p和q都当做指针是一个很常见的错误。
7.1.2 通过指针访问值
使用间接运算符*可以访问指针所指的变量值。这个运算符也称为取消引用运算符(dereferencing operator),因为它用于取消对指针的引用。假设声明以下的变量:
int number = 15;
int *pointer = &number;
int result = 0;
pointer变量含有number变量的地址,所以可以在表达式中使用它计算一个新的汇总值,如下:
result = *pointer +5;
表达式*pointer等于存储在pointer中的地址的值。这是存储在number中的值15,所以result是15+5,等于20。
这完全符合理论。下面的小程序将凸显指针变量的某些特性。
试试看:声明指针
这个例子将声明一个变量和一个指针,然后给出它们的地址和它们所含的值。
/* Program 7.1 A simple program using pointers */
#include <stdio.h>
int main(void)
{
int number = 0; /* A variable of type int initialized to 0 */
int *pointer = NULL; /* A pointer that can point to type int */
number = 10;
printf("\nnumber's address: %p", &number); /* Output the address */
printf("\nnumber's value: %d\n\n", number); /* Output the value */
pointer = &number; /* Store the address of number in pointer */
printf("pointer's address: %p", &pointer); /* Output the address */
printf("\npointer's size: %d bytes", sizeof(pointer));
/* Output the size */
printf("\npointer's value: %p", pointer);
/* Output the value (an address) */
printf("\nvalue pointed to: %d\n", *pointer); /* Value at the address */
return 0;
}
这个程序的输出如下所示。注意,实际地址在不同的计算机上是不同的。
number's address: 0012FEE4
number's value: 10
pointer's address: 0012FEE0
pointer's size: 4 bytes
pointer's value: 0012FEE4
value pointed to: 10
代码的说明
首先,声明一个int变量和一个指针:
int number = 0; /* A variable of type int initialized to 0 */
int *pointer = NULL; /* A pointer that can point to type int */
指针pointer是int类型指针。指针的声明和其他变量一样。声明指针pointer时,在变量名称前添加一个星号(*)。这个星号将pointer定义成一个指针,它的类型是int,表示整数变量的指针。变量pointer的初值是NULL,对于指针NULL表示0—— 它没有指向任何对象。
声明之后,在变量number存储值10,然后用以下语句输出它的地址和值:
number = 10;
printf("\nnumber's address: %p", &number); /* Output the address */
printf("\nnumber's value: %d\n\n", number); /* Output the value */
要输出变量number的地址,应使用输出格式指定符%p,它以十六进制格式输出内存的地址。
下一条语句使用寻址运算符&获取变量number的地址,将该地址存储到pointer中:
pointer = &number; /* Store the address of number in pointer */
注意,在pointer中只能存储地址。
接下来有4个printf()语句,分别输出pointer的地址(pointer所占的内存位置的第一个字节)、pointer所占的字节数、存储在pointer的值(它是number的地址),以及在pointer所含的地址内存储的值(它是存储在number中的值)。
为了解释清楚,下面逐行解释这些代码。第一条输出语句如下:
printf("pointer's address: %p", &pointer);
这条语句输出pointer的地址。指针本身也有一个地址,就像一般的变量一样。使用%p作为转换指定符,以显示一个地址,然后用&(寻址)运算符引用pointer变量的地址。
接着,输出这个指针的字节数:
printf("\npointer's size: %d bytes", sizeof(pointer));
/* Output the size */
可以像其他变量一样,使用sizeof运算符获得指针所占的字节数,在某台机器上,一个指针占用4个字节,所以该机器上的内存地址是32位。
下一条语句输出存储在pointer中的值:
printf("\npointer's value: %p", pointer);
存储在pointer中的值是number的地址。因为这是一个地址,所以用%p显示它,用变量名pointer访问这个地址值。
最后一条输出语句如下所示:
printf("\nvalue pointed to: %d\n", *pointer);
这里使用pointer访问存储在number中的值。*运算符的作用是访问存储在pointer中的地址的数据。使用%d是因为它是一个整数。变量pointer存储number的地址,所以可以使用该地址访问存储在number中的数值。如前所述,*运算符称为间接运算符,有时也称为取消引用运算符。
所显示的地址在不同的机器上是不同的,在同一台机器上,如果程序的运行时间不同,所显示的地址也不相同。后者是因为程序不会每次都加载到相同的内存位置。number和pointer的地址是变量在这台计算机上存放的地方。它们的值存储在该地址中。number变量是一个整数(10),但pointer变量是number的地址。使用*pointer可以访问number的值,即间接地使用number变量的值。
间接运算符*也是乘的符号,编译器不会混淆它们。编译器会根据星号出现的位置确定它是间接运算符还是乘号。
图7-2说明了指针的用法。

图7-2 使用指针
7.1.3 使用指针
可以通过指针pointer访问number的内容,所以可以在算术语句中使用取消引用的指针,例如:
*pointer += 25;
上述语句将变量pointer所指向的地址中的值增加25。星号*表示访问pointer变量所指向的内容。这里它是变量number的内容。
变量pointer能存储任何int变量的地址。这表示可以用下面的语句改变pointer指向的变量:
pointer = &another_number;
重复之前的语句:
*pointer += 25;
该语句操作的是新的变量another_number。这表示指针可以包含同一类型的任意变量的地址,所以使用一个指针变量可以改变其他许多变量的值,只要它们的类型与指针相同。
试试看:使用指针
下面的例子使用指针递增存储在其他变量中的值。
/* Program 7.2 What's the pointer */
#include <stdio.h>
int main(void)
{
long num1 = 0L;
long num2 = 0L;
long *pnum = NULL;
pnum = &num1; /* Get address of num1 */
*pnum = 2; /* Set num1 to 2 */
++num2; /* Increment num2 */
num2 += *pnum; /* Add num1 to num2 */
pnum = &num2; /* Get address of num2 */
++*pnum; /* Increment num2 indirectly */
printf("\nnum1 = %ld num2 = %ld *pnum = %ld *pnum + num2 = %ld\n",
num1, num2, *pnum, *pnum + num2);
return 0;
}
执行这个程序,会得到如下输出:
num1 = 2 num2 = 4 *pnum = 4 *pnum + num2 = 8
代码的说明
printf()后面的注释使这个程序比较容易理解。首先,在main()函数体中有这些声明:
long numl = 0;
long num2 = 0;
long *pnum = NULL;
两个变量numl和num2的初值设置为0。第三个语句声明了一个整数指针pnum,它初始化为NULL。
警告:
声明指针时,一定要初始化它们。使用未初始化的指针存储数据项是很危险的。在使用指针存储一个值时,谁也不知道会覆盖什么内容。
下一条语句是赋值:
pnum = &num1; /* Get address of num1 */
指针pnum设定为指向numl,因为该语句使用寻址运算符获取numl的地址,并将它保存在pnum中。
下两行语句是:
*pnum = 2; /* Set num1 to 2 */
++num2; /* Increment num2 */
第一条语句利用了指针的新功能,为pnum取消引用,间接设定了numl的值2。然后,变量num2以正常方式用递增运算符加1。
之后的语句:
num2 += *pnum; /* Add num1 to num2 */
这条语句把pnum指向的变量内容加到num2上。pnum仍指向num1,所以给num2加上num1的值。
下两条语句是:
pnum = &num2; /* Get address of num2 */
++*pnum; /* Increment num2 indirectly */
首先,指针重新指向num2。然后,通过指针间接地递增变量num2。表达式++*pnum递增了pnum指向的值。但如果要使用后置形式,必须写成(*pnum)++。括号很重要,它指定要递增的是数值,而不是地址。如果省略括号,就会递增pnum所含的地址。这是因为运算符++和一元运算符*(和一元运算符&)的优先级相同,且都是从右到左计算的。编译器会先给pnum应用运算符++,递增地址,然后取消对它的引用,得到它包含的值。这是一个通过指针递增数值的常见错误,所以最好在任何情况下都使用括号。
最后,在结束程序的return语句之前,有一条printf()语句:
printf("\nnum1 = %ld num2 = %ld *pnum = %ld *pnum + num2 = %ld\n",
num1, num2, *pnum, *pnum + num2);
它会显示numl、num2、num2通过pnum加1的结果,最后是以pnum形式出现的num2和num2的值之和。
第一次遇到指针时,很可能会弄不清楚。指针有多层意义,这就是混乱的根源。我们可以使用地址、数值、指针或变量,有时很难搞清楚到底是怎么回事。最好编写短一点的程序,使用指针得到数值,改变值,打印地址等。这是能有信心用好指针的唯一方法。
这里又一次提到运算符优先级的重要性。C语言中所有运算符的优先级可参阅第3章的表3-2,如果不清楚某个运算符的优先级,可以参阅该表。
下面的例子说明了指针如何用于键盘输入。
试试看:scanf()与指针的使用
前面使用scanf()输入数值时,使用了&运算符获取传给函数的地址。有了一个含有地址的指针后,只需使用这个指针的名字作为参数。如下面的例子:
/* Program 7.3 Pointer argument to scanf */
#include <stdio.h>
int main(void)
{
int value = 0;
int *pvalue = NULL;
pvalue = &value; /* Set pointer to refer to value */
printf ("Input an integer: ");
scanf(" %d", pvalue); /* Read into value via the pointer */
printf("\nYou entered %d\n", value); /* Output the value entered */
return 0;
}
这个程序只是输出了输入的信息。输出如下:
Input an integer: 10
You entered 10
代码的说明
scanf()语句中的每个参数都很清晰:
scanf(" %d", pvalue);
这条语句将用户输入的值存储到变量的地址中。就这个例子而言,可以使用&value。但是这里使用指针pvalue将value的地址传递给scanf()。下面的赋值语句将value的地址存储到pvalue中:
pvalue = &value; /* Set pointer to refer to value */
pvalue和&value是相同的,所以用任何一个都可以。
然后,显示value:
printf("\nYou entered %d\n", value); /* Output the value entered */
这是一个没什么意义的例子,但它说明了指针和变量可以一起使用。
测试NULL指针
在上面的例子中,指针声明如下:
int *pvalue = NULL;
这个语句用NULL初始化pvalue。如前所述,NULL在C语言中是一个特殊的常量,它是相当于数字0的指针。NULL的定义包含在<stdio.h>和其他许多头文件中,所以如果使用它,一定要包含其中一个头文件。
给指针赋予0时,就等于将它设为NULL,所以可以编写如下语句:
int *pvalue = 0;
因为NULL等于0,如果要测试指针pvalue是否为NULL,可以编写如下语句:
if(!pvalue)
{
...
}
pvalue是NULL,则!pvalue就是true,所以这段语句只有在pvalue是NULL时才会执行。也可以将这个测试写成如下语句:
if(pvalue == NULL)
{
...
}
7.1.4 指向常量的指针
声明指针时,可以使用const关键字指定,该指针指向的值不能改变。下面是声明const指针的例子:
long value = 9999L;
const long *pvalue = &value; /* Defines a pointer to a constant */
把pvalue指向的值声明为const,所以编译器会检查是否有语句试图修改pvalue指向的值,并将这些语句标记为错误。例如,下面的语句就会让编译器生成一条错误信息:
*pvalue = 8888L; /* Error - attempt to change const location */
pvalue指向的值不能改变,但可以对value进行任意操作。
value = 7777L;
改变了pvalue指向的值,但不能使用pvalue指针做这个改变。当然,指针本身不是常量,所以仍可以改变它指向的值:
long number = 8888L;
pvalue = &number; /* OK - changing the address in pvalue */
这会改变指向number的pvalue中的地址,仍然不能使用指针改变它指向的值。可以改变指针中存储的地址,但不允许使用指针改变它指向的值。
7.1.5 常量指针
当然,也可以使指针中存储的地址不能改变。此时,在指针声明中使用const关键字的方式略有区别。下面的语句可以使指针总是指向相同的对象:
int count = 43;
int *const pcount = &count; /* Defines a constant */
第二条语句声明并初始化了pnumber,指定该指针存储的地址不能改变。编译器会检查代码是否无意中把指针指向其他地方,所以下面的语句会在编译时生成一条错误信息:
int item = 34;
pcount = &item; /* Error - attempt to change a constant pointer */
但使用pcount,仍可以改变pcount指向的值:
*pcount = 345; /* OK - changes the value of count */
这条语句通过指针引用了存储在count中的值,并将其改为345。还可以直接使用count改变这个值。
可以创建一个常量指针,它指向一个常量值:
int item = 25;
const int *const pitem = &item;
pitem是一个指向常量的常量指针,所以所有的信息都是固定不变的。不能改变存储在pitem中的地址,也不能使用pitem改变它指向的内容。
7.1.6 指针的命名
我们已经开始编写相当大的程序了。程序越来越大,就越难记住哪个是一般变量,哪个是指针。因此,最好将p作为指针名的第一个字母。如果严格遵循这个命名方法,肯定很清楚哪个变量是指针。





