你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C语言的输入输出流

[复制链接]
gaosmile 发布时间:2020-8-1 22:44
前言

/ j0 x( R/ O% O+ g; y2 X4 T( ?

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

一件趣事

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

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

hello world 是怎么显示出来的

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

#include <stdio.h>5 {2 K- ?# b! }& [
int main(int argc,char **argv)
, r6 B1 h( C% M2 F{* S# O# R/ C1 d5 ]& l
  //printf("Hello World!");
- {9 l7 ?4 x( F* m  h  printf("I Love xxx!");% |3 M2 @* `  B& S
}
3 b6 w7 j6 _" d5 A

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

我们看到程序中的printf();由系统或者编译器提供商提供的一个应用接口,是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。一般其函数原型应该是这样的:

int  printf(const char *,...);/ a; l; @% ^% I
int _EXFUN(printf, (const char *__restrict, ...)5 l) s% S9 J1 R; V- i7 O
    _ATTRIBUTE ((__format__ (__printf__, 1, 2))));

1 m! _! @6 a0 w  f

经过预处理,编译,汇编,链接四个过程,借助了相应的缓冲区来进行输入与输出,就会显示出来

微信图片_20200801223447.png

输入输出流

流是什么

“流”即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据的抽象描述。

在计算机系统中是指信息从外部输入设备向计算机内部输入,或者从内存向外部输出设备输出的过程。这种输入输出的过程被形象的比喻为“流”。

输入输出

什么是输入输出呢?C语言中我们用到的最频繁的输入输出方式就是scanf()与printf()。

微信图片_20200801223451.png

scanf():从标准输入设备(键盘)读取数据,并将值存放在变量中。

微信图片_20200801223454.png

printf():将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。

微信图片_20200801223457.png

字符输入输出(getchar/putchar),字符串输入输出函数 (gets与puts),与gets/puts类似的还有fgets与fputs,它们一般用于对文件的操作.

缓冲区定义

缓冲区是内存空间的一部分,也就是说在内存空间中预留了一定大小的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

原理介绍

当调用输入函数scanf()时,输入函数会将我们输入的数字输入到输入缓冲区,而当我们的输入缓冲区有内容时,再次输入将不会被执行,而是直接跳过执行,将输入缓冲区的内容赋给变量。

引入缓冲区的意义

缓冲区就是一块内存,用来做数据的一个临时存放点,在输入输出操作中起着至关重要的作用,在百度百科定义如下

比如我想把一篇文章以字符序列的方式输出到计算机显示器屏幕上,那么我的程序内存作为数据源而显示器驱动程序作为数据目标,如果数据源直接对数据目标发送数据的话。数据目标获得第一个字符,便将它显示。然后从端口读取下一个字符,可是这时就不能保证数据源向端口发送的恰好是第二个字符(也许是第三个,而第二个已经在数据目标显示时发送过了)。这样的话就不能保证输出的数据能完整的被数据目标所接受并处理。

缓冲区的类型

缓冲区有三种,我一个一个地说下:

1、全缓冲

内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,只要1024个字节都放满了,那么程序会立即来读取这1024个字节的数据。只要1024个字节没有放满,哪怕只放了1023个字节,程序都不会来读取,这就是全缓冲的意思。

#include <fstream> & k! l) u* ^. H. V
using namespace std;
) L/ z, ~6 D- y/ U5 i  }4 E; h4 v, H! m8 n) `% W9 ?
int main()
: f5 R% E5 v0 y* r+ w  A" J9 M{ 6 F5 ]4 G) m" m7 \- Y0 Y" ]' Q  L
    //创建文件test.txt并打开 + v/ M& X8 k/ ?" C
   ofstream outfile("test.txt");
+ e( Z! C9 y8 v" N: w# s$ R0 t9 `4 B' X  `# A3 c
    //向test.txt文件中写入4096个字符’a’ ! C1 K  d1 |8 }7 H" z! N
   for(int n=0; n < 4096; n++)
# {) F$ W/ i8 b" N/ s! ?  G6 i, Q& z; j0 K    {
6 H& M$ A7 ]" }3 _8 t8 Z        outfile << 'a';
3 v* y+ d4 `# A9 s% K3 y- a    }0 m$ |7 N' ^7 a/ h& |

1 R( @7 O( l! ~" b) T$ g    //暂停,按任意键继续
+ y+ U) _7 }! b   system("PAUSE");
2 R3 k3 G; a7 A2 r% A% T1 E. i2 ~& A& y: l# O+ W# I( d# P( u- c; C
    //继续向test.txt文件中写入字符’b’,也就是说,第4097个字符是’b’
; |+ x: X* ^: m: R% L& c8 U   outfile << 'b'; 0 u* g( n; T" c% t; R
6 A9 Y! J9 ~0 J9 a; Q7 r
    //暂停,按任意键继续
; n! S' E/ y* Q   system("PAUSE");
7 A5 e& i. c1 [% t, G6 ?
$ [% u; T  Z8 K/ c1 G$ b  return 0;
: f# Q* P- G5 M  V- W8 }}

1 c% X: m6 @- v$ v2 O3 Q( x9 i

编译并执行,运行结果如下:

微信图片_20200801223500.png

此时打开工程所在文件夹下的test.txt文件,您会发现该文件是空的,这说明4096个字符“a”还在缓冲区,并没有真正执行I/O操作。敲一下回车键,窗口变为如下:

微信图片_20200801223503.png

此时再打开test.txt文件,您就会发下该文件中已经有了4096个字符“a”。这说明全缓冲区的大小是4K(4096),缓冲区满后执行了I/O操作,而字符“b”还在缓冲区。

再次敲一下回车键,窗口变为如下:

微信图片_20200801223506.png   

此时再打开test.txt文件,您就会发现字符“b”也在其中了。这一步验证了文件关闭时刷新了缓冲区。

2、行缓冲

内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,假如放了10个字节的数据,你敲了回车键,那么程序就马上来读取了。假如放了20个字节,你敲了回车奖,程序也会来读取。所以即使1024个字节没有放满,但是你敲了回车键,程序就会来读取,这个就叫做行缓冲。

使用键盘操作演示行缓冲,先介绍getchar()函数。函数原型:

int getchar(void) ;6 E( t5 i0 ^9 G" d

说明:当程序调用getchar()函数时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()函数才开始从键盘缓冲区中每次读入一个字符。也就是说,后续的getchar()函数调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才重新等待用户按键。

#include<iostream>
7 p1 Y/ r; q5 R2 ?% O( fusing namespace std;
  P3 u* z- m( ?7 e: v
/ |& x4 s. G& @8 x+ S- r" U. Lint main() - L6 R' d& J0 `, Z. O/ ?
{  1 O& D. T- o- F7 V& S9 j

9 A% M4 p8 B3 _    char c;  
0 m1 E7 ]! Z% R* O) b  //第一次调用getchar()函数,程序执行时,您可以输入一串字符并按下回车键,按下回车键后该函数返回。返回值是用户输入的第一个字符 (假设用户输入了 abcdef,函数返回a)
/ o4 y5 u+ u+ T1 a: `# J( R8 R   c = getchar(); ' J' W( H% [. \1 U
    //显示getchar()函数的返回值  i& Q1 K1 q% G- Z5 V
   cout<< c << endl; // 输出 a  
* Z6 `( `. e& [8 D& H' A# j  // 循环多次调用getchar()函数,将每次调用getchar()函数的返回值显示出来,直到遇到回车符才结束。 这时函数执行不会让用户输入而是顺序读取缓冲区字符内容。第一个字符用户输入结束后已经读取,所以会从第二个字符开始读
2 ]+ F" x: c) W) G   while((c = getchar())!='\n')
5 `- E7 h! J/ I! U- k   { 5 G* X  g4 K/ [& z5 w8 W' Q4 e
       cout<< "," << c <<endl
$ j& b* y( F, R0 o' \  s$ b   }
& l8 g% p2 c9 x* t& j
4 t' d3 v0 L: [! l3 g   return 0;
& _# E. l; E) t$ T" Y}

6 O: N0 F/ x' Z- F( r

最后输出结果是

a ,b ,c ,d ,e ,f可以交替按下一些字符,编译结果如下:

微信图片_20200801223508.png

当按到第4096个字符时,提示您不能再按下去,说明行缓存的大小是4k,此时按下回车键,返回第一个字符是‘a’ 微信图片_20200801223511.png

继续敲下回车键,缓存区的其他字符就全部输出

微信图片_20200801223517.png 3、无缓冲

内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,刚放了1个字节,程序就马上来读取了;又放了一个字节,程序又马上来读取了,这就是没有缓冲。

在C语言中,一般规定是要有行缓冲的。但是使用scanf函数和getchar时,如果行缓冲的换行符没有处理好,程序运行可能会有异常或者闪退等现象。也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。

如错误输出时使用:

cerr<<”错误,请检查输入的参数!” ;8 C0 n/ _9 |5 o* U& t! r% X) _

这条语句等效于:

fprintf(stderr, ”错误,请检查输入的参数!”) ;
: e7 C' @: O2 e* X3 o& q/ N  I缓冲区的大小

如果我们没有自己设置缓冲区的话,系统会默认为标准输入输出设置一个缓冲区,这个缓冲区的大小通常是 512个字节 的大小。

缓冲区大小由 stdio.h 头文件中的宏 BUFSIZ 定义,如果希望查看它的大小,包含头文件,直接输出它的值即可:

printf("%d", BUFSIZ);
4 T; ~, z5 C% o* @8 B' n$ G

缓冲区的大小是可以改变的,也可以将文件关联到自定义的缓冲区,详情可以查看 setvbuf()和 setbuf() 函数。

缓冲区的刷新

下列情况会引发缓冲区的刷新:

  • 缓冲区满时;
  • 执行flush语句,即使用特定函数刷新缓冲区;
  • 执行endl语句,即行缓冲区遇到回车时;
  • 关闭文件。. d% q' h9 l/ J

可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区) 如:

cout << flush; //将显存的内容立即输出到显示器上进行显示% \7 T1 Q) F! J4 \  i, V. g" h7 k

endl控制符的作用是将光标移动到输出设备中下一行开头处,并且清空缓冲区。

cout < < endl;
, _6 ]4 L  G9 I- t6 K

相当于

cout < < ”\n”< < flush;! W* F. s* o; L& ^3 a
强制缓冲区的数字打印/* . v# a' f, X2 p% q5 i
输出缓冲区演示
1 ]6 X7 T2 d" U" Q*/ * L7 F. e6 X6 o8 l/ |: v: i
#include <stdio.h>+ e. h# T# q* B. ^; J* H
int main(){
2 X  }/ K& E. X. x5 z9 G$ L    printf("1\n"); " j7 i7 E$ z% B( l5 c" z
    fflush(stdout); //强制将输出缓冲区的内容显示在屏幕上 % w, n2 f( y% p3 V; B8 z
    while (1){
/ u  m& g) q9 k2 J7 ~    } ' z2 u" }4 t$ T7 h5 n4 \
    return 0;
( U% y. `# s$ x1 E4 K8 r% W}

% T, c9 k! w" X. u+ |  _3 \如何清空输入缓冲区的内容?
. T# s& Y& \- G% j$ Q1 l  C2 O6 ^

如果我想让getchar()每次都能够等待用户输入的话就要清空缓冲区,下面就介绍不同平台的方法

C标准规定 fflush()函数是用来刷新输出(stdout)缓存的。对于输入(stdin),它是没有定义的。但是有些编译器也定义了 fflush( stdin )的实现,比如微软的VC。其它编译器是否也定义了 fflush( stdin )的实现应当查找它的手册。GCC编译器没有定义它的实现,所以不能使用 fflush( stdin )来刷新输入缓存。

对于没有定义 fflush( stdin )的编译器,可以使用 fgets()函数来代替它(比用 getchar()、scanf()等函数通用性好)。可以这样忽略输入流中留下的回车等其它输入,从而使下一次的输入总是保持一个“干净”的状态。(这个是任何平台下都可以的)

char sbuf[1024];& F1 J4 ~% r! \  |! ?
// ...

& d9 P. Z9 c5 b( zfgets( sbuf, 1024, stdin );
  p* y) g) I0 D6 G+ ~' E// ...
$ |* G3 J7 M, J' Y# P1 b9 _

在windows 的vc下面就可以这样了:

for(int i=0;i<10;++i)
, F5 G9 s. D7 ?{# U6 Y: a5 i0 C+ D
    char ch=getchar();
+ y+ y' m3 H" A    fflush(stdin); //每次都会有等待状态了1 ~' ^) K+ `+ n9 p
}
( W$ q# R* v) S' O7 `

这里说到gcc编译器没有定义fflush的实现,我们一般用getchar();来清除缓冲区

#include <stdio.h>- D9 G$ g6 u0 T1 B% D/ K5 X) }3 ^% j
main()
, c7 }. F+ h% G{6 O" e: r  t8 G2 J
    char c;5 I" v5 U, j# ~1 B
   for(;(c=getchar())!='a';)5 l. U* q  C% f2 n- _5 x4 ?- Q
   printf("%c",c);
% n, c9 L- I/ }; H; r8 f   getchar();
' I0 T& Y3 |( P$ z( t( u! t' A* ^    c=getchar();( B- V1 z% d1 M& z
   printf("%c",c);
! w/ L2 s2 W9 c$ d5 ?}
9 Q2 J* u# I( J' y/ k6 G2 n2 w

输入: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

最后

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

赞 收藏 评论0 发布时间:2020-8-1 22:44

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版