今天有个初学编程的朋友问了我一个关于 scanf() 的问题,大概就是对书上一段有关"%d%c"的解释不太明白,但作为 Cpp 学习者的我,老实说有一万年没有用过这个 C 函数了,虽说 printf()/scanf() 在开销上是比 C++ 的 cout/cin 要小的,但我其实连 cin 都不怎么用,刚好借着这个机会来回顾回顾 scanf() 的用法,这篇文章不仅是给朋友写的,也算是我个人的一个复习,希望不要误人子弟 (ゝ ∀・)
输入缓冲区
在讲解 scanf() 之前,需要明确一下输入缓冲区的概念:
程序执行的过程中,我们在键盘上按下的键会被存储到一块内存中,这个内存块就叫做输入缓冲区。
缓冲区的存在是为了解决慢速的 I/O 设备与高速的 CPU 之间速度不匹配的问题,如果没有这个缓冲区,程序直接从键盘读取输入,那每次需要读取输入时,程序就要停下来等待用户按键盘 (标准输入设备)。
有了缓冲区,每当程序执行到 scanf() 语句时,就直接从缓冲区中拿数据而不必等待用户按键盘,如果缓冲区中没有数据,才会阻塞进程,等待用户按键盘 (用户输入),举个例子:
#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 () 时,缓冲区内没有数据,进程阻塞等待用户输入,这时候我们可以输入如下序列:
0 0 a abc 1 1bbcd 2,2,c,cde回车
如果你知道 scanf() 的用法,那不难推测出如下的输出:
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() ,转换说明和变量地址的数量一定要是一一对应的,例如:
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'
转换说明中也会穿插一些字符用来表示“我接受这种形式的输入”,接下来我们来看一下上面那个例子:
缓冲区:[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,1asdf]
scanf("%d,%d,%c,%s", &num0, &num1, &ch, str);
在匹配完 num0 = 1,num1 = 1 后,scanf 就会返回,返回值为 2
最后,如果你完全理解了 scanf() 的话,可以试着运行一下这段代码,并试着分析一下为什么^^
#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() 用户控制符中的空白符会匹配所有任意数量的空白符
最后祝我的这位朋友能在代码世界里找到属于自己的乐趣~