一文带你读懂 C/C++ 语言输入输出流与缓存区
(给CPP开发者加星标,提升C/C++技能)
前言
有没有发现,基本上所有的C语言入门书籍,或者是我们的教程里面,第一个C语言程序实体,都是“Hello World!
”;我不知道这是不是行业的“潜规则”,总之,它把无数的程序员带进了计算机的世界,步入了代码的大坑里,所以你好,世界!
一件趣事
我记得大学学习计算机的时候,就是在电脑这样的一个程序,不知道经过了什么过程,就能在计算机上显示出"Hello World!
"。后来我把这个"Hello World!
"改成了"I Love xxx!
",哦买噶,真是惊呆我了,有一种立马在女神面前炫耀的感觉了.
那么,这其中有什么奥妙呢,我们从C语言的输入输出流开始说起.
hello world 是怎么显示出来的
对的,就是这样的一个程序
#include <stdio.h>
int main(int argc,char **argv)
{
//printf("Hello World!");
printf("I Love xxx!");
}
在我们日常生活中,总看到电子显示屏上面显示着"欢迎某位领导莅临我校观察","宾馆"之类的,那是不是"Hello World!
"也是这样编译出来的呢?
我们看到程序中的printf();由系统或者编译器提供商提供的一个应用接口,是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。一般其函数原型应该是这样的:
int printf(const char *,...);
int _EXFUN(printf, (const char *__restrict, ...)
_ATTRIBUTE ((__format__ (__printf__, 1, 2))));
经过预处理,编译,汇编,链接四个过程,借助了相应的缓冲区来进行输入与输出,就会显示出来输入输出流
流是什么
“流”即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据的抽象描述。
在计算机系统中是指信息从外部输入设备向计算机内部输入,或者从内存向外部输出设备输出的过程。这种输入输出的过程被形象的比喻为“流”。
输入输出
什么是输入输出呢?C语言中我们用到的最频繁的输入输出方式就是scanf()与printf()。
scanf():从标准输入设备(键盘)读取数据,并将值存放在变量中。
printf():将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。
字符输入输出(getchar/putchar),字符串输入输出函数 (gets与puts),与gets/puts类似的还有fgets与fputs,它们一般用于对文件的操作.
缓冲区
定义
缓冲区是内存空间的一部分,也就是说在内存空间中预留了一定大小的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
原理介绍
当调用输入函数scanf()时,输入函数会将我们输入的数字输入到输入缓冲区,而当我们的输入缓冲区有内容时,再次输入将不会被执行,而是直接跳过执行,将输入缓冲区的内容赋给变量。
引入缓冲区的意义
缓冲区就是一块内存,用来做数据的一个临时存放点,在输入输出操作中起着至关重要的作用,在百度百科定义如下
比如我想把一篇文章以字符序列的方式输出到计算机显示器屏幕上,那么我的程序内存作为数据源而显示器驱动程序作为数据目标,如果数据源直接对数据目标发送数据的话。数据目标获得第一个字符,便将它显示。然后从端口读取下一个字符,可是这时就不能保证数据源向端口发送的恰好是第二个字符(也许是第三个,而第二个已经在数据目标显示时发送过了)。这样的话就不能保证输出的数据能完整的被数据目标所接受并处理。
缓冲区的类型
缓冲区有三种,我一个一个地说下:
1、全缓冲
内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,只要1024个字节都放满了,那么程序会立即来读取这1024个字节的数据。只要1024个字节没有放满,哪怕只放了1023个字节,程序都不会来读取,这就是全缓冲的意思。
#include <fstream>
using namespace std;
int main()
{
//创建文件test.txt并打开
ofstream outfile("test.txt");
//向test.txt文件中写入4096个字符’a’
for(int n=0; n < 4096; n++)
{
outfile << 'a';
}
//暂停,按任意键继续
system("PAUSE");
//继续向test.txt文件中写入字符’b’,也就是说,第4097个字符是’b’
outfile << 'b';
//暂停,按任意键继续
system("PAUSE");
return 0;
}
编译并执行,运行结果如下:
此时打开工程所在文件夹下的test.txt文件,您会发现该文件是空的,这说明4096个字符“a”还在缓冲区,并没有真正执行I/O操作。敲一下回车键,窗口变为如下:
此时再打开test.txt文件,您就会发下该文件中已经有了4096个字符“a”。这说明全缓冲区的大小是4K(4096),缓冲区满后执行了I/O操作,而字符“b”还在缓冲区。
再次敲一下回车键,窗口变为如下: 此时再打开test.txt文件,您就会发现字符“b”也在其中了。这一步验证了文件关闭时刷新了缓冲区。
2、行缓冲
内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,假如放了10个字节的数据,你敲了回车键,那么程序就马上来读取了。假如放了20个字节,你敲了回车奖,程序也会来读取。所以即使1024个字节没有放满,但是你敲了回车键,程序就会来读取,这个就叫做行缓冲。
使用键盘操作演示行缓冲,先介绍getchar()函数。函数原型:
int getchar(void) ;
说明:当程序调用getchar()函数时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()函数才开始从键盘缓冲区中每次读入一个字符。也就是说,后续的getchar()函数调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才重新等待用户按键。
#include<iostream>
using namespace std;
int main()
{
char c;
//第一次调用getchar()函数,程序执行时,您可以输入一串字符并按下回车键,按下回车键后该函数返回。返回值是用户输入的第一个字符 (假设用户输入了 abcdef,函数返回a)
c = getchar();
//显示getchar()函数的返回值
cout<< c << endl; // 输出 a
// 循环多次调用getchar()函数,将每次调用getchar()函数的返回值显示出来,直到遇到回车符才结束。 这时函数执行不会让用户输入而是顺序读取缓冲区字符内容。第一个字符用户输入结束后已经读取,所以会从第二个字符开始读
while((c = getchar())!='\n')
{
cout<< "," << c <<endl
}
return 0;
}
最后输出结果是
a ,b ,c ,d ,e ,f
可以交替按下一些字符,编译结果如下:
当按到第4096个字符时,提示您不能再按下去,说明行缓存的大小是4k,此时按下回车键,返回第一个字符是‘a’继续敲下回车键,缓存区的其他字符就全部输出
3、无缓冲
内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,刚放了1个字节,程序就马上来读取了;又放了一个字节,程序又马上来读取了,这就是没有缓冲。
在C语言中,一般规定是要有行缓冲的。但是使用scanf函数和getchar时,如果行缓冲的换行符没有处理好,程序运行可能会有异常或者闪退等现象。也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。
如错误输出时使用:
cerr<<”错误,请检查输入的参数!” ;
这条语句等效于:
fprintf(stderr, ”错误,请检查输入的参数!”) ;
缓冲区的大小
如果我们没有自己设置缓冲区的话,系统会默认为标准输入输出设置一个缓冲区,这个缓冲区的大小通常是 512个字节 的大小。
缓冲区大小由 stdio.h 头文件中的宏 BUFSIZ 定义,如果希望查看它的大小,包含头文件,直接输出它的值即可:
printf("%d", BUFSIZ);
缓冲区的大小是可以改变的,也可以将文件关联到自定义的缓冲区,详情可以查看 setvbuf()和 setbuf() 函数。
缓冲区的刷新
下列情况会引发缓冲区的刷新:
-
缓冲区满时; -
执行flush语句,即使用特定函数刷新缓冲区; -
执行endl语句,即行缓冲区遇到回车时; -
关闭文件。
可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区) 如:
cout << flush; //将显存的内容立即输出到显示器上进行显示
endl控制符的作用是将光标移动到输出设备中下一行开头处,并且清空缓冲区。
cout < < endl;
相当于
cout < < ”\n”< < flush;
强制缓冲区的数字打印
/*
输出缓冲区演示
*/
#include <stdio.h>
int main(){
printf("1\n");
fflush(stdout); //强制将输出缓冲区的内容显示在屏幕上
while (1){
}
return 0;
}
如何清空输入缓冲区的内容?
如果我想让getchar()每次都能够等待用户输入的话就要清空缓冲区,下面就介绍不同平台的方法
C标准规定 fflush()函数是用来刷新输出(stdout)缓存的。对于输入(stdin),它是没有定义的。但是有些编译器也定义了 fflush( stdin )的实现,比如微软的VC。其它编译器是否也定义了 fflush( stdin )的实现应当查找它的手册。GCC编译器没有定义它的实现,所以不能使用 fflush( stdin )来刷新输入缓存。
对于没有定义 fflush( stdin )的编译器,可以使用 fgets()函数来代替它(比用 getchar()、scanf()等函数通用性好)。可以这样忽略输入流中留下的回车等其它输入,从而使下一次的输入总是保持一个“干净”的状态。(这个是任何平台下都可以的)
char sbuf[1024];
// ...
fgets( sbuf, 1024, stdin );
// ...
在windows 的vc下面就可以这样了:
for(int i=0;i<10;++i)
{
char ch=getchar();
fflush(stdin); //每次都会有等待状态了
}
这里说到gcc编译器没有定义fflush的实现,我们一般用getchar();来清除缓冲区
#include <stdio.h>
main()
{
char c;
for(;(c=getchar())!='a';)
printf("%c",c);
getchar();
c=getchar();
printf("%c",c);
}
输入:ssss,回车
得到:ssss,光标处(等待输入)
说明: 此时程序没有结束,进行到for循环,因为并没有字符a出现,所以还没跳出for循环.键入回车后,getchar依次从缓冲区内取出(for循环):'s''s''s''s''\n'
如果我们输入:ssssa,回车
得到:ssssa,光标处(等待输入)
说明: 程序已经跳出for循环,但是由于我们用getchar();清除了换行'\n',后面第7句c=getchar();需要你输入一个字符(因为ssssa后面并没有新的字符),所以程序仍然没有结束.
如果我们注释掉getchar();这一句,那么得到:ssss,光标处(程序结束) 这个输入ssssa是的回车中的换行符'\n'就被c=getchar();这一句读取并输出了。
总结:键盘输入的字符都存到缓冲区内,一旦键入回车,getchar就进入缓冲区读取字符,一次只返回第一个字符作为getchar函数的值,如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到'\n'.
要理解这一点,之所以你输入的一系列字符被依次读出来,是因为循环的作用使得反复利用getchar在缓冲区里读取字符,而不是ge
最后
很多表面的现象看起来可能不能引起我们的注意,但是当我们注意到细节的时候,才能深究其中的奥秘,写代码亦是如此。
- EOF -
看完本文有帮助?请分享给更多人
关注「CPP开发者」加星标,提升C/C++技能
好文章,我在看❤️