就像回到了从前——scanf是如何工作的

就像回到了从前——scanf是如何工作的

今天有个初学编程的朋友问了我一个关于scanf()的问题,大概就是对书上一段有关”%d%c”的解释不太明白,但作为 Cpp 学习者的我,老实说有一万年没有用过这个 C 函数了,虽说 printf()/scanf() 在开销上是比 C++ 的 cout/cin 要小的,但我其实连 cin 都不怎么用,刚好借着这个机会来回顾回顾 scanf() 的用法,这篇文章不仅是给朋友写的,也算是我个人的一个复习,希望不要误人子弟(ゝ∀・)

输入缓冲区

在讲解 scanf() 之前,需要明确一下输入缓冲区的概念:

程序执行的过程中,我们在键盘上按下的键会被存储到一块内存中,这个内存块就叫做输入缓冲区。

缓冲区的存在是为了解决慢速的I/O设备与高速的CPU之间速度不匹配的问题,如果没有这个缓冲区,程序直接从键盘读取输入,那每次需要读取输入时,程序就要停下来等待用户按键盘(标准输入设备)。

有了缓冲区,每当程序执行到 scanf() 语句时,就直接从缓冲区中拿数据而不必等待用户按键盘,如果缓冲区中没有数据,才会阻塞进程,等待用户按键盘(用户输入),举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdio>
int main()
{
int num0 = 0;
int num1 = 0;
char ch = 0;
char str[50];

scanf("%d %d %c %s", &num0, &num1, &ch, str);
printf("num0:%d num1:%d ch:%c str:%s\n", num0, num1, ch, str);

scanf("%d%d%c%s", &num0, &num1, &ch, str);
printf("num0:%d num1:%d ch:%c str:%s\n", num0, num1, ch, str);

scanf("%d,%d,%c,%s", &num0, &num1, &ch, str);
printf("num0:%d num1:%d ch:%c str:%s\n", num0, num1, ch, str);
}

上面这段程序有三个scanf(),当执行到第一个 scanf () 时,缓冲区内没有数据,进程阻塞等待用户输入,这时候我们可以输入如下序列:

1
0 0 a abc 1 1bbcd 2,2,c,cde回车

如果你知道 scanf() 的用法,那不难推测出如下的输出:

1
2
3
num0:0 num1:0 ch:a str:abc
num0:1 num1:1 ch:b str:bcd
num0:2 num1:2 ch:c str:cde

而这个例子正好解释了缓冲区的妙用,那就是在两种设备(这里是CPU和I/O设备)存在处理速度差异时,减少一种设备等待另一种设备的时间开销(这里是减少了程序的阻塞次数)

因为有缓冲区,我们可以一次性的输入所有的数据,而不必等待程序找我们要输入,程序也不用阻塞来等待我们进行输入;如果没有缓冲区,那么上面的程序就要阻塞三次,每次阻塞后都要等待我们的键盘输入才能继续执行

同理,有输入缓冲区就一样有输出缓冲区,如果说标准输入设备是键盘的话,那标准输出设备就是电脑屏幕了(实际情况并没有这么简单,这里只是类比),屏幕绘制图形的速度肯定是比不上CPU处理数据的速度的,所以CPU处理完数据后,直接存入输出缓冲区,屏幕再读取输出缓冲区的内容进行绘制,这样CPU就可以一直处理数据,而不用管屏幕是不是已经把之前的数据全打印完了。

不止C/C++有输入输出缓冲区,各种速度不等的设备之间都用类似缓冲区的东西,如硬盘和内存之间的缓存,GPU和屏幕之间双缓冲机制等。

scanf()读取缓冲区的规则

scanf()读取缓冲区是通过其参数中的”用户控制符“来控制的,用户控制符是一个由字符转换说明组成的字符串,在scanf()参数的首位

scanf()将根据用户控制符的内容来从缓冲区读入输入,将转换说明对应的字符放入变量中,这些变量的地址同样需要作为参数传递给 scanf() ,转换说明和变量地址的数量一定要是一一对应的,例如:

1
scanf(%d %d %d,&a,&b,%c);    //三个转换说明,后面就需要跟三个变量的地址

以下是转换说明的列表:

转换说明 对应内容
%c 把输入解释成字符
%d 把输入解释成有符号十进制整数
%e,%f,%g,%a 把输入解释成浮点数( C99标准新增了%a)
%E,%F,%G,%A 把输入解释成浮点数(C99标准新增了%A)
%i 把输入解释成浮点数(C99标准新增了%A)
%o 把输入解释成有符号八进制整数
%p 把输入解释成指针(地址)
%s 把输入解释成字符串.从第一个非空白字符开始,到下一个空白字符之前到所有字符都是输入
%u 把输入解释成无符号十进制整数
%x,%X 把输入解释成有符号十六进制整数

一般常用的也就是’%c’,’%d’,’%s’,’%f’这一些

scanf()的用户控制符在匹配时还遵循以下四个规则:

1.转换说明匹配时会忽略缓冲区开头的所有分隔符,从第一个非分隔符开始匹配

很合理吧,分隔符就是用来分隔字符,当然不匹配直接忽略了

2.转换说明的匹配结束于下一个输入的分隔符 ,或者不属于转化说明的部分

分隔符就是用来分隔字符的,所以匹配到一个分隔符,这个转换说明就匹配结束了;什么叫不属于转换说明的部分呢?比如%d匹配12asdf,匹配完12后,asdf显然不是十进制数字,所以就结束了,但%s匹配asdf12是会匹配完的,毕竟也有”asdf12”这种字符串,像%c匹配asdf,就只匹配一个a就结束了,因为它只匹配一个字符的内容

3.’%c’不受规则1的限制,也就是说’%c’会匹配分隔符

有的时候,我们也会需要输入一些分隔符到程序里,所以分隔符也需要能匹配到,这个任务就交给 %c 了,匹配一些空格、换行符之类的

4.用户控制符中的分隔符会匹配所有的任意数量的分隔符

%d %d就相当与告诉你我接受“数字空格数字”或者“数字回车数字”之类的输入,另外%d%d也是相同的效果,参照规则2

5.分隔符又称空白符,包括:换行符’\n’、空格’ ‘ 、制表符’\t’

转换说明中也会穿插一些字符用来表示“我接受这种形式的输入”,接下来我们来看一下上面那个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
缓冲区:[0 0 a abc 1 1bbcd 2,2,c,cde\n]    //注意 Linux 下的回车是 '\n' ,如果是 windows 那么回车就是 '\r\n' ,其中 '\r' 表示光标回到行首

scanf("%d %d %c %s", &num0, &num1, &ch, str);
//%d匹配缓冲区的0到num0
//空格匹配缓冲区的空格
//%d匹配缓冲区的0到num1
//空格匹配缓冲区的空格
//%c匹配缓冲区的a到ch
//空格匹配缓冲区的空格
//%s匹配缓冲区的abc到str
printf("num0:%d num1:%d ch:%c str:%s\n", num0, num1, ch, str);
//故输出num0:0 num1:0 ch:a str:abc

剩余缓冲区:[ 1 1bbcd 2,2,c,cde\n]
scanf("%d%d%c%s", &num0, &num1, &ch, str);
//%d忽略缓冲区开头的空格匹配缓冲区的1到num0
//%d忽略缓冲区开头的空格匹配缓冲区的1到num1
//%c匹配缓冲区的b到ch
//%s匹配缓冲区的bcd到str
printf("num0:%d num1:%d ch:%c str:%s\n", num0, num1, ch, str);
//故输出num0:1 num1:1 ch:b str:bcd

剩余缓冲区:[ 2,2,c,cde\n]
scanf("%d,%d,%c,%s", &num0, &num1, &ch, str);
//%d忽略缓冲区开头的空格匹配缓冲区的2到num0
//,匹配缓冲区的,
//%d匹配缓冲区的2到num1
//,匹配缓冲区的,
//%c匹配缓冲区的c到ch
//,匹配缓冲区的,
//%s匹配缓冲区的cde到str
printf("num0:%d num1:%d ch:%c str:%s\n", num0, num1, ch, str);
//故输出num0:2 num1:2 ch:c str:cde

如果你细心的话,你会发现,在上面三个scanf语句后,缓冲区还留下了一个’\n’字符

如果紧接着再写第四个scanf语句 scanf("%c",&ch);的话,那么’\n’将会被存入到ch中,如果使用 scanf("%d",&num0);就不会出现这种情况,因为%d会忽略掉分隔符

所以缓冲区一定要及时清空,比如用 getchar()将剩余的那个’\n’取出来

以上的例子,用户的输入非常友善,每个字符都恰好是scanf()能匹配到的,如果这个用户非常邪恶,将一些肮脏的东西敲进你的程序呢?

scanf()会尽力匹配能匹配的,如果匹配不上或者匹配完了,函数就会返回,返回值是匹配上的转换说明的个数,例如:

1
2
3
缓冲区:[1,1asdf]

scanf("%d,%d,%c,%s", &num0, &num1, &ch, str);

在匹配完num0 = 1,num1 = 1后,scanf就会返回,返回值为2

最后,如果你完全理解了scanf()的话,可以试着运行一下这段代码,并试着分析一下为什么^^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *method1(void)
{
static char a[4];
scanf("%s\n", a);
return a;
}

int main(void)
{
char *h = method1();
printf("%s\n", h);
return 0;
}

这是stackoverflow上的一个问题,Why does scanf ask twice for input when there's a newline at the end of the format string?

总结

1.scanf()并非直接从键盘读输入,而是从缓冲区中

2.scanf()用户控制符中的转换说明,只有%c会读入分隔符(换行符,制表符,空格),其他如%d之类的都会忽略分隔符,从第一个非分隔符开始匹配

3.scanf()用户控制符中的空白符会匹配所有任意数量的空白符

最后祝我的这位朋友能在代码世界里找到属于自己的乐趣~

就像回到了从前——scanf是如何工作的

https://fly.meow-2.com/post/records/scanf.html

作者

Meow-2

发布于

2022-02-20

更新于

2022-06-24


评论