稿C语言基础 -- scanf函数的工作原理

一、前言、基本概念和术语

在C语言中,输入主要是靠标准输入函数,也就是scanf函数来完成的。要正确的调用scanf函数来完成输入,需要了解scanf的工作原理。为了讲清楚原理,我先铺垫一下,介绍几个概念。

(1)输入流:就是输入缓存区中从输入设备中输入的一系列字符。在我们个人电脑上,输入设备就是键盘,你从键盘上敲的一堆东西就是输入流。在PTA等刷题网站,实际上使用文件模拟了输入流,题目中的输入样例,还有测试点中的输入也是输入流。

(2)匹配字符串:匹配字符串就是scanf函数调用时传入的第一个参数。也就是双引号里的那些字符。我们把匹配字符串中能出现的字符分为三类:1.格式转换说明符,简称格式符;2.空白符,包括空格、换行(\n)、制表(\t);3.其他字符,就是除了格式符和空白符以外的其他字符。

(3)地址列表:就是scanf函数调用时传入的第二个及其以后的参数。之所以叫作列表,是我们把它当作一个整体来看,它包含有多个地址,之间用逗号分隔开。

我们通过下面了例子,来熟悉这些概念。

输入:

1,2,3

代码:

#include 
int main () {
	int a,b,c;
	scanf("%d,%d,%d", &a, &b, &c);
	return 0;
}

例子中的输入框中的所有字符,就构成输入流,它依次为:'1',',','2',',','3',共有5个字符。

代码框中scanf的括号里面的"%d,%d,%d"就是匹配字符串,它依次为:'%d',',','%d',',','%d'。其中的'%d'是格式符,','是其他字符。

代码框中scanf的括号里面的 &a,&b,&c 就是地址列表。

二、scanf函数的工作原理

scanf函数工作时,主要是通过两个扫描探头和一个工作缓存来完成输入。这两个扫描探头分别工作在匹配字符串上和输入流上,分别称作匹配探头和输入探头。具体工作步骤如下:

0. 初始时,匹配探头和输入探头分别位于匹配字符串和输入流的首字符位置。工作缓存区为空。

1. 匹配探头读取第一个匹配字符;然后根据匹配探头读取到的字符的类别,接下来的操作也不同:

2-1. 若读取到的匹配字符是格式符,如%d,那么输入探头和输入缓存区开始工作。输入探头在当前位置扫描一个字符,并存放入输入缓存区。然后输入缓存区会判断现存的这些字符与当前的格式符匹配是否匹配。这里匹配的意思就是说,这些字符能否转换为合法的与格式符对应的值。如果匹配,则输入探头移动到下一个字符继续扫描。如果不匹配,说明一定当前扫描并存入的字符是非法的。那么输入缓存区将这个字符删除掉。然后将现存的这些合法的字符们转化为与格式符对应的值,存放到地址列表中对应的内存地址中,输入缓存区清零。最后匹配探头后移一位(如果还有的话),重复第1步。如果对于某个匹配字符,输入缓存区去掉非法字符后为空,或者说存入的第一个字符都是非法的,那么直接结束scanf,并返回。

2-2. 若读取到的匹配字符是空白符,则匹配探头后移一位,重复第1步。

2-3. 若读取到的匹配字符是其他字符,则输入探头开始工作。输入探头从当前位置扫描一个字符,与当前的匹配字符比较判断是否匹配。这里匹配的意思就是相等。如果匹配,则输入探头后移一位,匹配探头后移一位,重复第1步。如果不匹配,结束扫描,结束scanf,并返回。

3. 如果匹配探头无法再继续后移一位,结束scanf,并返回。

这里需要强调一点,如果匹配探头扫描到的格式符对应的值是数,例如%d,%f,%lf等,那么输入探头和工作缓存区会自动过滤掉合法字符出现前所有的空白符,这一点我们后面会举例说明。

从上面的讲解可以看出,scanf的工作过程还是稍微有些复杂,特别是当匹配探头扫描的格式符时,接下来工作缓存区如何判断是否匹配还是需要仔细考虑的。下面我们先给出一些例子,帮助大家来理解scanf整个的一个工作步骤。然后再给出更多例子,加强理解当匹配探头扫描的格式符时的处理情况。

例1

输入:

3.1416

代码:

#include 
int main () {
	int a=0;
	float b=0;
	scanf("%d%f", &a, &b);
	printf("%d %f\n", a, b);
	return 0;
}

输出:

3 0.141600

对于上面的例子,scanf的工作步骤如下:

(0) 初始化时匹配探头位置处于'%d'处;输入探头处于"3.1416"的'3'处,工作缓存区为空。

(1) 匹配探头扫描到%d,这是一个格式符。

(2) 输入探头扫描'3',工作缓存区存储"3",缓存区判断,当前存储的3能够合法的转为%d对应的值,即一个十进制整数,于是输入探头继续扫描。输入探头扫描到'.',工作缓存区存储"3.",显然"3."不能合法转换为一个整数。工作缓存区将"3."中的'.'删除,将"3"转化为十进制整数,存放到变量a中。匹配探头后移一位,继续第1步。

(1')匹配探头扫描到%f,是一个格式符。

(2')输入探头扫描'.',工作缓存区存储".",缓存区判断当前存储的"."能够合法的转为%f对应的值,即一个十进制浮点数(可以自行测试以%f,单独敲入一个'.',看看输出)。输入探头移动到'1'继续扫描,工作缓存区存储".1",缓存区判断当前存储的".1"能够合法的转为一个十进制浮点数。以此类推,直到输入探头扫描到3.1416的6后面'\n'或者'\0'时,工作缓存区存储".1416\n",缓存区判断当前存储的".1416\n"不能够合法的转为一个十进制浮点数,于是删掉'\n',将".1416"转换为十进制整数0.1416存方到变量b中。

(3)匹配探头无法再继续后移一位,结束scanf,并返回。

于是,我们看到的输出就是整数3和浮点数0.141600。

例2

输入:

3.1416

代码:

#include 
int main () {
	int a=0,b=2;
	scanf("%d%d", &a, &b);
	printf("%d %d\n", a, b);
	return 0;
}

输出:

3 2

对于上面的例子,scanf的工作步骤如下:

(0)初始化时匹配探头位置处于'%d'处;输入探头处于"3.1416"的'3'处,工作缓存区为空。

(1)匹配探头扫描到%d,这是一个格式符。

(2)输入探头扫描'3',工作缓存区存储"3",缓存区判断,当前存储的3能够合法的转为%d对应的值,即一个十进制整数,于是输入探头继续扫描。输入探头扫描到'.',工作缓存区存储"3.",显然"3."不能合法转换为一个整数。工作缓存区将"3."中的'.'删除,将"3"转化为十进制整数,存放到变量a中。匹配探头后移一位,继续第1步。

(1')匹配探头扫描到%d,是一个格式符。

(2')输入探头扫描'.',工作缓存区存储".",缓存区判断当前存储的"."不能合法的转为%f对应的值,即一个十进制浮点数,由于对于当前匹配字符%d,工作缓存区存入的第一个字符'.'都是非法的,所以直接结束scanf,并返回。

于是,我们看到的输出就是整数3和整数2(初始化时给b的值)。

例3

输入:

3.1416

代码:

#include 
int main () {
	int a=0,b=2;
	scanf("%d.%d", &a, &b);
	printf("%d %d\n", a, b);
	return 0;
}

输出:

3 1416

对于上面的例子,scanf的工作步骤如下:

(0) 初始化时匹配探头位置处于'%d'处;输入探头处于"3.1416"的'3'处,工作缓存区为空。

(1)匹配探头扫描到'%d',这是一个格式符。

(2)输入探头扫描'3',工作缓存区存储"3",缓存区判断,当前存储的3能够合法的转为%d对应的值,即一个十进制整数,于是输入探头继续扫描。输入探头扫描到'.',工作缓存区存储"3.",显然"3."不能合法转换为一个整数。工作缓存区将"3."中的'.'删除,将"3"转化为十进制整数,存放到变量a中。匹配探头后移一位,继续第1步。

(1') 匹配探头扫描到'.',是一个其他字符。

(2') 输入探头扫描'.',输入探头和匹配探头扫描到的字符相同。输入探头后移一位,匹配探头后移一位,继续第1步。

(1'')匹配探头扫描到'%d',这是一个格式符。

(2'')输入探头扫描'1',工作缓存区存储"1",缓存区判断,当前存储的1能够合法的转为%d对应的值,即一个十进制整数,于是输入探头继续扫描。直到输入探头扫描到"3.1416"后面的'\n',工作缓存区存储"1416\n",显然"1416\n"不能合法转换为一个整数。工作缓存区将"\n"删除,将"1416"转化为十进制整数,存放到变量b中。

(3)匹配探头无法再继续后移一位,结束scanf,并返回。

于是,我们看到的输出就是整数3和整数1416(显然是scanf改变了b的值)。

例4

输入:

3
1416

代码:

#include 
int main () {
	int a=0,b=2;
	scanf("%d%d", &a, &b);
	printf("%d %d\n", a, b);
	return 0;
}

输出:

3 1416

对于上面的例子,scanf的工作步骤如下:

(0)初始化时匹配探头位置处于'%d'处;输入探头处于"3\n1416"的'3'处,工作缓存区为空。

(1)匹配探头扫描到'%d',这是一个格式符。

(2)输入探头扫描'3',工作缓存区存储"3",缓存区判断,当前存储的3能够合法的转为%d对应的值,即一个十进制整数,于是输入探头继续扫描。输入探头扫描到'\n',工作缓存区存储"3\n",显然"3\n"不能合法转换为一个整数。工作缓存区将"3\n"中的'\n'删除,将"3"转化为十进制整数,存放到变量a中。匹配探头后移一位,继续第1步。

(1')匹配探头扫描到'%d',是一个格式符。

(2')输入探头扫描'\n',由于这是对于%d而言的合法字符出前的空白符,所以会被忽略,输入探头后移一位继续扫描。直到输入探头扫描到"1416\n"后面的'\n',工作缓存区存储"1416\n",显然"1416\n"不能合法转换为一个整数。工作缓存区将"\n"删除,将"1416"转化为十进制整数,存放到变量b中。

(3)匹配探头无法再继续后移一位,结束scanf,并返回。

于是,我们看到的输出就是整数3和整数1416(显然是scanf改变了b的值)。

注意分别观察例4和例2中的(2')对于当前匹配字符是数对应的格式符时,扫描到合法输入字符前,对输入的空白符会忽略,而对于输入的其他字符会停止输入,直接返回。

例5

输入:

A
B

代码:

#include 
int main () {
	char a='a',b='b';
	scanf("%c%c", &a, &b);
	printf("%c %c\n", a, b);
	return 0;
}

输出:

A
 

对于上面的例子,scanf的工作步骤如下:

(0)初始化时匹配探头位置处于'%c'处;输入探头处于"A\nB"的'A'处,工作缓存区为空。

(1)匹配探头扫描到'%c',这是一个格式符。

(2)输入探头扫描'A',工作缓存区存储"A",缓存区判断当前存储的"A"能够合法的转为%c对应的值,即一个字符,于是输入探头继续扫描。输入探头扫描到'\n',工作缓存区存储"A\n",显然"A\n"不能合法转换为一个字符。工作缓存区将"A\n"中的'\n'删除,将"A"转化为一个字符,存放到变量a中。匹配探头后移一位,继续第1步。

(1')匹配探头扫描到'%c',是一个格式符。

(2')输入探头扫描'\n',工作缓存区存储"\n",缓存区判断,当前存储的"\n"能够合法的转为一个字符,于是输入探头继续扫描。输入探头扫描到'B',工作缓存区存储"\nB",显然"\nB"不能合法转换为一个字符。工作缓存区将"\nB"中的'B'删除,将"\n"转化为一个字符,存放到变量b中。

(3)匹配探头无法再继续后移一位,结束scanf,并返回。

于是,我们看到的输出就是字符A和一个换行符'\n'。

这个例子同时说明,当匹配字符时%c(也包括%s)时,输入探头和工作缓存区不会忽略掉输入流中的空白符,因为空白符对于%c来说就是合法的字符。

进一步,在上例中如果想要将'A'输入变量a,将'B'输入变量b,我们可以有两种方法:一是在scanf的匹配字符串中合理添加'\n',二是在输入流中,用getchar()吃掉输入探头当前的'\n',让输入探头后移一位。具体实现代码如下:

代码1:

#include 
int main () {
	char a='a',b='b';
	scanf("%c\n%c", &a, &b);
	printf("%c %c\n", a, b);
	return 0;
}

代码2:

#include 
int main () {
	char a='a',b='b';
	scanf("%c", &a);
	getchar();
	scanf("%c", &b);
	printf("%c %c\n", a, b);
	return 0;
}

(更多例子未完待续... ...)

你可能感兴趣的:(C语言基础,c语言)