指针和printf的爱恨情仇
C语言指针与 printf 的爱恨情仇
C 语言的指针一直以来都是初学者的噩梦,尤其是当它和 printf、数组以及自增自减运算符混在一起的时候。
最近看到一道非常经典的指针代码阅读题,它不仅考察了基础的指针运算,还隐藏了几个关于 printf 内存机制的深坑。今天我们就借这道题,来一次彻底的“排雷”行动。
01. 原题重现
请阅读以下代码,并写出程序的输出结果:
1 |
|
如果你想先自己挑战一下,可以在这里暂停。
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 | while (putchar(*(a + 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 | while (--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 | Program |
03. 进阶:那些会导致“崩溃”的写法
这道题做对不难,但真正有价值的是:如果我们稍微改动一下 printf 里的参数,会发生什么? 很多人就是在这里栽了跟头。
💣 死亡陷阱 1:printf(b[3])
如果你写成:
1 | printf(b[3]); // 或者 printf(*b); |
后果:程序崩溃 (Crash / Segmentation Fault)
深度解析:
- b[3] 的值是字符 ‘g’ 。在计算机眼中,它就是整数 103 (ASCII码)。
- printf 的第一个参数要求是内存地址(用来读取格式字符串)。
- 你把 103 传进去,计算机会试图去访问内存地址 103。
- 低位内存地址通常是系统保留区,这是非法访问。操作系统会立刻“杀死”你的程序。
💣 死亡陷阱 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 | printf(&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 传地址。千万不要把“值”当“地址”传,否则程序必崩!
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 SingYan's Blog!