您的位置:首页 > 产品设计 > UI/UE

APUE学习笔记——标准I/O

2015-11-20 20:58 459 查看
  今天我们围绕标准I/O做一些详细的讨论。

  首先,我们先来看一些重要的概念。

流和文件指针

  文件I/O操作都是针对文件描述符进行的,相对的,标准I/O的操作都是围绕一种叫做流(stream)的东西进行的,当使用标准 I/O 库打开或创建一个文件时,我们就已使一个流与这个文件相关联,通过流的读入和输出完成所需要的 I/O操作。

  用fopen打开一个流会返回一个指向FILE对象的指针,即文件指针,FILE对象通常是一个结构,包含了标准I/O库为管理该流需要的所有信息,如用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区长度、出错标志等。

  系统为每个进程预定义了3个可以自动被使用的流:标准输入、标准输出和标准错误,这3个标准I/O流通过预定义的文件指针stdin、stdout、stderr加以引用。

缓冲

  标准I/O库提供缓冲的目的是尽可能减少使用read和write系统调用的次数,缓冲类型有三种:

  (1)全缓冲:填满标准I/O缓冲区后才进行实际I/O操作,磁盘中的文件通常是全缓冲,术语冲洗(flush)说明标准I/O缓冲区的写操作。

  (2)行缓冲:在输入和输出中遇到换行符或行缓冲区被填满时执行实际I/O操作,当流涉及一个终端(如标准输入和标准输出)时,通常使用行缓冲。

  (3)不带缓冲:标准I/O不对字符进行缓冲存储,标准错误stderr通常默认为不带缓冲的。

ISO C规定

  当且仅当stdin和stdout不指向交互式设备时,他们才是全缓冲。

  stderr决不会是全缓冲。

Linux系统默认

  stderr是不带缓冲的。

  若流指向终端设备,则是行缓冲,否则为全缓冲。

  

  对于一个给定的已经打开且尚未执行任何操作的流,我们可以调用setbuf和setvbuf来更改系统默认的缓冲类型。

#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);

/*返回值:若成功。返回0;若出错,返回非0*/


  我们来看一下这两个函数的区别:

  setbuf只提供两种功能——打开或关闭由第一个参数fp指定的流的缓冲机制。若为打开缓冲,第二个参数buf必须指向一个长度为BUFSIZ的缓冲区,BUFSIZ定义在stdio.h中,至于调用函数之后究竟是全缓冲还是行缓冲,那就取决于该流是与终端相关还是与文件相关了;若想关闭该流的缓冲,只需把buf设为NULL即可。

  setvbuf功能就要强大一些,它多了两个参数mode和size,不仅可以实现setbuf的功能,还可以在打开缓冲时由mode指定具体的指定缓冲类型,由size指定缓冲区的长度。

mode参数缓冲类型
_IOFBF全缓冲
_IOLBF行缓冲
_IONBF不带缓冲
  关于这两个函数更详细的动作,可以看下下面这张表,这里就不再多费口舌了。

  


关键字restrict:

  大家可能都注意到了,这两个函数的参数中都带有restrict这个关键字,查了下发现是C99新增的,它只用于限定指针,作用是告诉编译器,所有修改该指针所指向内容的操作都必须通过该指针进行,而不能通过其它途径(其它变量或指针)来修改。这样做的好处是能帮助编译器进行更好的代码优化,生成更有效率的汇编代码。

  

  接下来,我们围绕对流的操作介绍一些函数,包括打开和关闭流、读和写流。

打开和关闭流

1、打开流

  以下三个函数可以打开一个标准I/O流。

#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type,
          FILE *restrict fp);
FILE *fdopen(int fd, const char *type);

/*三个函数返回值:若成功,返回文件指针;若失败,返回NULL*/


  三个函数的区别:

  (1)fopen:打开路径名为pathname的一个指定文件

  (2)freopen:在一个指定的流上打开一个指定的文件,该函数一般用于将一个指定的文件打开为一个预定义的流,也就是前面说的标准输入、标准输出和标准错误。

  (3)fdopen:读取一个现有的文件描述符,并使一个标准I/O流与其结合,常用于由创建管道和网络通信通道函数返回的描述符。

  其中,type参数可以用来指定对该流的读写方式,如下图所示:



2、关闭流

#include <stdio.h>
int fclose(FILE *fp);
/*返回值:若成功,返回0;若失败,返回EOF*/


  当我们使用完一个文件之后,需要调用 fclose函数关闭该文件、释放相关的资源,否则会造成内存泄漏,但在文件关闭前,系统还会进行一些操作,如冲洗缓冲区中输出数据、丢弃缓冲区中的所有输入数据、释放为流分配的缓冲区等。

读和写流

  读写操作分成两大类:非格式化I/O、格式化I/O

  非格式化I/O又包括三种:每次一个字符I/O、每次一行I/O、每次一个结构I/O。 

(一)非格式化I/O

1、每次一个字符的I/O

#include <stdio.h>

/*输入函数*/
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);   /*等同于getc(stdin)*/

/*三个函数返回值:若成功,返回下一个字符;若已到达文件尾或出错,返回EOF*/

/*对应的输出函数*/
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);

/*三个函数返回值:若成功,返回c;若出错,返回EOF*/


  关于三个输入函数的返回值,注意这样一句话:若已到达文件尾或出错,返回EOF,所以如果想知道是哪一种情况,可以调用ferror或feof进行判断。

#include <stdio.h>

int ferror(FILE *fp);
int feof(FILE *fp);

/*两个函数返回值:若条件为真,返回真;否则,返回假*/


  那么,ferror和feof是怎么知道条件是否为真呢?原来系统为每个流在FILE对象中维护了两个标志:出错标志和文件结束标志,所以当把FILE指针作为参数传递给这两个函数时,就可以通过这两个标志来判断条件是真还是假了。

2、每次一行I/O

#include <stdio.h>

/*buf:缓冲区地址; n:缓冲区长度; fp:指定的流*/
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);

/*两个函数返回值:若成功,返回buf;若已到达文件尾或出错,返回NULL*/

int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);

/*两个函数返回值:若成功,返回非负值;若出错,返回EOF*/


  我们先来分析两个输入函数:

  fgets和gets都指定了缓冲区的地址,读入的行送入其中,只不过gets从标准输入读,而fgets从指定的流读而已。

  gets函数并不检查输入行的长度是否超过缓冲区长度,因此有缓冲区溢出的危险,历史上的蠕虫病毒就是利用这个漏洞做的,所以gets一般不推荐使用。fgets弥补了gets的缺点,我们必须给它指定缓冲区长度n,当输入行长度超过缓冲区长度时就会出错。

  关于fgets、缓冲区长度、输入行长度要注意一个小问题:

  fgets读取输入行直到遇到换行符,注意fgets也读入换行符,而缓冲区总以null字节结尾,所以输入行包括换行符在内的字符数不能超过n-1,也就是说除换行符外的实际字符数最多为n-2,否则fgets返回一个不完整的行,该行的剩余部分会在下一次调用fgets时接着读取。

  测试代码

  

#include <stdio.h>
#include <stdlib.h>

#define MAXLINE 20

int main()
{
char buf[MAXLINE];

if (fgets(buf, MAXLINE, stdin) != NULL)
if (fputs(buf, stdout) == EOF)
printf("output error\n");

if (ferror(stdin))
printf("input error\n");

exit(0);
}


运行结果可看出,缓冲区长度20,第一次输入18个字符,fgets将这些字符与换行符一起读入,fputs输出有换行;第二次输入19字符,fgets没有读入换行符,fpus输出无换行。



  分析完了输入,咱们再来看输出。

  puts将一个字符串写到标准输出,尾部终止符’\0’不写出,随后puts会补一个换行符到标准输出,也就是说puts会自动换行。

  fputs将一个字符串写到指定的流,尾部终止符’\0’不写出,但并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非NULL字节

3、二进制I/O(每次读写一个结构)

  该方式可一次读写一个完整的结构,通过fread和fwrite实现。

#include <stdio.h>

size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);

sizt_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);

/*两个函数返回值:读或写的对象数*/
/*对于读,若出错或到达文件尾,返回值可少于nobj,需调用ferror或feof判断是哪一种*/
/*对于写,若返回值少于要求的nobj,则为出错*/


  这两个函数有以下常见用法:

  (1)读或写一个二进制数组:例如将一个数组的第2~5个元素写到一文件上,可以使用如下代码。其中,size为每个数组元素的长度,nobj为欲写的元素个数

int data[10];

if (fwrite(&data[1], sizeof(int), 4, fp) != 4)
printf("fwrite error\n");


  (2)读或写一个结构:如下代码,其中,size为结构的长度,nobj为1(要写的结构体个数)

struct
{
short count;
long total;
char name[NAMESIZE];
} item;

if (fwrite(&item, sizeof(item), 1, fp) != 1)
printf("fwrite error\n");


  (3)将(1)(2)结合起来就可以读或写一个结构数组:代码如下,其中size是结构的sizeof,nobj是数组的元素个数。

/*定义一个结构数组*/
struct student
{
char name[20];
char sex[2];
int age;
char address[100];
} student[40];

if (fwrite(student, sizeof(struct student), 40, fp) != 40)
printf("fwrite error\n");


(二)格式化I/O

1、格式化输出

#include <stdio.h>

int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);

int sprintf(char *restrict buf, const char *restrict format, ...);
int snprintf(char *restrict buf, size_t n,
       const char *restrict format, ...);


  我们来比较一下这5个函数:

  printf将格式化数据写到标准输出,注意它的返回值是成功打印的字符数(不包括\0字符)

  fdprintf将格式化数据写到指定的流,dprintf写到指定的文件描述符,这两个函数的返回值也是成功打印的字符数(不包括\0字符);

  sprintf将格式化数据写到数组buf中,并在数组尾端自动加一个\0,若成功,函数返回写入数组的字符数(不包括为数组自动添加的\0),否则返回负值。

  与gets类似,sprintf也有可能造成缓冲区溢出,所以有了snprintf,需要显示指定缓冲区长度n,超过缓冲区尾端的所有字符都被丢弃。如果n足够大,返回写入buf的字符数,否则返回负值。

  

2、格式化输入

#include <stdio.h>

int scanf(const char *restrict format, ...);
int fscasnf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(char *restrict buf, const char *restrict format, ...);


  这三个函数的区别和上面的printf函数族类似,大家比较着看下就行了,这里不再详细阐述了。

  最后再来看下面这个函数:

#include <stdio.h>
int fileno(FILE *fp);


  每个标准I/O流都有一个与其相关联的文件描述符,可以调用fileno获得其描述符并返回。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: