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

来源:互联网 发布:海关数据编码查询 编辑:程序博客网 时间:2024/05/20 17:24

  今天我们围绕标准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 20int 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获得其描述符并返回。

0 0