缓冲区
根据数据刷新的时机可以将缓冲区的类型分为:全缓冲、行缓冲、无缓冲
(注意:Windows下的输出设备没有缓冲区,意思是printf是无缓冲的,但是在Linux下printf就是行缓冲的,至于为什么Windows下printf是无缓冲的,后文会提到)
全缓冲:当缓冲区被填满以后才进行真正的输入输出操作
行缓冲:当在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作
无缓冲:没有缓冲区,立即进行输入输出
(图片来源:https://www.cnblogs.com/mydomain/p/9817320.html)
(缓冲区其实是一块内存空间,它用在硬件设备和用户程序之间,用来缓存数据,目的是让快速的 CPU 不必等待慢速的输入输出设备,同时减少操作硬件的次数)
为什么Windows下C语言的printf是无缓冲的?
C语言作为一个面向过程的语言,算不上高级语言也不能说是低级语言,C语言完成的工作大多都是偏底层开发工作(再底层一点可能就是汇编了),C语言常常用在嵌入式、单片机之类的小缓冲区的东西上,
嵌入式设备的缓冲区通常远小于PC,如果printf是行缓冲的,可能刚写进去一点东西,就可能被别的玩意儿盖掉了,所以嵌入式做开发带流的函数进行I/O的时候通常一次I/O马上就要fflush()
缓冲区的刷新遵循以下的规则
1、不管是行缓冲还是全缓冲,缓冲区满时会自动刷新
2、行缓冲遇到换行符\n
时会刷新
3、关闭文件时会刷新缓冲区
4、程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的
5、使用特定的函数也可以手动刷新缓冲区
C语言标准规定,输入输出缓冲区要具有以下特征:
1、当且仅当输入输出不涉及交互设备时,它们才可以是全缓冲的
2、错误显示设备不能带有缓冲区
所谓交互设备,就是现代计算机上的显示器和键盘。C标准虽然规定它们不能是全缓冲的,但并没有规定它们到底是行缓冲还是无缓冲,这就导致不同的平台有不同的实现
输入设备
Windows、Linux、Mac OS 在实现时都给输入设备带上了行缓冲,所以 scanf()、getchar()、gets() 在每个平台下的表现都一致
Windows 下特有的 getche() 和 getch()函数,都不带缓冲区
输出设备
Windows 平台下,输出设备是不带缓冲区的
Linux 和 Mac OS 平台下,输出设备带有行缓冲区
scanf函数——行缓冲
这个可能是最常用到的输入函数了,scanf() 是从标准输入设备(键盘)读取数据,当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据,是带有行缓冲区的,如果没有,就等待用户输入,
用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符('\n'),输入结束
需要注意的是:当输入的数据与存储该数据的变量类型不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等
例如:
#include <stdio.h> #include <Windows.h>
int main() { int a = 10, b = 20; scanf("%d%d", &a, &b); printf("%d %d", a, b); return 0; }
如果输入1(空格)2,中间的空格会被自动忽略到,也就是上面所说的会尝试忽略掉一些空白符(像前面这样的数字(空格)数字输入格式,无论中间输入多少的空白符会被忽略)
scanf还有一个比较奇怪的特性:只有当控制字符串以格式控制符开头时,才会忽略换行符、空格、制表符等空白符,为了读者理解这句话的含义,下面上代码
情况一:不会忽略换行符
1 #include <stdio.h>
2 #include <Windows.h>
3
4 int main() 5 { 6 int a = 10, b = 20; 7
8 scanf("%d", &a); 9 scanf("b=%d", &b); 10 printf("%d %d", a, b); 11
12 return 0; 13 }
当输入1回车时,回车符虽然是属于空白符,但是此时不能被忽略,不能被忽略的换行符('\n')和格式控制字符串%d又不匹配,所以只能读取失败了,b的值不变还是20
情况二:会忽略换行符
1 #include <stdio.h>
2 #include <Windows.h>
3
4 int main() 5 { 6 int a = 10, b = 20; 7
8 scanf("%d", &a); 9 scanf("%d", &b); 10 printf("%d %d", a, b); 11
12 return 0; 13 }
此时控制字符串变成以格式控制字符串(%d)开头,此时输入1回车,第二个scanf("%d", &b)就会把回车符('\n')忽略掉,等待用户输入下一个数字(在输入1回车后无论输入多少个空格\制表符\回车都会被忽略掉)
下面来分析下scanf("%s")输入字符串时的特点
scanf使用格式控制字符串%s读入字符串时,遇到回车、空格、制表符结束输入,下面将举多个栗子来分析scanf输入字符串时的特点
例一:
1 #include <stdio.h>
2 #include <Windows.h>
3
4 int main() 5 { 6 char ss[2][10]; 7
8 scanf("%s%s", ss[0], ss[1]); 9
10 printf("%s%s\n", ss[0], ss[1]); 11
12 return 0; 13 }
输入为:Hello(空格)World
从结果可以看出,中间的空格被忽略掉了,现在来分析下过程:
前半部分的 Hello(空格) 碰到空格时就结束第一次输入,将Hello存储在ss[0]这个字符串中(注意:此时空格符仍然存在于缓冲区中,并没有被读走)继续读取第二个字符串,
此时scanf会先瞧一瞧缓冲区中有没有东西,发现里面有一个空格符,忽略掉,继续读取World,发现后面有个换行符,结束第二次输入,将World存储在ss[1]这个字符串中,换行符仍然留在缓冲区
(这里的特点和输入单个字符串一致,即在输入第一个非空白符前,输入多少个空格符(空格\制表符\回车)都会被忽略掉)
getchar()——行缓冲
getchar()可以看作scanf("%c");的一个简化版本,它和scanf("%c")的特性相同,这里就不再赘述了
gets()函数——行缓冲
gets(ss)遇到回车结束输入,读取的换行符被转换为NULL值,做为字符数组的最后一个字符,来结束字符串(意思就是将'\n'变成了'\0'),gets()以回车作为字符串的终止符,同时将回车符从输入缓冲区读走,
但不作为字符串的一部分。而scanf()不读走回车符,回车符仍留在输入缓冲区中,除了输入结束标志的不同外,gets和scanf在读取字符串时还有一个很大的区别,gets在读入字符串时不会忽略掉空白符,
意思是如果有语句gets(ss[0]),如果只按一下回车,那么回车就被当作字符串读入ss[0]这个字符串中,而不像scanf那样在读入到第一个非空白符之前忽略掉输入的所有空白符
代码示例:
1 #include <stdio.h>
2 #include <Windows.h>
3
4 int main() 5 { 6 char ss[2][10]; 7
8 gets(ss[0]); 9 gets(ss[1]); 10
11 printf("%s%s", ss[0], ss[1]); 12
13 return 0; 14 }
getch()函数——不带缓冲区(无缓冲)
函数功能:当有输入时,立即读取(不需要按回车),不显示在屏幕上(不带回显),需要注意的是这个函数并非标准函数,跨平台使用时需要考虑移植性(getch() 位于 conio.h 头文件中,而这个头文件是 Windows 特有的,
Linux 和 Mac OS 下没有包含该头文件。换句话说,getche() 并不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用)
getche()函数——不带缓冲区(无缓冲)
函数功能:当有输入时,立即读取(不需要按回车),并显示在屏幕上(带回显),和getch()一样位于conio.h中,是非标准函数,认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用
关于清空缓冲区的三种方法
在谈到清空缓冲区的方法时,可能很多读者都会想到fflush(stdin)来清空缓冲区,但是fflush(stdin)存在一个问题,就是C语言标准规定,当 fflush() 用于stdout时,必须要有清空输出缓冲区的作用
但是C语言标准并没有规定 fflush() 用于stdin时的作用,编译器的实现者可以*决定,所以它的行为是未定义的
(fflush(stdin) 这种不标准的写法只适用于一部分编译器,通用性比较差)
方法一:使用getchar()来清空缓冲区
特点:getchar() 是带有缓冲区的,并且一切字符通吃,或者说一切字符都会读取,不会忽略
int c; while((c = getchar()) != '\n' && c != EOF);
方法二:使用scanf()来清空缓冲区
scanf("%*[^\n]"); scanf("%*c");
上面的语句分成两部分来看,第一部分:scanf("%*[^\n]"),这个语句作用是读取缓冲区中回车符('\n')之前的所有字符,并丢弃,在遇到回车符('\n')时便停止读取(注意:此时缓冲区中还有一个回车符),
第二部分:scanf("%*c"),这个语句的作用是读取一个字符,并丢弃,这样就将缓冲区中仅剩的一个回车符也读走了,此时就已经清空了缓冲区
方法三:使用fflush(stdin)来清空缓冲区
fflush(stdin);
不太推荐使用这种方法(还是因为通用性的问题,建议使用前面两种方法,因为前两种方法在任何平台、任何编译器、任何情景下都有效)