文章目录
第11章 指针
——我记不清第十一条戒律是“你应该计算”还是“你不应该计算”了。
指针是C
语言最重要——也是最常被误解——的特性之一。由于指针的重要性,本书将用3
章的篇幅对其进行讨论。本章侧重于基础知识,而第12章和第17章则介绍指针的高级应用
。
本章将从内存地址
及其与指针变量
的关系入手(11.1节),然后11.2节介绍取地址运算符
和间接寻址运算符
,11.3节探讨指针赋值
的内容,11.4节说明给函数传递指针
的方法,而11.5节则讨论从函数返回指针
。
11.1 指针变量
理解指针的第一步是在机器级上观察指针表示的内容。大多数现代计算机将内存分割为字节(byte)
,每个字节可以存储8
位的信息。
0 1 0 1 0 0 1 1
每个字节都有唯一的地址(address)
,用来和内存中的其他字节相区别。如果内存中有n
个字节,那么可以把地址看作0~n-1
的数。
地址 内容
0 01010011
1 01110101
2 01110011
....
n-1 01000011
可执行程序由代码
(原始C
程序中与语句对应的机器指令)和数据
(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节内存,把第一个字节的地址称为变量的地址。下图中,变量i
占有地址为2000
和2001
的两个字节,所以变量i
的地址是2000
:
i--------
地址 .... 2000 2001 ....
内容 .... xxxx xxxx ....
11.1.1 指针变量的声明
对指针变量的声明与对普通变量的声明基本一样,唯一的不同就是必须在指针变量名字前放置星号
:
int *p;
int i, j, a[10], b[20], *p, *q;
在这个例子中,i
和j
都是普通整型变量,a
和b
是整型数组,而p
和q
是指向整型对象的指针。
C
语言要求每个指针变量只能指向一种特定类型(引用类型)的对象:
int *p; /* points only to integers */
double *q; /* points only to doubles */
char *r; /* points only to characters */
至于引用类型是什么类型则没有限制。事实上,指针变量甚至可以指向另一个指针,即指向指针的指针(17.6节)。
11.2 取地址运算符和间接寻址运算符
11.2.1 取地址运算符
声明指针变量是为指针留出空间,但是并没有把它指向对象:
int *p; /* points nowhere in particular */
在使用前初始化p
是至关重要的。一种初始化指针变量的方法是使用&运算符
把某个变量的地址赋给它,或者更常采用左值
(4.2节):
int i, *p;
...
p = &i;
在声明指针变量的同时对它进行初始化是可行的:
int i;
int *p = &i;
//甚至可以把i的声明和p的声明合并,但是需要首先声明i:
int i, *p = &i;
11.2.2 间接寻址运算符
一旦指针变量指向了对象,就可以使用*运算符
访问存储在对象中的内容。例如,如果p
指向i
,那么可以显示出i
的值,如下所示:
printf("%d\n", *p);
//printf函数将显示i的值,而不是i的地址。
习惯于数学思维的读者可能希望把*
想象成&
的逆运算。实际上,对变量使用&
运算符产生指向变量的指针,而对指针使用*
运算符则可以返回到原始变量:
j = *&i; //等同于 j = i;
只要p
指向i
,*p
就是i
的别名。*p
不仅拥有和i
相同的值,而且对*p
的改变也会改变i
的值。(*p
是左值,所以对它赋值是合法的。)
p = &i;
i = 1;
printf("%d\n", i); /* prints 1 */
printf("%d\n", *p); /* prints 1 */
*p = 2;
printf("%d\n", i); /* prints 2 */
printf("%d\n", *p); /* prints 2 */
int *p;
printf("%d", *p); /*** WRONG ***/
/*
给*p赋值尤其危险。如果p恰好具有有效的内存地址,下面的赋值会试图修改存储在该地址的数据:
*/
int *p;
*p = 1; /*** WRONG ***/
11.3 指针赋值
C
语言允许使用赋值运算符进行指针的复制,前提是两个指针具有相同的类型。假设i
、j
、p
和q
声明如下:
int i, j, *p, *q;
*p = 1;
*q = 2;
注意不要把q = p;
和*q = *p
搞混。第一条语句是指针赋值,而第二条语句不是。就如下面的例子:
p = &i;
q = &j;
i = 1;
*q = *p;
赋值语句*q = *p;
是把p
指向的值(i
的值)复制到q
指向的对象(变量j
)中。
11.4 指针作为参数
在9.3节
中我们看到,因为C
语言用值进行参数传递
,所以在函数调用中用作实际参数的变量无法改变。当希望函数能够改变变量时,C
语言的这种特性就很讨厌了。
指针提供了此问题的解决方法:不再传递变量x
作为函数的实际参数,而是提供&x
,即指向x的指针
。声明相应的形式参数p
为指针。调用函数时,p
的值为&x
,因此*p
(p
指向的对象)将是x
的别名。函数体内*
p的每次出现都将是对x的间接引用
,而且函数既可以读取x
也可以修改x
。
void decompose(double x, long *int_part, double *frac_part)
{
*int_part = (long) x;
*fract_part = x - *int_part;
}
decompose
函数的原型既可以是
void decompose(double x, long *int_part, double *frac_part);
也可以是
void decompose(double, long *, double *);
以下列方式调用decompose
函数:
decompose(3.14159, &i, &d);
用指针作为函数的实际参数实际上并不新鲜,从第2章
开始就已经在scanf
函数调用中使用了。思考下面的例子:
int i;
...
scanf("%d", &i);
须把&
放在i
的前面以便给scanf
函数传递指向i
的指针,指针会告诉scanf
函数把读取的值放在哪里。如果没有&
运算符,传递给scanf
函数的将是i
的值。
虽然scanf
函数的实际参数必须是指针,但并不总是需要&
运算符。在下面的例子中,我们向scanf
函数传递了一个指针变量:
int i, *p;
...
p = &i;
scanf("%d", p);
既然p
包含了i
的地址,那么scanf
函数将读入整数并且把它存储在i
中。在调用中使用&
运算符将是错误的:
scanf("%d", &p); /*** WRONG ***/
//scanf函数读入整数并且把它存储在p中而不是i中。
11.4.1 找出数组中的最大元素和最小元素
为了说明如何在函数中传递指针,下面来看一个名为max_min
的函数,该函数用于找出数组中的最大元素和最小元素。调用max_min
函数时,将传递两个指向变量的指针;然后max_min
函数把答案存储在这些变量中。max_min
函数具有下列原型:
void max_min(int a[], int n, int *max, int *min);
max_min
函数的调用可以具有下列的形式:
max_min(b, N, &big, &small);
Enter 10 numbers: 34 82 49 102 7 94 23 11 50 31
Largest: 102
Smallest: 7
/*
maxmin.c
--Finds the largest and smallest elements in an array
*/
#include <stdio.h>
#define N 10
void max_min(int a[], int n, int *max, int *min);
int main(void)
{
int b[N], i, big, small;
printf("Enter %d numbers: ", N);
for (i = 0; i < N; i++)
scanf("%d", &b[i]);
max_min(b, N, &big, &small);
printf("Largest: %d\n", big);
printf("Smallest: %d\n", small);
return 0;
}
void max_min(int a[], int n, int *max, int *min)
{
int i;
*max = *min = a[0];
for (i = 1; i < n; i++) {
if(a[i] > *max)
*max = a[i];
else if(a[i] < *min)
*min = a[i];
}
}
11.4.2 用const保护参数
当调用函数并且把指向变量的指针作为参数传入时,通常会假设函数将修改变量(否则,为什么函数需要指针呢?)。例如,如果在程序中看到语句:f(&x);
大概是希望f
改变x
的值。但是,f
也可能仅需要检查x
的值而不是改变它的值。指针可能高效的原因是,如果变量需要大量的存储空间,那么传递变量的值会浪费时间和空间。
11.5 指针作为返回值
我们不仅可以为函数传递指针,而且还可以编写返回指针的函数。返回指针的函数是相对普遍的,第13章
中将遇到几个。
int *max(int *a, int *b)
{
if (*a > *b)
return a;
else
return b;
}
调用max
函数时,用指向两个int
类型变量的指针作为参数,并且把结果存储在一个指针变量中:
int *p, i, j;
...
p = max(&i, &j);
调用max
期间,*a
是i
的别名,而*b
是j
的别名。如果i
的值大于j
,那么max
返回i
的地址;否则,max
返回j
的地址。调用函数后,p
或者指向i
,或者指向j
。
指针可以指向数组元素,而不仅仅是普通变量。设a
为数组,则&a[i]
是指向a
中元素i
的指针。当函数的参数中有数组时,返回一个指向数组中的某个元素的指针有时是挺有用的。
例如,下面的函数假定数组a
有n
个元素,并返回一个指向数组中间元素的指针:
int *find_middle(int a[], int n) {
return &a[n/2];
}
//第12章会详细讨论指针和数组的关系。
问与答
答:通常是,但不总是。考虑用字而不是字节划分内存的计算机。字可能包含36
位、60
位等。
当用字划分内存时,每个字都有一个地址。通常整数占一个字长度,所以指向整数的指针可以就是一个地址。但是,字可以存储多于一个的字符。例如,36
位的字可以存储6
个6
位的字符,或者4
个9
位的字符。
由于这个原因,可能需要用不同于其他指针的格式存储指向字符的指针
。指向字符的指针可以由地址(存储字符的字)加上一个小整数(字符在字内的位置)组成。
在一些计算机上,指针可能是“偏移量”而不完全是地址。例如,Intel x86 CPU
(用于许多个人计算机)可以在多种模式下执行程序。最老的模式称为实模式(real mode)
,可以追溯到1978年的8086
处理器。在这种模式下,地址有时用一个16
位数(偏移量)表示,有时用两个16
位数(段—偏移量对)表示。偏移量不是真正的内存地址,CPU
必须把它和存储在专用寄存器中的段值结合起来。为了支持实模式,旧的C
语言编译器通常提供两种指针:近指针(16
位偏移量)和远指针(32
位段—偏移量对)。这些编译器通常保留单词near
和far
作为非标准关键字,用于指针变量的声明。
答:可能。17.7节
将介绍指向函数的指针
。
答:造成困惑的根源在于,根据使用上下文的不同,C
语言中的*
号可以有多种含义。
在声明int *p = &i;
中,*
号不是间接寻址运算符,其作用是指明p
的类型以便告知编译器p
是一个指向int
类型变量的指针;而在语句中出现时,*
号(作为一元运算符使用时)会执行间接寻址。
语句*p = &i;
是不正确的,因为它把i
的地址赋给了p
指向的对象,而不是p
本身。
答:任何指针(包括变量的地址)都可以通过调用printf
函数并在格式串中使用转换说明%p
来显示。详见22.3
节。
答:不是。这说明不能改变指针p
指向的整数,但是并不阻止f
改变p
自身。
void f(const int *p)
{
int j;
*p = 0; /*** WRONG ***/
p = &j; /* legal */
}
因为实际参数是值传递的,所以通过使指针指向其他地方的方法给p
赋新值不会对函数外部产生任何影响
。
答:是合法的。然而效果不同于把const
放在p
的类型前面。在11.4
节中已经见过在p
的类型前面放置const
可以保护p
指向的对象。在p
的类型后面放置const
可以保护p
本身:
void f(int * const p)
{
int j;
*p = 0; /* legal */
p = &j; /*** WRONG ***/
}
这一特性很少用到。因为p
仅仅是另一个指针(调用函数时的实际参数)的副本,所以极少有什么理由保护它。
更罕见的一种情况是需要同时保护p
和它所指向的对象,这可以通过在p
类型的前后都放置const
来实现:
void f(const int * const p)
{
int j;
*p = 0; /*** WRONG ***/
p = &j; /*** WRONG ***/
}