智能程序设计(C语言)


格式化输入输出

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • 速查: printf 与转换说明 → 任务 1: 打印对齐的成绩表

  • 速查: 转义序列 → 任务 2: 用转义序列打印三行

  • 速查: scanf 与输入缓冲区 → 任务 3: 遗留换行的坑

  • 格式串中的普通字符 → 任务 4: 一次读入混合类型

  • printf/scanf 混淆、getchar/putchar → 任务 5: 大小写转换

  • 综合案例: 分数相加 · 课堂小测


printf: 格式串 = 普通字符 + 转换说明

先看输出: printf 把值填进格式串的空位:

printf 必须提供格式字符串, 后跟要插入到串中的值:

printf(格式串, 表达式1, 表达式2, ...);

格式串 = 普通字符 (原样打印) + 转换说明 (以 % 开头, 用对应的值替换)

格式字符串 = 普通字符 + 占位符 "i = " %d ", j = " %d "\n" i = 10 j = 20 按顺序一一对应, 值填进空位 输出: i = 10, j = 20↵

宽度 / 精度 / 左对齐, 一个例子:

printf("|%-8s|%10.1f|\n", "name", 85.5);

输出: |name••••|••••••85.5|

%-8s: 宽 8、左对齐

%10.1f: 宽 10、1 位小数、右对齐

编译器不检查转换说明与参数的数量、类型: 多了少了、或用 %d 打印 double (%d 从整数通道取到的是无关值), 输出都无意义.


转换说明速查表

刚才的 %-8s、%10.1f 只是其中两例, 完整清单如下:

写法: %[标志][宽度][.精度]X, 如 %-8s (宽 8、左对齐)、%10.1f (宽 10、1 位小数); 标志、宽度、精度都可省略 (省略精度时句点一并去掉).

转换说明 用于 示例
%d int 42
%f float / double (printf) 3.140000
%lf double (仅 scanf) 读入 1.75
%c char A
%s 字符串 (char []) Hi
%o 八进制整数 12 (由 10 得到)
%x 十六进制整数 ff (由 255 得到)
%e 指数格式 1.234500e+03
%% 百分号本身 100%

printf 与 scanf 的 double 差异: printf 的 %f 可打印 float/double (float 自动提升); scanf 读 double 必须用 %lf, 读 float 才用 %f.

宽度不够自动加宽: 宽度 m 是最小字段宽度, 值更宽时自动加宽、不截断; - 标志表示左对齐 (%4d→•123, %-4d→123•).

精度 p 随 X 变: %d 的 p 是最少位数 (不足补 0, 默认 1); %f/%e 的 p 是小数位数 (默认 6, p 为 0 不显示小数点); %g 按大小自动选 %e/%f, p 是最大有效位数并去掉尾随零.


任务 1: 打印对齐的成绩表

任务: 打印下面的表, 三列对齐 (科目左对齐, 分数右对齐保留 1 位小数, 等级右对齐)

科目        分数    等级
语文        85.5       A
数学        92.0       A
英语        78.2       B

提示: 数据列用 %10.1f (总宽 10, 1 位小数) 与 %8c; 中文表头不要用 %8s——%s 的宽度按字节算, 一个汉字 3 字节却只占 2 列, 直接用固定空格对齐表头

⚡ 在 VS Code 中打开 task-table.c (骨架)

任务 1: 参考代码与要点

#include <stdio.h>

int main(void)
{
  printf("科目        分数    等级\n");   /* 中文表头用固定空格 */
  printf("%-8s%10.1f%8c\n", "语文", 85.5, 'A');
  printf("%-8s%10.1f%8c\n", "数学", 92.0, 'A');
  printf("%-8s%10.1f%8c\n", "英语", 78.2, 'B');
  return 0;
}
  • 格式: %[标志][宽度][.精度]转换——%-8s 左对齐, %10.1f 右对齐保留 1 位

  • 中文表头不要用 %8s: %s 的宽度按字节算, 一个汉字 3 字节却只占 2 列; 表头用固定空格, 数据列仍用 %10.1f/%8c

  • 列对齐的前提: 每一行同一个位置用相同的显示宽度

  • 宽度不够会自动加宽 (不会截断数字); 字符串超宽则照原样输出

  • 表格类输出是 printf 宽度修饰最常见的用途

对齐靠宽度; 字符串里还有看不见的字符——转义序列:


转义序列: 反斜杠引出特殊字符

转义序列 让字符串包含非打印 (控制) 字符或具有特殊含义的字符; 反斜杠 \ 是转义引子——它和后面的一个字符合成"一个字符" (如 \n 是一个字符, 不是 \ 和 n 两个).

转义序列 含义 示例
\n 换行符 光标移到下一行开头
\t 水平制表符 printf("A\tB") 两列对齐
\" 双引号 " printf("\"Hi\"") → "Hi"
\\ 一个反斜杠 \ printf("C:\\temp") → C:\temp
\a 警报 (铃声) 响铃
\b 回退符 光标退回一格
  • 一个字符串可以包含任意数量的转义序列: printf("Item\tUnit\tPurchase\n\tPrice\tDate\n");

  • 要打印双引号必须写 \"; 要打印单个 \ 必须写 \\, 否则它会和后一个字符组成别的转义序列.


任务 2: 用转义序列打印三行

任务: 打印下面的内容 (注意引号、反斜杠、制表符):

姓名	分数
"张三"	92
路径: C:\temp

提示: 换行 \n; 制表 \t (列间对齐); 双引号写 \"; 反斜杠本身写 \\

⚡ 在 VS Code 中打开 task-escape.c (骨架)

任务 2: 参考代码与要点

#include <stdio.h>

int main(void)
{
  printf("姓名\t分数\n");
  printf("\"张三\"\t92\n");
  printf("路径: C:\\temp\n");
  return 0;
}
  • \n 换行, \t 制表 (两列对齐比空格稳)

  • \" 才能在字符串里打印出双引号; \\ 才能打印出一个反斜杠

  • 反斜杠是转义引子: 它和后面的字符合成"一个字符", 如 \n 是一个字符

  • 反斜杠写单个会引发警告/意外转义 (如 \t 变制表符)

输出会了, 反过来看输入——scanf 怎么读?


scanf(一): 按格式串顺序匹配, 别忘 &

要点
  • scanf 按格式串从左到右依次匹配; 格式串通常只含转换说明

  • 通常每个变量前要加 & (取地址), 告诉 scanf 把值存到哪里

  • 返回值 = 成功读入的项数, 可用于检查输入是否完整

  • 读 double 必须用 %lf (读 float 才用 %f)

代码

一次读入两个 int、两个 double:

int i, j;
double x, y;

scanf("%d%d%lf%lf", &i, &j, &x, &y);

样本输入: 1 -20 .3 -4.0e3

i=1  j=-20  x=0.3  y=-4000.0

scanf 不检查转换说明的数量和类型是否与变量匹配; 顺序错了, 读到的是无意义的值.

一次读多个值已经清楚; 连续多次 scanf 时, 数据从哪来?


scanf(二): 输入先进缓冲区, 再逐个消费

要点
  • 输入先进入缓冲区; scanf 每次从上次读到的位置接着读, 多次调用共享同一条输入流

  • 对每个转换说明, scanf 在缓冲区里找合适的数据, 必要时跳过空白 (空格 / 制表 / 换行)

  • 读整数时先找数字或正负号, 继续读数字, 直到遇到非数字; 这个字符被放回, 留给下一次读取

  • 缓冲区没有数据时, scanf 等待用户输入——交互式程序"卡住"往往是在等你敲键盘; 数据不匹配时则立即返回

代码
同一个 scanf 调用, 数据从缓冲区来:
int i, j; double x, y;
scanf("%d%d%lf%lf", &i, &j, &x, &y);

输入的数字可以在同一行, 也可以分布在多行:

••1
-20•••.3
•••-4.0e3

1-20.3-4.0e3 的处理:

%d  → i=1, j=-20      (放回 - 和 .)
%lf → x=0.3, y=-4000.0 (放回 - 和换行)

%d / %f 读完后停在第一个不能构成数字的字符; 换行符因此常留在缓冲区里——下一页的 %c 会读到它.


遗留换行: %c 会读到上一次的回车

代码
先看两条 scanf 如何依次读取:
int a, b;
printf("请输入两个整数: ");
scanf("%d", &a);          /* 读走 10, 剩 " 20↵" */
printf("a = %d\n", a);
scanf("%d", &b);          /* 跳过空格, 接着读 20 */
printf("b = %d\n", b);

用户只输入一行 10 20↵:

请输入两个整数: 10 20↵
a = 10
b = 20
要点
  • 输入先全部进入缓冲区, 两条 scanf 依次"消费": 第一个拿走 10, 第二个接着拿走 20

  • scanf 读走的数据不会再次被读到; 夹在中间的 printf 只打印已读到的值

  • 若第一次只输入 10↵ 就回车, 第二个 scanf 在缓冲区找不到整数, 程序卡住等待继续输入

  • %d / %f 跳过空白, %c 不跳过——它读到的可能就是上一次留下的换行

int n; char ch;
scanf("%d", &n);
scanf("%c", &ch);     /* ch 读到 '\n', 不是新字符! */
scanf(" %c", &ch);    /* 空格: 跳过所有空白 */

修复: 在 %c 前加一个空格 (" %c"), 格式串里的空白会跳过所有空白字符.

scanf 与 getchar() 混用同理: scanf 结束后换行符还留在输入流里, 会被后面的 getchar() 当"第一个字符"吃掉; 先 getchar() 把换行吃掉再读, 也常用于读整行之前.


任务 3: 遗留换行的坑

任务: 先读年龄 (整数), 再读性别 (字符)——结果性别读到了换行, 打印为空。修复它。

样例:

输入: 19 M
期望: age=19 gender=M

提示: scanf("%d") 读完后缓冲区还剩一个 \n; 试在 %c 前加一个空格

⚡ 在 VS Code 中打开 task-newline.c (骨架)

任务 3: 参考代码与要点

#include <stdio.h>

int main(void)
{
  int age;
  char gender;

  scanf("%d", &age);
  scanf(" %c", &gender);   /* 空格: 跳过空白字符 */

  printf("age=%d gender=%c\n", age, gender);
  return 0;
}
  • %d 读完数字就停, 换行还留在输入缓冲区里

  • %c 不跳过空白, 直接把那个 \n 读走——所以"看起来什么都没读到"

  • 在 %c 前加空格 (" %c"): 格式串里的空白会跳过所有空白字符

  • 同类坑: 用 getchar() 手动吃掉换行, 也常用于读整行之前

换行的坑搞清楚了; 格式串里的普通字符也有讲究:


格式串的普通字符: 必须逐字匹配

  • 当在格式字符串中遇到一个或多个空白字符时, scanf 从输入中读取空白字符, 直到它遇到一个非空白字符(即*放回*)

  • 当它在格式字符串中遇到非空白字符时, scanf 将其与下一个输入字符进行比较

  • 如果匹配, 则 scanf 丢弃输入字符并继续处理格式字符串

  • 若不匹配, 则 scanf 会将有问题的字符放回输入中, 然后中止

例子:

  • 如果格式字符串是 "%d/%d" 并且输入是 •5/•96, scanf 成功

  • 如果输入 •5•/•96, 则 scanf 失败, 因为格式字符串中的 / 与输入中的空格不匹配

要在第一个数字后允许空格, 需改用格式字符串 "%d /%d"


任务 4: 一次读入混合类型

任务: 一行输入年龄、身高、性别, 读入 int、double、char 并回显:

输入 (年龄 身高 性别): 19 1.75 M
age=19 height=1.75 gender=M

提示: 三个转换说明按顺序匹配; %c 前留空格 (" %c") 跳过空白; 别忘 &

⚡ 在 VS Code 中打开 task-mixed-scanf.c (骨架)

任务 4: 参考代码与要点

#include <stdio.h>

int main(void)
{
  int age;
  double height;
  char gender;

  printf("输入 (年龄 身高 性别): ");
  scanf("%d %lf %c", &age, &height, &gender);

  printf("age=%d height=%.2f gender=%c\n", age, height, gender);
  return 0;
}
  • 一条 scanf 可以按顺序读多个值: %d → int, %lf → double, %c → char

  • 输入里的空格会被格式串中的空白跳过; %c 不跳过, 所以写 " %c"

  • scanf 的返回值是成功读入的项数 (这里应为 3), 可用于检查输入

  • 变量与转换说明一一对应, 顺序错了读到的是垃圾值

混合读入没问题; 但 printf 和 scanf 长得像, 容易混:


printf 与 scanf: 三个易混点

scanf 和 printf 的调用看起来相似, 但两者之间存在显著差异

  • 一个常见的错误是在printf调用中将&放在变量前面:

printf("%d %d\n", &i, &j);  /*** WRONG ***/
  • 不要错误地假设 scanf 格式字符串应该类似于 printf:

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

若输入中逗号的位置实际是空格, 则scanf将终止而不读取j的值

  • 多数情况下, 一定不要在 scanf 格式化字符串的末尾放置换行符, 否则会导致交互式程序 挂起 直到用户输入下一个非空格字符

除了格式化读写, 还可以逐个字符读写:

getchar / putchar: 每次读写一个字符

getchar: 读取单个字符

int ch;              /* 注意: 返回值是 int, 不是 char! */
ch = getchar();      /* 从键盘读取一个字符, 存入 ch */

为什么用 int? 因为 getchar 还可能返回 EOF, char 装不下

putchar: 输出单个字符

putchar(ch);      /* 在屏幕上显示字符 ch */
putchar('\n');    /* 输出换行符 */

getchar/putchar 读写字符的效率高于 scanf/printf, 在字符处理类程序(如统计输入字符个数)中常用


任务 5: getchar / putchar 大小写转换

任务: 输入一个小写字母, 用 getchar/putchar 输出对应大写字母+换行:

输入: b
输出: B

提示: getchar() 返回字符的编码 (int); 小写比大写大 32: c - 'a' + 'A'

⚡ 在 VS Code 中打开 task-putchar.c (骨架)

任务 5: 参考代码与要点

#include <stdio.h>

int main(void)
{
  int c = getchar();
  putchar(c - 'a' + 'A');
  putchar('\n');
  return 0;
}
  • getchar() 读一个字符并返回编码 (int), 适合逐字符处理

  • putchar(c) 输出一个字符; 用 putchar('\n') 换行

  • 字符本质是整数: c - 'a' + 'A' 完成小写→大写 (差 32)

  • 注意: 输入 "b 回车" 时, 回车还留在缓冲区——前面"遗留换行"的坑同理


综合案例: 分数相加

格式串里的 / 需要原样匹配; 用它读两个分数并相加:

#include <stdio.h>
 
int main(void)
{
  int num1, denom1, num2, denom2, result_num, result_denom;

  printf("Enter first fraction: ");
  scanf("%d/%d", &num1, &denom1);
 
  printf("Enter second fraction: ");
  scanf("%d/%d", &num2, &denom2);
 
  result_num = num1 * denom2 + num2 *denom1;
  result_denom = denom1 * denom2;
  printf("The sum is %d/%d\n",result_num, result_denom);

  return 0;
}

示例程序输出:

Enter first fraction: 5/6 Enter second fraction: 3/4 The sum is 38/24


课堂小测

  1. printf("%-8.3f", x) 中 -、8、.3 分别表示什么?
  2. 要打印出字符 " 和 \,转义序列分别怎么写?
  3. scanf("%d/%d", &a, &b) 要求用户怎么输入?
  4. getchar() 返回值的类型是什么?为什么它适合读取字符?



未完待续