scanf函数读取缓冲区数据的问题
标准I\O的缓冲类型
标准I\O根据不同的应用需求,提供了全缓冲、行缓冲、无缓冲三种缓冲方式。
全缓冲:只有当划定的缓冲区被填满或者数据读取至末尾时,才开始执行 I\O 操作(执行系统提供的 read\write 操作)。磁盘文件的读写一般采用这种方式。
行缓冲:当输入输出过程遇到换行符''\n"或者当分配缓冲区已满时,才开始执行 I\O 操作。一般涉及终端的读写操作如 stdin 与 stdout 使用这种缓冲方式。
无缓冲:当有数据产生时,马上由相应的设备进行处理。一般来说 stderr(standard error) 使用这种缓冲方式,使得有错误信息时马上能够得到响应。需要注意的是,标准库不缓存并不意味着操作系统或者设备驱动不缓存。
注意,以上关于 stdin/stdout 的缓冲方式并不是直接规定死的。一些语言的语言规范会对缓冲实现给出一定的限制,但并不具体,只是许多标准I/O是以上述方式实现的而已。可以参考关于流和缓冲区。
行缓冲
标准输入缓冲区 stdin 使用行缓冲的方式存储输入。用户的输入数据首先被暂存在临时缓冲区中,当用户键入回车键或临时缓冲区满后,stdin 才进行 I/O 操作,将数据由临时缓冲区拷贝至 stdin 中。C语言提供的输入输出函数如 scanf 、getchar 等则从上述缓冲区 stdin 中读取数据输入。
scanf 和 getchar 等函数会在 stdin 中读取数据,若上述缓冲区中已存在数据,则直接读取其中的数据,若上述缓冲区为空,则上述函数会挂起,等待数据缓冲的完成( 用户输入回车键或数据缓冲区满后, stdin 会进行数据缓冲,之后上述函数才能继续执行)。 用户一次输入的数据可能会超过 scanf 、getchar 等函数调用所需要的数据,那么所需数据被读取后,剩余的数据仍会存放在缓冲区中,之后的函数调用会直接读取 stdin 中已有的数据。只有当缓冲区为空后,scanf 等函数才会等待用户输入(实际应该是等待 stdin 的缓冲。
scanf函数
scanf函数: scanf C++ reference
函数声明:int scanf( format string , arg1 , arg2 , ...);
从函数声明可以看到,scanf 的参数由指示读取动作的格式化字符串( format string )和相应的地址参数 arg1...argn 组成。scanf 函数将输入从标准输入缓冲区 stdin 中读入,并将它们以格式化字符串中指定的格式存储到额外的参数 arg1...arg2 等指定的内存空间中。其中额外的参数(additional argument)指向的内存空间的数据类型应该与格式化字符串中指定的数据类型相一致。
格式化字符串(format string)
格式化字符串规定了 scanf 等函数如何从输入缓冲 stdin 中读取数据,其组成字符的含义如下所示:
(1)空白字符(whitespace)。scanf 会读取并忽略在 stdin 中下一个非空白字符之前的所有空白字符(空格、换行和 tab),然后读取格式化字符串中规定格式的数据。若格式化字符串中包含空白字符,则该空白字符会与输入缓冲区中任意数量的连续空白字符相匹配,并将其从缓冲区中清除(包括0个)。例如格式化字符串"%d %d",会要求 scanf 首先从缓冲区中读取一个整型(若之前存在空白字符则跳过),再跳过输入缓冲区中连续的空白字符(与格式化字符串中的空白字符匹配),最后再读取一个整形;
(2)非空白字符(non whitespace)。对于格式化字符串中既非空白字符又不是格式说明符(format specifier,由%标识)的一部分的字符,scanf 会尝试从 stdin 中读取输入,并将输入与该字符比较,若匹配,则继续进行后续读取,若不匹配,则函数返回错误信息;
(3)格式说明符。以 % 开头的用于指定输入数据格式的字符。如 %d 指定需要读取一个整形,%s 需要读取一个字符串。scanf 等函数首先根据格式说明符尝试去解析 stdin 中的数据,如对于 %d ,scanf 会尝试对 stdin 中已有数据以整型的格式进行解析。若解析成功,则将上述解析结果存放到指定的内存中,若解析失败,如 stdin 中仅存在一个字符 'a',scanf 会退出并返回,但是上述不匹配的数据并不会从缓冲区中清除,后续的 scanf 调用仍从上述输入开始读取;
由以上3条规则,通过设置格式化字符串可以规定了 scanf 函数的行为。下面为示例:
scanf("%s,%d",&a,&b); //scanf需先读取一个字符串,再读取一个 ','(规则2),最后读取一个整数
scanf("%d\t%d",&a,&b); //scanf需先读取一个整数,再将格式化字符串中的 '\t' (空白字符)与缓冲区中0个或多个空白字符匹配并清除(规则1),最后读取一个整数
scanf("%d%d",&a,&b); //scanf需要先读取一个整数,之后再读取一个整数,两个整数之间的空白字符会被忽略(规则1)
字符和字符串的读取
对于 stdin 中的字符的读取,scanf 、 getchar 等函数会读取缓冲区中的第一个字符,包括空白字符和非空白字符。可能存在首先通过 scanf 读取了数据,如一个带换行的数字,之后再用 "%c" 读取字符时,字符读取的是换行符 '\n' 的情况。为了消除该换行符对正常逻辑的影响,可以在格式化字符串前面加入一个空格作为空白字符,如" %c",此时对于字符的读取会首先跳过上一次输入留下的换行符(前面的规则1),从而保证读取数据的正确性。
对于 stdin 中的字符串的读取,scanf 会在开始处理后(跳过第一个非空白字符之前的空白字符,规则1)读取到的第一个空白字符处退出,并在读取的字符串尾部加入'\0'作为结束标志。
缓冲区读取数据问题示例
例1:
程序先输出变量未初始化之前的值,再使用scanf读取输入,再显示读取输入之后的值
printf("%d,%d,%c\n",a,b,c); //输出未初始化之前的值
scanf("%d%d",&a,&b);
scanf("%c",&c);
printf("%d,%d,%c\n",a,b,c); //输出初始化之后的值
结果如下图所示
解释如下:
(1)用户输入至缓冲区中的数据实际为 12 + 空格 + a + 换行符 ;
(2)第一次读取输入时,首先将读取到的第一个数字12赋值给变量 a,之后 scanf 会试图读取下一个十进制数,但是发现下一个非空白字符(忽略输入的空格)为字符 'a',与其所需要读取的数据类型不符,scanf 会退出并返回一个常数值来表示错误信息.此时字符 'a' 并未被读取,仍然存在于缓冲区中;
(3)第二次读取输入时,scanf 就会发现缓冲区中第一个非空白字符为字符 'a',从而会将字符 'a' 赋值给变量 c,并退出。
故而,再次输出变量时,变量 a 和 c 均已改变,而变量 b 只能保持原值。
例2:
用于测试的函数先读取一个字符串,再读取一个字符,并将结果输出
scanf("%s",a);
scanf("%c",&b);
printf("%s,%d",a,(int)b);
输出结果如下
解释如下:
(1)用户在输入时,实际进入缓冲区中的数据为字符串'"abcd" + 换行符;
(2)第一次读取时,scanf 会读取一个字符串,并在遇到第一个空白字符处停止,这里为换行符,即读取的字符串为"abcd",scanf 函数还会在该字符串尾部加入'\0'进行存储;
(3)第二次读取时,scanf 会读取一个字符,进行字符读取时空白符也被视为有效输入字符,故而 scanf 会读取换行符,而换行符的ASCII值即为10;
例3:
将读取输入的要求换一下,要求读取两个字符串
结果scanf会再次等待用户输入
原因在于在读取第一个字符串后,缓冲区中剩余一个换行符,而根据规则1,在读取字符串之前会跳过所有的空白字符,之后scanf会发现此时缓冲区已经为空,从而需要再次等待用户输入。
事实上,对于上述情况,除非第二次读取的参数是可以读取空白字符的 %c,其他的参数均会使得 scanf 认为缓冲区已为空,从而进入等待用户输入的状态。
getchar
getchar 是用于字符输入的C库函数,其函数的声明包含在头文件 stdio.h,函数声明为: int getchar(void).其功能是读取标准输入stdin中的一个字符。
getchar 从标准输入中读取数据,而 stdin 是采用行缓冲的方式记录用户输入,也就是只有当用户键入回车键或输入至缓冲区末尾时,才会开始 I\O 操作,亦即读取一个字符。这样用户可以一次输入不止一个字符,读取过后缓冲区可能不为空。当再次调用 getchar 时,若缓冲区不为空,getchar 就会直接读取在缓冲区中字符,而不是等待用户输入。可以认为是getchar 等待的是行缓冲的完成,而不是用户输入的完成,在行缓冲完成后,只要缓冲区不为空,getchar 就可以读取字符,而不需要等待用户输入。
/*codeblocks13.12*/ #include <stdio.h> int main(void) { char ch = '\0'; while(ch != '\n') { printf("输入一个字符:"); ch = getchar(); printf("\n"); putchar(ch); printf("\n"); } return 0; }
程序的运行结果如下:
可以明显看到,后续执行中并不要求用户输入,getchar()会直接读取缓冲区中的数据。而且对于字符的读取操作而言,换行符'\n'也被视为一个字符,而不是单纯的结束标志。
等待用户输入的字符输入
getchar 可以直接从缓冲区中读取字符,而不等待用户输入,但这种方式也有可能带来潜在的错误。这里给出两种等待用户输入的字符传入方式。
1.使用 getche 与 getch 函数。上述函数均从键盘上读入一个字节,其中后者不会将字符回显到屏幕上。以这两个函数读取字符时,都是通过调用函数读取一个键盘输入且只有一个。如调用 getche,键盘敲击 'abc' 时,只有一个字符 'a' 会被读取。其他字符为无效输入。但上述函数并不被包含在标准 C 函数库中,需要通过头文件 conio.h 来使用,并不被所有的编译器实现支持。
2.在每次调用 getchar 函数之后,手动对缓冲区进行清除操作。可以使用 fflush() 函数清理缓冲区。C标准规定 fflush()函数可用来刷新输出(stdout)缓冲区(一般是将缓冲区数据写回存储设备)。但对于标准输入(stdin)则没有明确定义。部分编译器定义了 fflush( stdin )的实现,如微软的VC。也就是不同的编译器对于 fflush( stdin )的支持可能不同。GCC编译器没有定义它的实现,所以不能使用 fflush( stdin )来刷新输入缓冲区。