C语言指针与 printf 的爱恨情仇

C 语言的指针一直以来都是初学者的噩梦,尤其是当它和 printf、数组以及自增自减运算符混在一起的时候。

最近看到一道非常经典的指针代码阅读题,它不仅考察了基础的指针运算,还隐藏了几个关于 printf 内存机制的深坑。今天我们就借这道题,来一次彻底的“排雷”行动。

01. 原题重现

请阅读以下代码,并写出程序的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

int main() {
int i = 0;
char b[] = "program"; // 字符数组
char *a = "PROGRAM"; // 字符指针

// 填空 1
printf("%c%s\n", *a, b + 1);

// 填空 2
while (putchar(*(a + i))) {
i++;
}

// 填空 3
printf("i = %d\n", i);

// 填空 4
while (--i) {
putchar(*(b + i));
}

// 填空 5
printf("\n%s\n", &b[3]);

return 0;
}

如果你想先自己挑战一下,可以在这里暂停。

02. 逐行代码拆解

让我们像编译器一样,一行一行地执行这段代码。

第一步:基础输出

1
printf("%c%s\n", *a, b + 1);
  • *a:a 指向字符串 “PROGRAM” 的首地址。*a 取出了第一个字符 ‘P’
  • b + 1:b 是数组 “program” 的首地址。b+1 意味着指针向后移动一位,指向了字符 ‘r’。%s 会从这个位置开始一直打印到结束符,所以输出 “rogram”
  • 结果 1: Program

第二步:正序循环

1
2
3
while (putchar(*(a + i))) {  
i++;
}
  • 这是一个典型的利用字符串结束符 \0 (ASCII 为 0) 来终止的循环。
  • 它依次取出了 “PROGRAM” 的每一个字符并打印。
  • 当 i 增加到 7 时,*(a+7) 读取到了 \0,putchar 返回 0,循环结束。
  • 结果 2: PROGRAM (注意:此处没有换行)

第三步:变量状态

1
printf("i = %d\n", i);
  • 循环结束时,i 已经自增到了 7。
  • 结果 3: i = 7 (在控制台中,这行文字会紧接在 PROGRAM 后面显示)

第四步:倒序循环(关键点)

1
2
3
while (--i) {  
putchar(*(b + i));
}
  • 注意前置减减 (–i) :先减 1,再判断。
  • 初始 i=7。第一次循环:i 变为 6,输出 b[6] (‘m’)。
  • …依次倒序输出…
  • 最后一次:i 变为 1,输出 b[1] (‘r’)。
  • 结束条件:当 i 变为 0 时,条件为假,循环停止。因此 b[0] (‘p’) 不会被输出。
  • 结果 4: margor

第五步:取地址输出

1
printf("\n%s\n", &b[3]);
  • &b[3] 取出了数组第 4 个元素 (‘g’) 的地址
  • %s 从这个地址开始往后打印,直到字符串结束。
  • 结果 5: gram

✅ 最终完整输出

1
2
3
4
5
Program  
PROGRAM
i = 7
margor
gram

03. 进阶:那些会导致“崩溃”的写法

这道题做对不难,但真正有价值的是:如果我们稍微改动一下 printf 里的参数,会发生什么? 很多人就是在这里栽了跟头。

💣 死亡陷阱 1:printf(b[3])

如果你写成:

1
printf(b[3]); // 或者 printf(*b);

后果:程序崩溃 (Crash / Segmentation Fault)

深度解析:

  1. b[3] 的值是字符 ‘g’ 。在计算机眼中,它就是整数 103 (ASCII码)。
  2. printf 的第一个参数要求是内存地址(用来读取格式字符串)。
  3. 你把 103 传进去,计算机会试图去访问内存地址 103
  4. 低位内存地址通常是系统保留区,这是非法访问。操作系统会立刻“杀死”你的程序。

💣 死亡陷阱 2:printf(“%s”, *a)

如果你写成:

1
printf("%s", *a);

后果:程序崩溃 (Crash)

深度解析:
原理同上。

  • *a 是字符 ‘P’ (ASCII 80)。
  • %s 需要一个地址。
  • 程序试图去访问内存地址 80 来读取字符串,再次触发非法访问。

04. 进阶:那些会导致“乱码”的写法

😵 乱码陷阱 1:printf(“%c”, b+1)

如果你写成:

1
printf("%c", b+1);

后果:输出乱码

深度解析:
这是典型的类型不匹配。

  • %c 想要一个具体的字符值(比如 114)。
  • b+1 给的是一个内存地址(比如 0x64fe10)。
  • printf 会强行把这个巨大的地址数值截断,试图当成 ASCII 码打印,结果自然是未知的乱码。

😵 乱码陷阱 2:printf(“%s”, &a)

如果你写成:

1
printf("%s", &a);

后果:输出乱码

深度解析:
这里涉及指针的层级(一级指针 vs 二级指针)。

  • a 指向字符串 “PROGRAM”。
  • &a 是指针变量 a 自己在内存里的地址
  • printf 去 &a 这个地址找字符串,它找到的是 a 的值(即一段二进制地址数据)。它把这些二进制数据当成文本打印出来,就是乱码。

形象比喻:

  • a 是一张藏宝图(指向宝藏)。
  • &a 是装藏宝图的保险箱
  • printf(“%s”, &a) 相当于打开保险箱,把藏宝图这张纸本身的材质当成宝藏去读,当然读不懂。

05. 一个奇怪的特例:为什么 printf(&b) 可以?

如果你尝试:

1
2
3
printf(&b);  
// 或者
printf("%s", &b);

你会发现,虽然编译器会报警告(incompatible pointer types),但程序竟然正常输出了 program!这和上面的 &a 乱码形成了鲜明对比。

原因在于数组和指针的区别:

  • 指针变量 (char *a) :是一个独立的变量,有自己的地址 (&a != a)。
  • 数组名 (char b[]):只是一个标签。在数值上,数组的首元素地址 (b) 和整个数组的起始地址 (&b) 是同一个数字

因为数值一样,printf 拿着这个地址去读,依然能读到正确的字符串。但这属于“歪打正着”,在严格的 C 语言规范中,类型是不匹配的。

总结:printf 指针避坑指南

为了避免上面的错误,请记住这张对照表:

你的目的 使用格式符 应该传入什么? 正确写法示例
打印单个字符 %c 值 (Value) *a, b[0], *(b+1)
打印字符串 %s 地址 (Address) a, b, b+1, &b[3]

记住一句话:
想打印内容,就给 %c 传值;想打印一串字,就给 %s 传地址。千万不要把“值”当“地址”传,否则程序必崩!