查看原文
其他

一文带你读懂C/C++语言输入输出流与缓存区

脚本之家 2021-06-30

The following article is from 技术让梦想更伟大 Author 李肖遥

  脚本之家

你与百万开发者在一起

ID:技术让梦想更伟大

作者:李肖遥

前言

有没有发现,基本上所有的C语言入门书籍,或者是我们的教程里面,第一个C语言程序实体,都是“Hello Word!”;我不知道这是不是行业的“潜规则”,总之,它把无数的程序员带进了计算机的世界,步入了代码的大坑里,所以你好,世界!

一件趣事

我记得大学学习计算机的时候,就是在电脑这样的一个程序,不知道经过了什么过程,就能在计算机上显示出"Hello Word!"。后来我把这个"Hello Word!"改成了"I Love xxx!",哦买噶,真是惊呆我了,有一种立马在女神面前炫耀的感觉了.

那么,这其中有什么奥妙呢,我们从C语言的输入输出流开始说起.

hello world 是怎么显示出来的

对的,就是这样的一个程序

#include <stdio.h>
int main(int argc,char **argv)
{
  //printf("Hello Word!");
  printf("I Love xxx!");
}

在我们日常生活中,总看到电子显示屏上面显示着"欢迎某位领导莅临我校观察","宾馆"之类的,那是不是"Hello Word!"也是这样编译出来的呢?

我们看到程序中的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

最后

很多表面的现象看起来可能不能引起我们的注意,但是当我们注意到细节的时候,才能深究其中的奥秘,写代码亦是如此。

点击图片参与活动,免费包邮送书👇🏻👇🏻👇🏻

●  脚本之家粉丝福利,请查看 

●  人人都欠微软一个正版? 

●  C 语言漏洞最严重,PHP 最易受攻击,程序员该怎么写代码?

● 这才是你需要的C语言、C++学习路线!

 C语言写个贪吃蛇游戏

● C语言获得“2019 年度编程语言”称号

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存