标准I\O的缓冲类型
标准I\O根据不同的应用需求,提供了全缓冲、行缓冲、无缓冲三种缓冲方式。
全缓冲:只有当划定的缓冲区被填满或者数据读取至末尾时,才开始执行I\O操作(执行系统提供的read\write操作)。磁盘文件的读写一般采用这种方式。
行缓冲:当输入输出过程遇到换行符''\n"或者当分配缓冲区已满时,才开始执行I\O操作。一般涉及终端的读写操作如stdio与stdout使用这种缓冲方式。
无缓冲:当有数据产生时,马上由相应的设备进行处理。一般来说stderr(standard error)使用这种缓冲方式,使得有错误信息时马上能够得到响应。需要注意的是,标准库不缓存并不意味着操作系统或者设备驱动不缓存。
注意,以上关于stdio/stdout的缓冲方式并不是直接规定死的。一些语言的语言规范会对缓冲实现给出一定的限制,但并不具体,只是许多标准I/O是以上述方式实现的而已。可以参考关于流和缓冲区。
scanf函数
scanf函数: scanf C++ reference
函数声明:int scanf( format string , arg1 , arg2 , ...);
从函数声明可以看到,scanf的参数由指示读取动作的format string和相应的地址参数arg1...argn组成。scanf函数将输入从标准输入缓冲区stdin中读入,并将它们以format string中指定的格式存储到额外的参数arg1...arg2等指定的内存空间中。其中额外的参数(additional argument)指向的内存空间的数据类型应该与format string中指定的数据类型相一致。
format string
format string字符串规定了如何从输入缓冲stdin中读取数据:
(1)空白字符(whitespace)。scanf会读取并忽略在stdin中下一个非空白字符之前的所有空白字符(空格、换行和tab),然后读取format string中规定格式的数据。format string中包含的空白字符会与输入缓冲区中任意数量的连续空白字符相匹配,并将其从缓冲区中清除(包括0个);
(2)非空白字符(non whitespace)。对于format string中既非空白字符又不是格式说明符(format specifier,由%标识)的一部分的字符,scanf从stdin中读取输入,并将输入与该字符比较,若匹配,继续进行读取,若不匹配,则函数返回错误信息;
(3)格式说明符说明了存储输入数据的格式,若当前的字符与格式说明符指定的数据类型不符,scanf会退出并返回;
由以上3条规则可知,format string规定了scanf函数的行为。下面为示例:
scanf("%s,%d",&a,&b); //scanf需先读取一个字符串,再读取一个 ‘,’(规则2),最后读取一个整数
scanf("%d\t%d",&a,&b); //scanf需先读取一个整数,再将format string中的 \t (空白字符)与缓冲区中0个或多个空白字符匹配并清除(规则1),最后读取一个整数
scanf("%d%d",&a,&b); //scanf需要先读取一个整数,之后再读取一个整数,两个整数之间的空白字符会被忽略(规则1)
行缓冲
标准输入缓冲区stdin使用行缓冲的方式存储输入。亦即在用户键入回车键或缓冲区满后才进行I/O操作,此时输入的数据均存放在缓冲区中。
scanf会在stdin中读取数据,且scanf未读取的数据仍会存放在缓冲区中,之后的scanf操作仍从这里读取数据,只有当缓冲区为空后,scanf才会等待用户输入(实际应该是等待stdin的缓冲)。
字符和字符串的读取
对于stdin中的字符的读取,scanf会读取缓冲区中的第一个字符,包括空白字符和非空白字符。
对于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;
但是将读取输入的要求换一下,要求读取两个字符串
结果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'会被读取。其他字符为无效输入。
2.在每次getchar()之后,手动对缓冲区进行清除操作。可以使用fflush()函数清理缓冲区。C标准规定 fflush()函数可用来刷新输出(stdout)缓冲区(一般是将缓冲区数据写回存储设备)。但对于标准输入(stdin)则没有明确定义。部分编译器定义了 fflush( stdin )的实现,如微软的VC。也就是不同的编译器对于 fflush( stdin )的支持可能不同。GCC编译器没有定义它的实现,所以不能使用 fflush( stdin )来刷新输入缓冲区。