Appearance
机试读入:字符串与混合格式
场景引入
题目要求先读一个整数 n,再读一个字符 c。你老老实实写了两行 scanf,结果 c 里存的既不是字母也不是数字,程序后面的分支全走偏了。
问题出在换行符上。你按下回车时,那个 \n 也进了输入缓冲区,scanf("%d") 读走数字后把它留在了原地,接着 scanf("%c") 就把它当成了要读的字符。
数字、字符、整行三种读入方式对空白字符的处理规则各不相同,混用时的边界就是机试里最容易踩空的地方。这一篇把规则说清楚。
三种读入方式,三种空白规则
先建立一张对照,后面所有的坑都是从这里推出来的。
| 读法 | 开头的空白 | 什么时候停 | 换行符去哪了 |
|---|---|---|---|
scanf("%d") / cin >> x | 自动跳过 | 遇到空白 | 留在缓冲区 |
scanf("%c") | 不跳过 | 读满一个字符 | 可能就是它读到的 |
scanf("%s") / cin >> s | 自动跳过 | 遇到空白 | 留在缓冲区 |
fgets / getline | 不跳过 | 遇到换行 | 被读走 |
三种方式里,只有 %c 和整行读入会看见空白字符。串行问题就出在「前一个读法把换行留下了,后一个读法看得见它」的交界处。
一、数字后面读字符:加一个前导空格
c
int n;
char c;
scanf("%d", &n);
scanf(" %c", &c); // 格式串里的空格:跳过任意多个空白scanf 格式串里的一个空格,含义是「跳过任意数量的空白字符,包括空格、制表符和换行」。加上它,之前遗留的 \n 就被吃掉了。
这个写法的代价接近于零,所以读单个字符时默认写 " %c",不用每次去推有没有遗留换行。
C++ 侧对应的问题出现在 cin >> n 之后用 cin.get(c),解决办法一样是先跳空白,或者直接用 cin >> c(>> 读 char 时会跳过空白)。
二、读带空格的一整行
scanf("%s") 和 cin >> s 遇到空格就停,读不了 hello world 这样的一行。
C 用 fgets:
c
char line[1005];
fgets(line, sizeof(line), stdin);
line[strcspn(line, "\r\n")] = '\0'; // 去掉行尾的换行两点说明:
fgets会把换行符一起读进来。strcspn(line, "\r\n")返回第一个\r或\n的下标,把那里改成\0就截断了。同时写\r和\n是为了兼容 Windows 风格的换行,测试数据用哪种都不影响。gets从 C11 起已经被移除,别再用。它无法限制读入长度,缓冲区多大都可能被写穿。
C++ 用 getline:
cpp
string line;
getline(cin, line);三、>> 和 getline 混用:先把换行吞掉
这是 C++ 侧容易撞上的串行。
cpp
int n;
cin >> n;
string line;
getline(cin, line); // line 是空的cin >> n 把数字读走,换行留在缓冲区;getline 一上来就撞见换行,于是认为「这一行到此为止」,返回一个空串。
两种修法:
cpp
cin >> n;
cin.ignore(); // 丢掉一个字符,多数情况够用
getline(cin, line);cpp
#include <limits>
cin >> n;
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 丢到行尾,更稳
getline(cin, line);第二种能处理「数字后面还跟着若干空格才换行」的情况。两种都行,第二种不用想边界。
四、字符矩阵:一行一个 %s
迷宫、棋盘、图形类的题,输入常是这样:
5 5
#####
#...#
#.#.#
#...#
#####矩阵里没有空格,所以可以一行当一个字符串读,%s 会自动跳过前面遗留的换行:
c
int n, m;
char g[105][105];
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i++) {
scanf("%s", g[i]); // 第 i 行整行读进 g[i]
}
// 访问用 g[i][j],天然是二维的这个写法把「跳过换行」的事情交给了 %s,省掉了所有 getchar 清缓冲的操作。数组第二维要开到 m + 1 以上,给结尾的 \0 留位置。
如果矩阵里有空格(比如用空格表示空地),%s 就会在空格处断掉,这时候改用整行读:
c
for (int i = 0; i < n; i++) {
fgets(g[i], sizeof(g[i]), stdin);
g[i][strcspn(g[i], "\r\n")] = '\0';
}注意这里有个交界:scanf("%d %d", &n, &m) 之后缓冲区里还留着换行,第一次 fgets 会读到一个空行。在循环前先补一次 getchar() 把它吃掉,或者干脆用 fgets 读第一行再 sscanf 解析出 n 和 m,全程只用一种读法。
全程只用一种读法是绕开这类问题最省心的策略:要么全 scanf,要么全整行读加 sscanf。混用才需要在每个交界处想清楚换行在谁手里。
五、跳过不需要的字段
题面给了五列数据,你只关心第二列和第五列。不用把它们都读进变量,%* 表示「按这个格式读,但不赋值」:
c
scanf("%*d %d %*d %*d %d", &b, &e);C++ 里没有直接对应的写法,读进临时变量丢掉即可。
延伸阅读
- 机试读入:多组数据与不定长输入 —— EOF、T 组、哨兵三种骨架
- 机试输出:格式与精度 —— 输出对了却判错的几种原因
- 机试环境:IO 速度与内存边界 —— 数组开多大、什么时候会超时