指针

If two people write exactly the same program, each should be put in micro-code and then they certainly won’t be the same.

目录


[TOC]

指针


零 前言

指针是 C 语言最重要——也是最常被误解——的特性之一。本节重点介绍指针的基础内容。

一 指针变量

现代大多数计算机将内存分割为字节(byte),每个字节可以存储 8 位的信息:0000 0001

每个字节都有唯一的地址(address),用来和内存种的其他字节相区别。如果内存中有 n 个字节,那么可以把地址看作 0 ~ n - 1的数。

可执行程序由代码(原始 C 程序中于语句对应的机器指令)和 数据(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节,把第一个字节的地址称为是变量的地址。

上图中,i 占有的字节是 2000 ~ 2003 4 个字节,2000 就是 i 的地址。

虽然用数表示地址,但是地址的取值范围可能不同于整数的取值范围,所以一定不能用普通的整型变量存储地址。

但是,我们可以用特殊的指针变量(pointer variable)存储地址。在用指针变量存储 p 存储变量 i 的地址时,我们说 p “指向” i 。换句话说,指针就是地址,而指针变量就是存储地址的变量。

1. 指针变量的声明

int* p;

上述声明说明p 是指向 int 类型对象的指针变量。这里我们用术语对象代替变量,这是因为 p 可以指向不属于变量的内存区域。(后面会讲)

指针变量可以与其他变量一起出现在声明中:

int a, b[10], *p, *q;

C 语言要求每个指针变量只能指向一种特定类型(引用类型)的对象。

int* p;
double* q;
char* r;

关于指针变量声明中 * 与谁挨着的问题

请看下面的声明:

int* p,q;

请问,上面的声明中 p 和 q 都是指针变量吗?

小黄:我觉得是,如果你写成这样:

int *p, q;

那就是只有 p 是指针变量了。

程序圆:你这样想就大错特错啦,上面这两种写法是等价的。都是声明 p 为指针变量而 q 是一个普通的 int 类型变量。

小黄:哦~那我们平时应该选择那种写法呢?

程序圆:通常情况下我们都是选择第一种写法,即:int* p。但是这样确实容易造成误解,所以我们通常一行只声明一个指针变量就可以了。


二 取地址运算符和间接寻址运算符

1. 取地址运算符

声明指针变量时我们没有将它指向任何对象:

int* p;

在使用之前初始化 p 是至关重要的。使用取地址运算符&把某个变量的地址赋值给它。

int i;
p = &i; //&i 就是 i 在内存中的地址

现在 p 就指向了整型变量 i

我们也可以声明的同时初始化:

int i;
int* p = &i;

甚至可以这样:

int i, *p = &i;

但是需要先声明 i

2. 间接寻址运算符

间接寻址运算符也叫解引用运算符,我个人还是喜欢叫它用解引用运算符。

int i;
int* p = &i;

指针变量 p 指向 i,使用*运算符可以访问存储在对对象中的内容(访问存储在指针变量指向的地址上的内容)。

printf("%d", *p); // (*p == i)

*&互为逆运算”:

j = *&i;// same as j = i;

只要 p 指向 i,*p 就是 i 的别名。**p 不仅拥有和 i 相同的值,而且 p 的改变也会改变 i 的值。

int i = 0;
int* p = &i;
printf("i = %d\n", i);
printf("p = %d\n", *p);
// 输出:0 0
*p = 1;
printf("now i = %d\n", i);
printf("now p = %d\n", *p);
//输出:1 1

注意:

解引用未初始化的指针变量会导致未定义行为

int* p;
printf("%d", *p);

*p赋值尤为危险。如果 p 恰好具有有效的内存地址,程序会试图修改存储在该地址的数据:

int* p;
*p = 1; // wrong

这是极度不安全的行为。好在我们的编译器会给出警告。即使这样使用了,编译器不会真的让你去修改其他地方(比如操作系统等)的数据。

所以如果你定义的指针特别多,你也不知道那个会被用上,可以这样初始化指针变量:

int* p = NULL; // NULL 表示空指针,该处的内存无法修改

然后在需要对 p 解引用的地方添加一个判断:

if(p != NULL){
...;
}

三 指针赋值

C 语言允许相同类型的指针变量进行赋值。

int i;
int* p = &i;
int* q;
q = p;

或者直接初始化并赋值:

int* q = p;

现在可以通过改变 *p 的值来改变 i :

int i = 0;
int* p = &i;
int* q = p;
printf("now i = %d\n", i);
printf("now p = %d\n", *q);
// 输出:0 0
*q = 2;
printf("now i = %d\n", i);
printf("now p = %d\n", *q);
//输出:2 2

不要将 *q = *pq = p 搞混,前者是将 p 指向的对象的值(变量 i 的值)赋值给 q 指向的对象(变量 j)中。

四 指针作为参数

还记得之前分解小数的函数 decompose 吗?我们曾将想在这个函数中通过改变形参来改变实参,但是我们失败了,今天我们再来重新看一下如何用指针作为参数完成这一任务:

将 decompose 函数定义中的形参 int_part 和 frac_part 声明成指针类型。

void decompose(double x, long* int_part; double* frac_part){
*int_part = (long)x;
*frac_part = x - *int_part;
}

调用该函数:

int i;
double x, d;
decompose(x, &i, &d);

当函数调用完成,实参 i 和 d 的值也修改了。你可以再 main 函数中输出一下 i 和 d 测试一下。

用指针作为参数其实并不新鲜:

int i;
scanf("%d", &i);

必须将 & 放在 i 前以便传给 scanf 函数指向 i 的指针,指针会告诉 scanf 函数将读取的值放在那里。如果没有 & 传递给 scanf 的将是 i 的值。

虽然 scanf 函数的实参必须是指针,但是并不是总需要 & 运算符:

int i;
int* p = &i;
scanf("%d", p);

p 已经包含了 i 的地址,所以不需要 &。使用 & 是错误的:

scanf("%d", &p);

scanf 函数将把读入的整数放在 p 中而不是 i 中。

注意:

向函数传递需要的指针却失败了可能会造成严重后果。比如,如果我们在调用 decompose 函数时没有在 i 和 d 前加上 & :

decompose(x, i, d);

函数期望的第二和第三个参数是指针,但传入的却是 i 和 d 的值。decompose 函数没有办法区分,所以它会把 i 和 d 的值当作指针来使用(指针本身是整数)。当函数修改 *int_part 和 *frac_part 时,它会修改未知的内存地址,而不是修改 i 和 d。

如果已经提供了函数原型,那么编译器将告诉我们实参类型不对。然而对于 scanf 来说,编译器通常不会检查出传递指针失败,因此 scanf 函数特别容易出错。

程序:找出数组中的最大元素和最小元素

与程序的交互如下:

Enter 5 numbers:9 5 2 7 8
Largest: 9
Smallest: 2

参考程序:

#include<stdio.h>

#define SIZE 5

void max_min(int a[], int len, int* max, int* min);

int main(void) {

int a[SIZE];
int max, min, i;

printf("Enter 5 numbers: ");
for (i = 0; i < SIZE; i++)
scanf("%d", &a[i]);

max_min(a, SIZE, &max, &min);

printf("Largest: %d\n", max);
printf("Smallest: %d\n", min);

return 0;
}

void max_min(int a[], int len, int* max, int* min) {

int i;

*max = *min = a[0];
for (i = 1; i < len; i++) {
// a[i] 如果比 *max 大 那肯定不会比 *min 小,反之也成立
if (a[i] > * max)
*max = a[i];
else if (a[i] < *min)
*min = a[i];
}
}

用 const 保护参数

指针传参时,可以使用 const来表明函数不会改变指针参数所指向的对象。 const 应放于形参中:

void f(const int* p){
...;
}

试图改变 *p 时编译器会报错。


小黄:const 一定只能放在 int 之前吗?这样写合不合法:

int* const p;

程序圆:合法。但是和上面那种声明方式的含义不同。上面的 const 修饰的是 *p,使得 *p 不能被修改;而这一种 const 修饰的 p,使得 p 的指向不能发生改变:

int i,j;
const int* p = &i;
int* const q = &j;

*p = 0; // wrong
p = q; // ok

*q = 0; // ok
q = p; // wrong

你甚至可以这样写:

int const *p;

这和第一中写法是同一个意思:使得 *p 不能被修改。


五 指针作为返回值

请看返回值类型为 int*类型的函数 max:

int* max(int* a, int* b){
if(*a > *b)
return a;
else
return b;
}

max 返回较大数的指针。

调用:

int a,b;
int* p = max(&a, &b);

需要使用相同的指针类型接收返回值。

注意:

永远不要返回指向自动局部变量的指针:

int* f(){
int i;
...
return i;
}

一旦 f 返回,i 就不存在了,所以指向 i 的指针是无效的。有的编译器可能给出警告:“function returns address of local variable”

参考资料:《C语言程序设计:现代方法》

[^1]: 如果两个人用低级语言写同一个程序,它们显然不会相同。Epigrams on Programming 编程警句