C 语言第 4 章:顺序结构——printf 与 scanf 的格式控制与典型错误
顺序结构是最简单的一种结构:语句从上到下依次执行,没有任何跳转。真正需要花时间的不是"顺序"这个概念,而是它的两个入口——printf 怎么把数据排成想要的版式,scanf 怎么把键盘输入正确切分并送进变量。这两件事看着琐碎,却是初学者 bug 最密集的地方:忘了取地址符、%c 读到了回车、5/9 算成了 0、%lf 与 %f 用反。本章的目标是把格式串当成一门小语言来学:一个 % 号后面,依次是修饰符、宽度、精度、长度、转换字符,读懂了这五段,任何格式串都能拆开。
一、顺序结构与三类语句
C 程序由函数组成,main 里的语句按书写顺序执行。构成顺序结构的基本语句有四类:
- 表达式语句:
a = b + c;、i++;,任何表达式加分号。 - 函数调用语句:
printf("hi");。 - 复合语句:用
{ }把多条语句包成一个逻辑整体,在语法上等价于一条语句。 - 空语句:单独一个分号,什么都不做,常用在循环体需要留空的地方。
顺序结构没有分支也没有回路,所以它的正确性完全取决于数据流:每个变量在使用前是否已经拿到了正确的值。这也是本章反复强调输入输出的原因。
二、printf:格式字符与修饰符
函数原型是 int printf(const char *format, ...),返回值是实际输出的字符数,出错时返回负数。格式串里的普通字符原样输出,% 开头的叫转换说明,完整的语法是:
%[标志][宽度][.精度][长度]转换字符
转换字符(最常用的几个):
| 字符 | 含义 | 示例(值) | 输出 |
|---|---|---|---|
| %d / %i | 十进制有符号整数 | 42 | 42 |
| %u | 十进制无符号整数 | 42u | 42 |
| %o | 八进制(无前缀) | 255 | 377 |
| %x / %X | 十六进制(小写/大写) | 255 | ff / FF |
| %c | 单个字符 | 'A' | A |
| %s | 字符串 | "abc" | abc |
| %f | 定点小数,默认 6 位 | 3.14159 | 3.141590 |
| %e / %E | 科学计数法 | 1234.5 | 1.234500e+03 |
| %g / %G | 自动在 %f 与 %e 间选较短者 | 0.0001 | 0.0001 |
| %% | 输出一个百分号 | — | % |
标志控制对齐与填充:- 左对齐(默认右对齐)、+ 强制显示正号、空格 正数前留空格、0 用 0 填充空位、# 加进制前缀(%#x 输出 0xff,%#o 输出 0377)。
宽度是最小字段宽度,精度对浮点是小数位数、对字符串是最多输出的字符数。几个对照记住就够:
printf("[%5d]", 42); /* [ 42] 右对齐补空格 */
printf("[%-5d]", 42); /* [42 ] 左对齐 */
printf("[%05d]", 42); /* [00042] 补零 */
printf("[%8.3f]", 3.14159); /* [ 3.142] 宽 8、3 位小数 */
printf("[%.2s]", "abcdef"); /* [ab] 只取前 2 个字符 */
注意宽度和精度都会四舍五入(%.3f 把 3.14159 变成 3.142),而宽度不足时不会截断数据,会原样输出更长的内容。
一条容易踩的坑:printf("%d %d", i++, i++) 这类写法里,参数求值顺序标准未规定,不同编译器结果可能不同,属于未定义行为,不要在格式串里塞有副作用的表达式。
三、scanf:格式串与典型错误
原型 int scanf(const char *format, ...),返回值是成功读入并赋值的项数;一个都没读到时返回 0,遇到输入结束返回 EOF(通常是 −1)。
scanf 的行为和 printf 不对称,必须单独记:
- 变量前必须加
&。scanf 需要知道往哪写,传的是地址。scanf("%d", a)把 a 的值当成了地址,程序很可能直接崩溃。数组名与字符数组例外(它们本身就是地址)。 - 格式串里的非格式字符要原样输入。
scanf("%d,%d", &a, &b)要求用户输入3,4,逗号必须敲;写成scanf("%d %d", ...)则任意空白(空格、制表、回车)都能分隔。 - 不要在格式串末尾加
\n。scanf("%d\n", &n)中的\n表示"跳过所有空白",程序会一直等到用户输入一个非空白字符才返回,表现为"按了回车没反应"。 %d会跳过前导空白,%c不会。这是 %c 最经典的坑:先用scanf("%d", &n)输入数字并回车,缓冲区里留下'\n',紧接着的scanf("%c", &ch)会把这个回车读走,看起来像"跳过了输入"。两种解法:写成scanf(" %c", &ch)(%c 前加一个空格,表示跳过空白),或先getchar();吃掉回车。- double 必须用
%lf。因为 scanf 收到的是指针,不存在 printf 那种"float 自动提升为 double"的机制,用 %f 读 double 会把 8 字节按 4 字节解释,数据错乱。(printf 里 %f 与 %lf 在 C99 起等价。) %s不检查长度。输入超过数组容量就会越界,应加宽度限制:scanf("%9s", buf),留一位给'\0'。
养成检查返回值的习惯,是让程序"抗摔"的第一步:
if (scanf("%d", &n) != 1) {
printf("输入不是合法整数\n");
return 1;
}
四、字符型变量与字符编码
char 在内存里就是一个字节的整数,存的是字符的编码值。C 保证所有字符编码中数字 0~9 是连续的、字母 a~z 与 A~Z 各自连续,所以可以直接做算术:
| 字符 | ASCII 码 | 常用运算 |
|---|---|---|
| '0' | 48 | ch - '0' 得数值 |
| 'A' | 65 | ch + 32 得小写 |
| 'a' | 97 | ch - 32 得大写 |
| 空格 | 32 | —— |
| '\0' | 0 | 字符串结束标志 |
'0' 与 0 差 48,这是"字符转数字"和"数字转字符"的唯一秘密。判断字符类别时可以用 <ctype.h>:isdigit、isalpha、isspace、toupper、tolower,比手写区间判断更清楚,也不会在非 ASCII 编码下出错。
转义字符用来表示不可打印或特殊的字符:\n 换行、\t 制表、\\ 反斜杠、\" 双引号、\' 单引号、\0 空字符、\r 回车、\b 退格、\a 响铃,以及 \ddd(1~3 位八进制)和 \xhh(十六进制)两种按码值写法。
五、温度换算实例
华氏转摄氏的公式是 C = 5/9 × (F − 32)。
#include <stdio.h>
int main(void)
{
double f, c;
printf("请输入华氏温度:");
if (scanf("%lf", &f) != 1) { printf("输入错误\n"); return 1; }
c = 5.0 / 9 * (f - 32); /* 不能写 5/9 */
printf("摄氏温度 = %.2f\n", c);
return 0;
}
这段代码藏着本章几乎全部的考点:%lf 读 double、检查返回值、5.0 / 9 避免整数除法、%.2f 控制两位小数。输入 100 时输出 37.78,输入 32 时输出 0.00,输入 −40 时输出 −40.00——最后这个巧合可以用来自测公式有没有写错。
把 5.0 / 9 写成 5 / 9 是本章最高频的错误:两个 int 相除,结果取整得 0,整个表达式恒为 0。同类问题还有 1 / 2、a / b 求平均。判断标准很简单:只要这一步的数学含义是"小数",就至少让一个操作数变成浮点。
易错点
- scanf 忘写
&。编译能过(最多一个警告),运行崩溃或写坏别的变量。 - 在 scanf 格式串末尾加
\n,导致程序卡住等待输入。 %c读到上一次输入残留的回车。用" %c"或getchar()清掉。5 / 9整数除法得 0,温度换算全错。- scanf 用
%f读 double。printf 里两者等价,scanf 里不等价。 - 用
%d输出 float,或用%f输出 int。类型不匹配是未定义行为,输出常是垃圾值。 %s没有宽度限制,输入过长时数组越界。
这一章在整门课里的位置
本章是第 3 章流程图中"平行四边形输入输出框"的代码落地,也是第 2 章数据类型的第一次实战检验——格式符与类型必须一一对应。它同时是第 5、6 章的前置:后面所有分支与循环程序,都要靠 scanf 拿数据、靠 printf 出结果。可以说第 5 章的选择结构、第 6 章的循环结构,本质上都是在本章这套输入输出骨架外面套上控制流。
相关课程
这篇笔记涉及的主题,我录过对应的视频课程:
- C 语言程序设计 15 讲