C语言期末速成8:指针
指针的本质、*p++ 与 (*p)++、二级指针、指针数组 vs 数组指针、函数指针。
本文是《C语言期末速成》系列第 8 篇。如果说 C 语言有什么”灵魂”,那就是指针。普通变量放数值,指针变量放地址——就这么一句话,但延伸出
*p与p、二级指针、指针数组、函数指针等一堆考点。本章从一个比喻出发,把这些概念一次打通。
上一篇:C语言期末速成 7:数组
考点清单
- 指针变量的本质是放地址,普通变量放数值
int *p:p是地址,*p是 p 指向的那个变量(数值)*p++是地址移动;(*p)++是数值自增(口诀:取当前值再操作)- 二级指针
**q:存放一级指针的地址 - 三名主义:数组名 = 首元素地址、函数名 = 函数入口地址、字符串常量名 = 首字符地址
- 传值与传地址:传地址能改实参
- 函数返回地址时,返回类型要写
*:int *fun(...) - 指向哪个就等价哪个:
*p出现的地方可以用它指向的变量等价替换 - 指针数组:
int *p[3](是数组);数组指针:int (*p)[3](是指针)
一、指针是什么:放地址的变量
内存是编好号的”储物柜”。普通变量 int a = 5 把 5 存进某个柜子;指针变量 int *p = &a 则是把”那个柜子的编号”存进 p。
int a = 5;
int *p = &a; /* p 里放的是 a 的地址 */
printf("%d\n", a); /* 5:直接读变量 */
printf("%p\n", p); /* a 的地址 */
printf("%d\n", *p); /* 5:顺着地址取 a 的数值 */
两个符号的分工:
| 写法 | 含义 |
|---|---|
p | 地址(指向哪里) |
*p | 数值(那里存的值),可以当变量用 |
*p 可以当变量用:*p = 20; 就是在修改 p 指向的变量,即 a = 20。
判断口诀:* 表示”顺着地址取里面的值”。
指针的两种初始化方式
/* 方法一:定义时同时初始化 */
int a = 2;
int *p = &a;
/* 方法二:定义后再初始化 */
int b = 2;
int *q;
q = &b;
NULL 指针:野指针与悬空指针(加深)
指针有三个容易混的状态:
| 状态 | 含义 | 正确做法 |
|---|---|---|
| NULL 指针 | 指向地址 0,表示”不指向任何东西” | 初始化或清理时用 int *p = NULL; |
| 野指针 | 定义了却没初始化,里面是垃圾地址 | 定义即初始化,别让它裸奔 |
| 悬空指针 | 指向的内存已被释放(如 free 后) | free(p) 后立刻 p = NULL; |
判断指针是否为空,直接拿指针本身当条件用:
int *p = NULL;
if (p) /* 非空才进来:p != NULL 才为真 */
*p = 1;
if (!p) /* 空才进来:p == NULL 才为真 */
printf("p 还没有指向任何东西\n");
不要解引用 NULL 或野指针:*p 访问的是”地址 0 处的内存”,程序会直接崩溃(空指针解引用是未定义行为)。这也是 malloc 后要检查返回值是否为 NULL 的原因(第 10 章)。
scanf 里的指针考点
第 4 章预告过:scanf 需要地址。
int x = 2, *p = &x;
scanf("%d", x); /* 错:x 是数值 */
scanf("%d", p); /* 对:p 就是 x 的地址 */
scanf("%d", &p); /* 错:这是指针变量自己的地址 */
scanf("%d", *p); /* 错:*p 是数值 */
凡是 scanf 第二个参数,都必须是”地址”。
二、指针的算术运算
2.1 *p++ vs (*p)++(必考)
这是改错题和概念题的高频点,关键看 ++ 作用在谁身上:
*p++:++作用在p上 → 地址移动(先取当前值,再移动指针);(*p)++:++作用在(*p)上 → 数值自增(指针不动,指向的值加 1)。
int a[] = {1, 3, 5, 7, 9};
int *p = a;
/* *p++:表达式值为 1(当前 *p);随后 p 移到 a[1] */
int v1 = *p++; /* v1 = 1,现在 p 指向 3 */
/* (*p)++:表达式值为 3(当前 *p);随后把 a[1] 变成 4 */
int v2 = (*p)++; /* v2 = 3,a[1] 变成 4,p 还在原地 */
口诀:*p++ 取完值地址后移;(*p)++ 取完值数值加 1。区分方法:看括号,括号让 ++ 先跟 *p(数值)结合,没有括号则 ++ 跟 p(地址)结合。
2.2 指针加减整数
指针加 k = 地址前进 k 个”所指向类型的大小”:
int a[5] = {10, 20, 30, 40, 50};
int *p = a;
p = p + 2; /* p 指向 a[2] */
printf("%d\n", *p); /* 30 */
p 每加 1 跳过一个 int(4 字节)。指针算术的意义在数组上最明显——用指针遍历数组:
int a[5] = {10, 20, 30, 40, 50};
for (int *p = a; p < a + 5; p++)
printf("%d ", *p); /* 10 20 30 40 50 */
三、二级指针:存”指针的地址”
一级指针 *p 存变量的地址;二级指针 **q 存一级指针的地址。
int x = 7;
int *p = &x; /* p 存 x 的地址 */
int **q = &p; /* q 存 p 的地址 */
printf("%d\n", *p); /* 7:p 指向 x */
printf("%p\n", *q); /* x 的地址:*q 就是 p */
printf("%d\n", **q); /* 7:**q 就是 *p 就是 x */
硬规则:二级指针只能存放一级指针的地址。
int x = 7;
int *p = &x;
int **q = &p; /* 对 */
int **r = &x; /* 错!&x 是一级指针的地址,类型不匹配 */
为什么要二级指针?最常见场景是”在函数里修改调用者传入的指针本身”(比如改变指针的指向),一级指针参数改不了指针变量的值,得用二级指针。
四、三名主义:三个”首地址”
记住三个”名”都代表地址,且都不能自增赋值:
| 名字 | 含义 | 等价 |
|---|---|---|
数组名 a | 首元素地址 | &a[0] |
函数名 add | 函数入口地址 | &add |
字符串常量名 "abc" | 首字符地址 | &"abc"[0] |
char *s = "hello"; /* "hello" 的首字符地址放进 s */
数组名、函数名、字符串常量名都是地址常量——a++、add++ 之类的写法非法。
五、移动指针遍历字符串(加深)
用指针遍历一串字符,遇到 \0 停止:
char *s = "meikanshu";
while (*s) { /* *s 非 0(没到 '\0')就继续 */
printf("%c", *s);
s++; /* 地址移动 */
}
/* 打印 meikanshu */
while (*s) 的妙处:字符串以 \0(ASCII 0)结尾,*s 恰好用”值是否为 0”充当循环条件。这是”指针 + 字符指针 + 循环”的经典组合,第 9 章还会用它手写字符串函数。
六、传值 vs 传地址(指针版再复习)
第 6 章已学过,这里从指针视角重申——要不要加 & 和 * 取决于你想不想改实参:
void no_change(int a, int b) { int t = a; a = b; b = t; } /* 传值,没用 */
void do_change(int *a, int *b) { int t = *a; *a = *b; *b = t; } /* 传地址,有效 */
int x = 1, y = 3;
no_change(x, y); /* 1 3 */
do_change(&x, &y); /* 3 1 */
“指向哪个就等价哪个”:*a 指向 x,凡是 *a 出现的地方,都可以看作 x 本身。所以 *a = *b 就是 x = y。
int a = 2, *p = &a;
*p = *p + 2; /* 等价于 a = a + 2 → a = 4 */
七、函数返回地址:返回值带 *
函数返回值可以是地址,前提是返回类型写成 指针:
int *getMax(int *a, int *b) { /* 返回 int*,即返回地址 */
return *a > *b ? a : b;
}
int main(void) {
int x = 7, y = 8;
int *max = getMax(&x, &y); /* max 接收返回的地址 */
printf("%d\n", *max); /* 8 */
return 0;
}
注意:函数返回的是”某个变量的地址”,因此该变量的生命周期要能覆盖使用它的时刻——不要返回局部变量的地址(函数返回后局部变量空间就销毁了)。返回全局变量、static 变量、或外部传入的地址才安全。
八、指针数组 vs 数组指针(易混淆,必考)
区别在谁先跟谁结合,看括号和类型的位置:
| 写法 | 本质 | 含义 |
|---|---|---|
int *p[3] | 数组(数组,里面存 3 个 int*) | 指针数组 |
int (*p)[3] | 指针(指向含 3 个 int 的数组) | 数组指针 |
int *p[3] vs int (*p)[3] | 括号改变结合方向 | —— |
记忆:*p[3] 里 [] 优先级比 * 高,所以 p[3] 先结合,p 是数组;加了括号 (*p)[3] 让 *p 先结合,p 是指针。
int a[3] = {1, 2, 3};
int *arrp[3]; /* 指针数组:三个指针元素 */
int (*ap)[3] = &a; /* 数组指针:指向 int[3] 这种类型的指针 */
arrp[0] = &a[0]; /* 指针数组里放地址 */
printf("%d\n", (*ap)[1]); /* 2:*ap 得到 a,再取下标 */
二维数组里的行指针 a(第 7 章)本质就是指向一行的数组指针:a + 1 跳一行,正好和 int (*p)[3] 的 p + 1 跳 3 个 int 对应。
九、函数指针:把函数当变量存
函数名是函数入口地址,可以存进”函数指针”,然后通过它调用函数:
int add(int x, int y) { return x + y; }
int main(void) {
int (*f)(int, int); /* f 是函数指针:指向"返回 int、两个 int 参数"的函数 */
f = add; /* 函数名 add 就是地址,直接赋值 */
printf("%d\n", add(2, 3)); /* 方式一:直接调 */
printf("%d\n", f(2, 3)); /* 方式二:通过指针调 */
printf("%d\n", (*f)(2, 3)); /* 方式三:解引用后再调(等价) */
return 0;
}
三种调用方式等价:add(2,3)、f(2,3)、(*f)(2,3)。函数指针最常见的用途是把函数作为参数传给另一个函数(回调),例如排序时传入比较函数。
声明语法拆解:int (*f)(int,int) 中 (*f) 表示 f 是指针,后面 (int,int) 表示它指向的函数带两个 int 参数,最前 int 是函数返回类型。
易错点 / 陷阱
*p++和(*p)++混用:前者地址移动、后者数值自增。int *p[3]当数组指针:[]优先,它是指针数组(数组);int (*p)[3]才是数组指针。- 二级指针存一级变量的地址:
int **q = &x非法,必须是int **q = &p(p 是一级指针)。 - 对数组名
a++:a 是地址常量,不能自增。 - 返回局部变量的地址:函数返回后局部变量销毁,返回它的地址是悬空指针。
scanf("%d", *p)当正确:*p 是数值不是地址。*(p+2)算成”第 2 个元素”:p+2指向下标 2,即第 3 个元素。- 函数指针写成
int *f(...):那是”返回指针的函数”,不是函数指针;函数指针要int (*f)(...)。
自测小练习
第 1 题:int x=10, *p=&x; 求 *p、p、&x 之间的关系。
第 2 题:int a[]={1,3,5,7}; int *p=a; 执行 int v = *p++; 后,v 是多少?p 指向哪?
第 3 题:int x=7; int *p=&x; int **q=&p; 求 *p、*q、**q。
第 4 题:解释 int *p[3] 和 int (*p)[3] 的区别。
第 5 题:int add(int,int); 写出它的函数指针声明,并列出三种等价调用。
参考答案
p里存的就是&x,所以p == &x;*p == x == 10。*p++先取当前值1给 v,再移动指针 →v=1,p指向a[1](3)。*p = 7(x 的值);*q = p(一级指针的值,即 x 的地址);**q = 7。int *p[3]是数组(3 个 int 指针);int (*p)[3]是指针(指向含 3 个 int 的数组)。- 声明
int (*f)(int, int);,赋值f = add;,三种调用add(2,3)、f(2,3)、(*f)(2,3)。
下一篇:C语言期末速成 9:字符串