本专题介绍 C 语言中内存地址及其与指针变量的关系,然后介绍取地址运算符和间接寻址运算符以及指针赋值的内容,最后介绍函数与指针的相关内容。
参考资料:《C 语言程序设计 · 现代方法 第 2 2 2 版》
1. 指针变量
理解指针的第一步是在机器级上观察指针表示的内容。大多数现代计算机都将内存分割为字节(
byte
\text{byte}
byte),每个字节可以存储
8
8
8 位的信息。每个字节都有唯一的地址(
address
\text{address}
address),用来和内存中的其他字节相区别。如果内存中有
n
n
n 个字节,那么可以把地址看作
0
∼
n
−
1
0 \sim n - 1
0∼n−1 的数。
可执行程序由代码(原始 C 程序中与语句对应的机器指令)和数据(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节内存,把第一个字节的地址称为是变量的地址。下图中,变量 i
占有地址为
2000
2000
2000 和
2001
2001
2001 的两个字节,所以变量 i
的地址是
2000
2000
2000:
这就是指针的出处。虽然用数表示地址,但是地址的取值范围可能不同于整数的范围,所以一定不能用普通整型变量存储地址。但是,可以用特殊的指针变量(
pointer variable
\text{pointer variable}
pointer variable)存储地址。在用指针变量 p
存储变量 i
的地址时,我们说 p
“指向” i
。换句话说,指针就是地址,而指针变量就是存储地址的变量。为了说明指针变量 p
存储变量 i
的地址,将把 p
的内容显示为指向 i
的箭头:
指针变量的声明
对指针变量的声明与对普通变量的声明基本一样,唯一的不同就是必须在指针变量名字前放置星号:
int *p;
上述声明说明 p
是指向 int
类型对象的指针变量。这里我们用术语对象来代替变量,是因为 p
可以指向不属于变量的内存区域(见专题十六)。
指针变量可以和其他变量一起出现在声明中:
int i, j, a[10], b[20], *p, *q;
C 语言要求每个指针变量只能指向一种特定类型(引用类型)的对象:
int *p; /* points only to integers */
double *q; /* points only to doubles */
char *r; /* points only to characters */
至于引用类型是什么类型则没有限制。事实上,指针变量甚至可以指向另一个指针,即指向指针的指针(见专题十六)。
2. 取地址运算符和间接寻址运算符
为使用指针,C 语言提供了一对特殊设计的运算符。为了找到变量的地址,可以使用 &
(取地址)运算符。如果 x
是变量,那么 &x
就是 x
在内存中的地址。为了获得对指针所指向对象的访问,可以使用 *
(间接寻址)运算符。如果 p
是指针,那么 *p
表示 p
当前指向的对象。
2.1 取地址运算符
声明指针变量是为指针留出空间,但是并没有把它指向对象:
int *p; /* points nowhere in particular */
在使用前初始化 p
是至关重要的。一种初始化指针变量的方法是使用 &
运算符把某个变量的地址赋给它,或者更常采用左值:
int i, *p;
...
p = &i;
通过把 i
的地址赋值给变量 p
的方法,上述语句把 p
指向了 i
:
在声明指针变量的同时对它进行初始化是可行的:
int i;
int *p = &i;
甚至可以把 i
的声明和 p
的声明合并,但是需要首先声明 i
:
int i, *p = &i;
2.2 间接寻址运算符
一旦指针变量指向了对象,就可以使用 *
(间接寻址)运算符访问存储在对象中的内容。例如,如果 p
指向 i
,那么可以如下所示显示出 i
的值:
printf("%d\n", *p);
printf
函数将会显示 i
的值,而不是 i
的地址。
习惯于数学思维的读者可能希望把 *
想象成 &
的逆运算。对变量使用 &
运算符产生指向变量的指针,而对指针使用 *
运算符则可以返回到原始变量:
j = *&i; /* same as j = i; */
只要 p
指向 i
,*p
就是 i
的别名。*p
不仅拥有和 i
相同的值,而且对 *p
的改变也会改变 i
的值。(*p
是左值,所以对它赋值是合法的)下面的例子说明了 *p
和 i
的等价关系,这些图显示了在计算中不同的点上 p
和 i
的值。
注意不要把间接寻址运算符用于未初始化的指针变量。如果指针变量 p
没有初始化,那么试图使用 p
的值会导致未定义的行为,给 *p
赋值尤其危险。如果 p
恰好具有有效的内存地址,下面的赋值会试图修改存储在该地址的数据:
int *p;
*p = 1; /*** WRONG ***/
如果上述赋值改变的内存单元属于该程序,那么可能会导致不规律的行为;如果改变的内存单元属于操作系统,那么很可能会导致系统崩溃。编译器可能会给出警告消息,告知 p
未初始化,所以请留意获得的警告消息。
3. 指针赋值
C 语言允许使用赋值运算符进行指针的复制,前提是两个指针具有相同的类型。假设有如下声明:
int i, j, *p, *q;
语句 p = &i;
是指针赋值的示例,把 i
的地址复制给 p
。q = p
是另一个指针赋值的示例,这条语句是把 p
的内容(即 i
的地址)复制给 q
,效果是把 q
指向了 p
所指向的地方:
现在 p
和 q
都指向了 i
,所以可以用对 *p
或 *q
赋新值的方法来改变 i
:
任意数量的指针变量都可以指向同一个对象。注意不要把 q = p;
和 *q = *p;
搞混。第一条语句是指针赋值,而第二条语句不是。就如下面的例子显示的:
赋值语句 *q = *p
是把 p
指向的值(i
的值)复制到 q
指向的对象(变量 j
)中。
4. 指针作为参数
因为 C 语言用值进行参数传递,所以在函数调用中用作实际参数的变量无法改变。当希望函数能够改变变量时,C 语言的这种特性就很麻烦。
指针提供了此问题的解决方法:不再传递变量 x
作为函数的实际参数,而是提供 &x
,即指向 x
的指针。声明相应的形式参数 p
为指针。调用函数时,p
的值为 &x
,因此 *p
将是 x
的别名。函数体内 *p
的每次出现都将是对 x
的间接引用,而且允许函数既可以读取 x
也可以修改 x
。
下面通过把形式参数 int_part
和 frac_part
声明成指针的方法来修改专题八中的 decompose
函数:
void decompose(double x, long *int_part, double *frac_part)
{
*int_part = (long) x;
*frac_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);
因为 i
和 d
前有取地址运算符 &
,所以 decompose
函数的实际参数是指向 i
和 d
的指针,而不是 i
和 d
的值。调用 decompose
函数时,把值
3.14159
3.14159
3.14159 复制到 x
中,把指向 i
的指针存储在 int_part
中,而把指向 d
的指针存储在 frac_part
中:
decompose
函数体内的第一个赋值把 x
的值转换为 long
类型,并且把此值存储在 int_part
指向的对象中。因为 int_part
指向 i
,所以赋值把值
3
3
3 放到 i
中:
第二个赋值把 int_part
指向的值(即 i
的值)取出,现在这个值是
3
3
3,把此值转换为 double
类型,并且用 x
减去它,得到
0.14159
0.14159
0.14159。然后把这个值存储在 frac_part
指向的对象中:
当 decompose
函数返回时,就像原来希望的那样,i
和 d
将分别有值
3
3
3 和
0.14159
0.14159
0.14159。
用指针作为函数的实际参数,我们在 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);
将读入整数并且把它存储在 p
中而不是 i
中。
向函数传递需要的指针却失败了可能会产生严重的后果。假设我们在调用 decompose
函数时没有在 i
和 d
前面加上 &
运算符,当 decompose
函数把值存储到 *int_part
和 *frac_part
中时,它会把 i
和 d
的值当成指针来使用,从而修改未知的内存地址,而不是修改 i
和 d
。如果已经提供了 decompose
函数的原型,那么编译器将告诉我们实际参数的类型不对。然而,在 scanf
的例子中,编译器通常不会检查出传递指针失败,因此 scanf
函数特别容易出错。
程序
maxmin.c
:找出数组中的最大元素和最小元素
下面来看一个名为max_min
的函数,该函数用于找出数组中的最大元素和最小元素。调用max_min
函数时,将传递两个指向变量的指针;然后max_min
函数把答案存储在这些变量中。
/* 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];
}
}
这个程序的运行过程如下(用户的输入用下划线标注):
Enter 10 numbers:
34
82
49
102
7
94
23
11
50
31
‾
\underline{34\ 82\ 49\ 102\ 7\ 94\ 23\ 11\ 50\ 31}
34 82 49 102 7 94 23 11 50 31
Largest: 102
Smallest: 7
用 const
保护参数
当调用函数并且把指向变量的指针作为参数传入时,通常会假设函数将修改变量。例如,如果在程序中看到语句 f(&x);
,大概是希望 f
改变 x
的值。但是,f
仅需要检查 x
的值而不是改变它的值也是可能的。指针可能高效的原因是:如果变量需要大量的存储空间,那么传递变量的值会浪费时间和空间。
可以使用单词 const
来表明函数不会改变指针参数所指向的对象。const
应放置在形式参数的声明中,后面紧跟着形式参数的类型说明:
void f(const int *p)
{
*p = 0; /*** WRONG ***/
}
这一用法表明 p
是指向 “常整数” 的指针。试图改变 *p
是编译器会检查的一种错误。
5. 指针作为返回值
我们不仅可以为函数传递指针,还可以编写返回指针的函数。当给定指向两个整数的指针时,下列函数返回指向两整数中较大数的指针:
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
。
这个例子中 max
函数返回的指针是作为实际参数传入的两个指针中的一个,但这不是唯一的选择。函数也可以返回指向外部变量或指向声明为 static
的局部变量的指针。
永远不要返回指向自动局部变量的指针:
int *f(void)
{
int i;
...
return &i;
}
一旦 f
返回,变量 i
就不存在了,所以指向变量 i
的指针将是无效的。有的编译器会在这种情况下给出类似 “
function returns address of local variable
\text{function returns address of local variable}
function returns address of local variable” 的警告。
指针可以指向数组元素,而不仅仅是普通变量。设 a
为数组,则 &a[i]
是指向 a
中元素 i
的指针。当函数的参数中有数组时,返回一个指向数组中的某个元素的指针有时是挺有用的。例如,下面的函数假定数组 a
有
n
n
n 个元素,并返回一个指向数组中间元素的指针:
int *find_middle(int a[], int n)
{
return &a[n/2];
}