第五章 指针和数组
指针是包含变量地址的变量。在 C 语言中,指针被大量使用,部分原因是有时只能用指针来表达某种计算,而部分原因是相比其他方式,指针通常能带来更紧凑和高效的代码。指针和数组是紧密关联的;本章也讲探讨它们的关系,并演示如何利用这个关系。
指针曾经和 goto 语句一起,被归结为用于创建“让人不可能理解”的程序的绝妙方式。如果粗心大意地使用指针,这个说法当然是对的,而且很容易创建指向不可预料位置的指针。然而,通过训练和规范约束,指针可以用来得到清晰性和简洁性。这正是我们在本书中尽量展示的方面。
ANSI C 的主要改动是把如何操纵指针的规则给明确了,实际上就是把优秀程序员已有的实践,和优秀编译器已加强的地方,做成了强制要求。另外,void * 类型(指向 void 的指针)取代了 char * ,作为通用指针的正确类型。
5.1 指针和地址
我们先从内存组织的简化图开始。一台典型的机器有一组连续编号或者叫编址的内存单元,这些内存单元可以被各自独立操作,或者是按连续分组进行操作。通常的情况是任一个字节可以是一个 char, 一对单字节的内存单元可以被当作是 short 整型,而四个连续的字节组成一个 long。指针是一组(通常是2个或4个)可以保存地址的单元。这样,如果 c 是一个char 而 p 是指向它的指针,则我们可以用下图来表示这种情况:
一元操作符 & 给出一个对象的地址,因此如下语句
p = &c;
把 c 的地址赋给 p,而 p 被称作“指向” c。操作符 & 只能用于内存中的对象:变量和数组元素。它不能用于表达式、常量 或者寄存器(register)变量。
一元操作符 * 是 间接 或者叫 解引用 操作符;当用于指针时,它访问指针指向的对象。假定 x 和 y 是整数而 ip 是指向 int 的指针。下面这段人为构造(但没有实际用途)的代码,演示了如何声明指针,以及如何使用 & 和 *:
int x = 1, y = 2, z[10];
int *ip; /* ip是指向int的指针 */ip = &x; /* ip现在指向x */
y = *p; /* y现在是1 */
*p = 0; /* x现在是0 */
ip = &z[0]; /* ip现在指向z[0] */
x,y,z的声明不需要多说了。指针 ip 的声明为
int *ip;
这种形式旨在助记;它表示表达式 *ip 是一个 int。变量声明的语法,模仿了变量可以在其中出现的表达式的语法。这个推理也适用于函数声明。例如
double *p, atof(char *);
表示,在一个表达式中,*p 和 atof(s) 都有着 double 类型的值,而 atof 的参数是 char 的指针。
你还应当注意到有个隐含的约束:指针被限制指向一种特定的对象类型。(有一个例外,“指向 void 的指针”被用来保存任意类型的对象,但不能对它自身解引用。我们会在5.11节说明)
如果 ip 指向整数 x,则 *ip 可以出现在任何 x 可以出现的上下文中,因此
*ip = *ip + 10;
将 *ip 增加10。
一元操作符 & 和 * 比算术操作符的结合更为紧密,因此赋值表达式
y = *ip + 1
首先取出 ip 所指向的对象,将其加1,并把结果赋给 y。而
*ip += 1
会把 ip 指向的对象递增,正如
++*ip
和
(*ip)++
最后一个例子中的括号是必须的;如果没有括号,则表达式会对 ip 递增,而不会对它指向的内容递增,因为一元操作符 * 和 ++ 是从右至左结合。
最后,由于指针是变量,那么不解引用也可以使用它们。例如,如果 iq 是另一个 int 的指针,
iq = ip
将 ip 的内容拷贝给 iq,这样就使 iq 也指向 ip 所指的对象了。
5.2 指针和函数参数
由于C将参数传给函数时是值传递,因此没有直接的方法可以使被调函数修改调用者函数中的变量。例如,一个排序例程可能会用一个 swap 函数来交换两个无序的元素。如果写
swap(a, b);
其中 swap 函数的定义为
void swap(int x, int y) /* 错误 */
{int temp;temp = x;x = y;y = temp;
}
但这样是不行的。因为所有调用都是值传递,swap 不能影响调用它的例程中的参数 a 和 b。上面的函数只是交换了 a 和 b 的拷贝。
要得到想要的效果,方法是让调用程序传递要修改的值的指针:
swap(&a, &b);
由于操作符 & 得到了变量的地址,&a 是 指向 a 的指针。在 swap 之中,参数被声明为指针,而通过它们来间接访问操作数。
void swap(int *px, int *py) /* 交换 *px和 *py */
{int temp;temp = *px;*px = *py;*py = temp;
}
用图来表示就是:
指针参数使一个函数能够访问并改变调用它的函数中的对象。举个例子,考虑实现一个函数 getint ,该函数执行自由格式的输入转换:将一个字符流拆分成多个整数值,每次调用得到一个整数。getint 必须返回它所找到的值,也必须在没有更多输入时指示文件结束。这两个值必须通过不同的路径返回,因为不管EOF使用什么值,都有可能是一个输入整数的值。
一种解决方案是让 getint 把文件结束状态作为函数返回值,而同时使用一个指针参数来保存转换后的整数,传递回给它的调用者函数。这也是 scanf 使用的模式,详见 7.4节。
下面的循环通过调用 getint 来填充整型数组:
int n, array[SIZE], getint(int *);for (n = 0; n < SIZE && getint(&array[n]) != EOF; n++);
每次调用都将 array[n] 设置为从输入中找到的下一个整数,并对 n 递增。注意,必须将 array[n] 的地址传给 getint 。否则,getint 没有办法将转换后的整数传给调用者。
我们这个版本的 getint 在文件结尾时返回 EOF,在下一个输入不是数字时返回0,而在输入包含合法的数字时返回一个正值:
#include <ctype.h>int getch(void);
void ungetch(int);/* getint: 将下一个整数从输入存入 *pn */
int getint(int *pn)
{int c, sign;while (isspace(c = getch())) /* 跳过空白字符 */;if (!isdigit(c) && c != EOF && c != '+' && c != '-') {ungetch(c); /* 非数字 */return 0;}sign = (c == '-') ? -1 : 1;if (c == '+' || c =='-')c = getch();for (*pn = 0; isdigit(c); c = getch())*pn = 10 * *pn + (c - '0');*pn *= sign;if (c != EOF)ungetch(c);return c;
}
在 getint 内, *pn 自始至终 都被当作普通的 int 变量来使用。我们还使用了 getch 和 ungetch(见4.3节),因此多读但又不得不读的一个字符,就能被推回给输入。
练习5-1、在我们的getint版本中,后面没有跟数字的单独的 + 和 - 号,也被当作是合法的数字,其值为 0。修改这个问题,将这种字符推回给输入。
练习5-2、模仿 getint,写一个浮点数版本的 getfloat 函数。 getfloat 用什么类型作为其返回值?