我在大学里学的第一门语言就是 C 语言,几年里用 C 语言在 OJ 上交了成百上千发题目,却连最基本的 scanf 函数的用法都不了解。碰到一些字符串和数字混杂输入的题目,基本就只能靠猜来输入……这次来具体的看一下 scanf 函数的用法。
scanf 函数的介绍信息转自 菜鸟教程
C 库函数
int scanf(const char *format, ...)
从标准输入
stdin
读取格式化输入。
下面是
scanf()
函数的声明。
int scanf(const char *format, ...)
format – 这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符 和 format 说明符。
format 说明符形式为 [=%[*][width][modifiers]type=],具体讲解如下:
| 参数 | 描述 |
| – | – |
| * | 这是一个可选的星号,表示数据是从流 stream 中读取的,但是可以被忽视,即它不存储在对应的参数中。 |
| width | 这指定了在当前读取操作中读取的最大字符数。 |
| modifiers | 为对应的附加参数所指向的数据指定一个不同于整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小: h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x) l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g) L :长双精度型(针对 e、f 和 g) |
| type | 一个字符,指定了要被读取的数据类型以及数据读取方式。具体参见下一个表格。 |
scanf 类型说明符:
| 类型 | 合格的输入 | 参数的类型 |
| – | – | – |
| c | 单个字符:读取下一个字符。如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。| char * |
| d | 十进制整数:数字前面的 + 或 - 号是可选的。 | int * |
| e,E,f,g,G | 浮点数:包含了一个小数点、一个可选的前置符号 + 或 -、一个可选的后置字符 e 或 E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4 | float * |
| o | 八进制整数。 | int * |
| s | 字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。 | char * |
| u | 无符号的十进制整数。 | unsigned int * |
| x,X | 十六进制整数。 | int * |
附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。
如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。
说实话,菜鸟教程的这个文档也不是很容易看,因为它基本是直接翻译的(原文档)……有些地方翻译的不是很通顺。要想了解具体的使用方法,还是要实际的敲一下。
在 OJ 中的使用
OJ 评测的原理
关于 OJ 评测的具体原理,我之后写一篇博客来说明一下,这里只说一下关于评测时的输入输出问题:
在 OJ 评测时,会通过流重定向的方式,用输入文件来替换待评测程序的标准输入,通过对比程序标准输出和输出文件来确定运行结果。
scanf 读取数据原理
文档中说明了,scanf 是从标准输入流 stream 中读取数据的。在评测开始的时候,(可以认为)一次性把所有输入数据扔到标准输入流的缓冲池中,此时我们可以把整个标准输入流当成一个输入数据的字符串,读取数据就是根据某种规则从这个字符串中解析出自己想要的数据。
在读取的时候,会有一个指针指向输入数据字符串中要读取的下一个字符,我们要做的就是正确的读取所有数据。
scanf 函数在读取的时候,每遇到一个类型说明符,都会尝试去匹配指针所指字符,当读取完所有符合规则的字符之后,将解析后的数据存在用户给定的变量中。
那怎样才能知道已经读取完符合规则的字符了呢? scanf 用以下条件来进行判断:
读取达到宽度限制(%3d
用于读取 3 位数字)
遇到(相对当前类型说明符的)非法输入(比如 %d
读取到了一个 'a'
)
scanf
遇到以上条件时就会停止当前的匹配,开始下一个类型说明符的匹配。
char ch;
char str[100];
scanf("%c%s", &ch, str);
printf("ch: %c\nstr: %s", ch, str);
$ HelloWorld!
> ch: H
str: elloWorld!
在程序开始运行的时候,标准输入流中的字符串为 "HelloWorld!"
,scanf
首先尝试匹配 '%c'
:
HelloWorld!
'%c'
匹配了指针指向的字符 'H'
,指针后移一位,长度达到限制, '%c'
停止匹配。
HelloWorld!
此时 scanf
尝试匹配 '%s'
, '%s'
的匹配规则为:匹配连续字符,直到遇到空白符(空格、换行或者格式制表符等)。 '%s'
成功匹配该行剩下的所有字符 "elloWorld!"
,并将其存到字符数组 str
中。
另一个例子:
char ch;
char str1[100], str2[100];
scanf("%c%s%s", &ch, str1, str2);
printf("ch: %c\nstr1: %s\nstr2: %s\n", ch, str1, str2);
$ Hello World!
> ch: H
str1: ello
str2: World!
当 ch
和 str1
读取结束的时候,指针的位置如下:
Hello World!
那为什么 str2
读取到的却是 "World!"
呢?原因很简单,因为 '%s'
类型说明符在匹配的时候会忽略字符串开头所有的空白符,指针会一直后移直到指向 'W'
的时候才正式开始匹配。
同样的程序,我们换一组输入:
$ Hello
World!
> ch: H
str1: ello
str2: World!
原理同上,在字符串中,换行和其他空白符没有本质上的区别,读取的字符串实际上是: "Hello\nWorld!"
,对于 '\n'
的处理和对其他空白符的处理是一样的,因此会出现和上面的程序一样的结果。
但是如果我们把上面程序的格式说明符交换一下位置的话就会出现问题:
char ch;
char str1[100], str2[100];
scanf("%s%c%s", str1, &ch, str2);
printf("ch: %c\nstr1: %s\nstr2: %s\n", ch, str1, str2);
$ Hello
World!
str1: Hello
str2: World!
ch
读取到了个空?并不是, 在 str1
读取结束的时候,指针刚好指到第一行的最后: '\n'
,'%c'
在读取的时候可不管三七二十一,只要它上场,就会读取当前指针所指的那个字符,因此, '%c'
实际上是把第一行末尾的 '\n'
给读取走了。因为 '\n'
在控制台下会表现为一个换行,因此会给人一种什么也没有读取到的感觉。
[空格] 的使用
在使用 OJ 的过程中,绝大部分人(我认识的所有人)都曾经被数据中的不可见字符给坑过,其中,每行末尾的换行符可以说卡死过无数人。其实,只要明白了换行符也只不过是一个空白符,稍加注意,就可以避免这些问题。但是,有时候问题并非出现在我们身上,也有可能是数据的锅(POJ 某题曾经卡过我很久,最后发现是他数据中有多余的空格),那这时候我们就要尽量的提高我们代码的鲁棒性,在数据有一些问题的情况下,依然能够尽量正确的读取数据。
那么,在哪里才能买到……啊不是,我们要怎么做呢?
其实,仔细看一下文档,就会发现一个神奇的格式说明符,就是 [空格] 。在菜鸟教程翻译的文档中,只是提到了有空格这个说明符,对其作用却并没有说明。查看英文文档,我发现,[空格] 这个格式说明符可以在很大程度上避免因不可见字符而读取到错误的结果。
[空格] 的作用:读取 0 个、 1 个或多个连续的空白字符。
从描述上就可以看出,如果不是主动想要读取空白字符,那么在原有的格式说明符两侧加 [空格] 是不会产生任何问题的,而且还可以完美避免因空白字符而导致的锅。
char ch;
char str1[100], str2[100];
scanf("%s %c %s", str1, &ch, str2);
printf("ch: %c\nstr1: %s\nstr2: %s\n", ch, str1, str2);
$ Hello
World!
> ch: W
str1: Hello
str2: orld!
加上 [空格] 之后,产生了期望的结果。
(其他空白符有同样的效果,但是空白符最顺手)
多看英文文档比啥都好
在格式说明符两侧加空格能有效增强 OJ 体验
以上就是我对 scanf 函数的理解,如果有什么不对的或者纰漏,还请斧正,如果能对你有些许的帮助,也希望你能给我留个言~