现在,我们从C程序员最熟悉的printf函数开始学习I/O流。

我们对printf函数一直是很喜爱的。至少,当我们第一次向C语言的世界问好的时候,我们还是很感激它的。但是,额,但是,printf函数比起我们以为的要复杂的多,除去printf复杂的语法知识,其实,printf也没那么的安全。但是,这些都不是本文的重点,目前,我们关注的是C语言的I/O流。

printf函数会直接将其格式化参数打印到我们的屏幕,这是hello world的全部解释,但是,还不够,我们来深入了解下,我们的屏幕输出的在标准里称为标准输出stdout,而从屏幕输入的叫做标准输入stdin。这两家伙在标准库里是固定的写法,不能修改。

以下是这两者的声明(mgwin):

 extern FILE _iob[];	/* A pointer to an array of FILE */
#define __iob_func() (_iob)
#define stdin (&__iob_func()[0])
#define stdout (&__iob_func()[1])
#define stderr (&__iob_func()[2])

可见,stdin、stdout、stderr都是FILE* 类型的指针,他们占据了这个数组的0,1,2三个序号,所以,他们的地位和我们一般的文件流操作一样,没什么了不起的。(另外,补充一下stderr也会直接打印到屏幕)

明确了输出流的地位,我们就可以搞点事情了。

要弄明白I/O流,就要知道I流和O流都对应了哪些操作,以及二者的区别

一、I流(input流)

1.格式化I流

我们最清楚的I流操作应该就是scanf函数了,当初,我无数次的少写&符号,导致了崩溃,人也很崩溃,格式化的I流 都是使用的scanf家族函数:

int fscanf(FIlE* stream,char const *format,...);
int scanf(char const *format,...);
int sscanf(char const *string,char const *format,...);

这些函数都是从输入源读取字符并根据format字符串给出的格式化代码对它们进行转换,fscanf的输入源是作为参数给出的流,scanf从标准流stdio中读取,而sscanf则从第一个参数所给出的字符串中读取字符。

当格式化字符串到达末尾或者读取的输入不再匹配格式字符串所指定的类型时,输入就停止,在任何一种情况下,被转换的输入值的树木作为函数的返回值返回,如果在任何输入值被转换之前文件就已到达尾部,函数就返回常量值EOF。

警告:现在,解释下 为何一定要在scanf家族参数前加一个&。由于C的传值参数传递机制,把一个内存位置作为参数传递给函数的唯一方法就是传递一个指向该位置的指针。在使用如scanf时,省略&符号将导致这个变量的值作为参数传递给函数,而scanf却将其解释成指针,当将它解引用时,或者导致程序终止,或者导致一个不可预料的内存位置的数据被改写,所以一定要杜绝这种错误。

下面举例子:

   while(fgets(buff,1024,stdin) != NULL)
{
if(5 != sscanf(buff,"%1d%1d%1d%1d%1d",&a,&b,&c,&d,&e))
{
fprintf(stderr,"Bad input skipped: %s",buff);
continue;
}
printf("a %d b %d c %d d %d e %d\n",a,b,c,d,e);
}
    if(5!= fscanf(stdin,"%d %d %d %d %d",&a,&b,&c,&d,&e))
{
fprintf(stderr,"Bad input skipped: %s",buff);
}
printf("a %d b %d c %d d %d e %d\n",a,b,c,d,e);

总体而言,sscanf比fcanf使用面更广,需要其他例子可以参考网络。

2.非格式化I流

非格式化I流api就更多了,比如:

 int getchar(void)
int getc(FILE * stream)
//gets 不使用了
int fgetc(FILE *stream);
char *fgets(char *s, int size, FILE *stream);

备注:除了getc外没有加f的都是只能操作标准stdin,其中gets函数已经禁止使用,当然,如果你要用后果自负咯。要想知道原因也请百度下。另外:fgetc和getc最大的区别在前者是函数,后者是宏,getc由fgetc通过宏实现,调用的时候注意参数stream不能是有副作用的表达式。I流为字符时,其返回值都是int类型,这一点也需要注意,因为EOF存在的缘故。

二、O流(output)

1.格式化O流

ok,下面回到printf函数家族,格式化O流油以下API:

 int fprintf(FILE *stream,char const* format,...);
int printf(char const *format,...);
int sprintf(char *buffer,char const *format,...);
int snprintf(char *str, size_t size, const char *format, ...);

printf家族函数根据格式代码个format参数中的其他字符对参数列表中的值进行格式化。使用printf是将结果输出到标准输出stdout,使用fprintf是设定的任意的输出流,而sprintf把他的结果作为一个以NUL结尾的字符串存储到指定的buffer缓存区中,而不是写入到某个流中。返回值是 实际打印或者存储的字符数。

又,因为sprintf没有限定缓冲区大小的参数,所以在使用中可能会造成缓存溢出,所以如果要推荐的化,请使用snprintf函数。

关于使用方法请 看代码:

  • sprintf 可以方便的将其他形式的类型转换成字符串,
char buf[100] = {0x0};
int a = 3,b =4;
sprintf(buf,"%d%d",a,b);

那么buf 就是字符串“34”;

  • fprintf可以方便地将字符串写入到文件中
FILE *fp = fopen("a.txt","w");
fprintf(fp,buf);

2.非格式化O流

api如下:

 int putchar(int character);
int putc(int character,FILE* stream);
int fputc(int character,FILE *stream);
int fputs(char const *buffer,FILE *stream);
int puts(char const *buffer);

备注:除了putc,没有f开头的api都只作用于stdout。

三、I/O缓存

在文章开头,我们说明了一件事,即所有的标准库的IO流操作都是基于FILE*指针类型的。即使没有明确指出,也是因为其默认为stdin或者stdout,而这两个类型也是FILE*的。

为了分析C语言的I/O缓存机制,我们来看看FILE的定义

 struct _iobuf {
char *_ptr; //文件输入的下一个位置
int _cnt; //当前缓冲区的相对位置
char *_base; //指基础位置(即是文件的起始位置)
int _flag; //文件标志
int _file; //文件的有效性验证
int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取
int _bufsiz; //文件的大小
char *_tmpfname; //临时文件名
};
typedef struct _iobuf FILE;

绝大多数流是完全缓冲的,这意味着”读取“和”写入“实际上是从一块被称为缓冲区的内存块区域来回复制数据。从内存中来回复制数据是非常快速的,用于输出流的缓冲区只有当它写满时才会被刷新到设备或文件中。一次性把写满的缓冲区写入和逐片把程序产生的输出分别写入相比 ,效率更高。类似,输入缓存区当它为空时从通过设备或者文件读取下一块较大的输入,重新填充缓存区

使用标准输入和输出时,这种缓冲可能会引起混淆,所以,只有当操作系统可以断定它们与交互设备并无联系时才会进行完全缓冲,否则,它们的缓存状态将因编辑器而差异,一个常见的策略是吧标准输出和标准输入联系在一起,就是当请求输入时同时刷新输出缓冲区。这样,在用户必须进行输入前,提示用户进行输入的信息和以前写入到输出缓冲区中的内容将出现在屏幕上。

改变缓冲区及其方式

在流上执行的缓冲方式有时也许并不合适,下面两个函数可以用来对缓冲区方式进行修改。这两个函数只有当指定的流被打开但还没有在它上面执行任何操作前才能被调用。

 void setbuf(FILE* stream,char*buf);
int setvbuf(FILE *stream,char *buf,int mode,size_t size);

setbuf设定了一个数组来缓冲流,这个数组的字符长度必须为BUFSIZ(在stdio.h中定义好了)。如果使用NULL调用该函数,setbuf将关闭流的所有的缓冲方式,

而setvbuf更加通用,使用它,可以指定一个并非标准长度的缓冲区,也可以选择希望缓冲的缓冲方式:全缓冲、行缓冲(遇到行就刷新)或者不缓冲。

说了半天缓冲,那么刷新呢,使用函数fflush就能即刻刷新缓冲区。

四、其他知识点

  • 流分为两种类型:文本(text)和二进制(binary)流,
  • 使用二进制写入二进制数据 比使用字符I/O效果更高。
05-08 14:57