APUE学习笔记——标准I/O
2015-11-20 20:58
459 查看
今天我们围绕标准I/O做一些详细的讨论。
首先,我们先来看一些重要的概念。
用fopen打开一个流会返回一个指向FILE对象的指针,即文件指针,FILE对象通常是一个结构,包含了标准I/O库为管理该流需要的所有信息,如用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区长度、出错标志等。
系统为每个进程预定义了3个可以自动被使用的流:标准输入、标准输出和标准错误,这3个标准I/O流通过预定义的文件指针stdin、stdout、stderr加以引用。
(1)全缓冲:填满标准I/O缓冲区后才进行实际I/O操作,磁盘中的文件通常是全缓冲,术语冲洗(flush)说明标准I/O缓冲区的写操作。
(2)行缓冲:在输入和输出中遇到换行符或行缓冲区被填满时执行实际I/O操作,当流涉及一个终端(如标准输入和标准输出)时,通常使用行缓冲。
(3)不带缓冲:标准I/O不对字符进行缓冲存储,标准错误stderr通常默认为不带缓冲的。
ISO C规定:
当且仅当stdin和stdout不指向交互式设备时,他们才是全缓冲。
stderr决不会是全缓冲。
Linux系统默认:
stderr是不带缓冲的。
若流指向终端设备,则是行缓冲,否则为全缓冲。
对于一个给定的已经打开且尚未执行任何操作的流,我们可以调用setbuf和setvbuf来更改系统默认的缓冲类型。
我们来看一下这两个函数的区别:
setbuf只提供两种功能——打开或关闭由第一个参数fp指定的流的缓冲机制。若为打开缓冲,第二个参数buf必须指向一个长度为BUFSIZ的缓冲区,BUFSIZ定义在stdio.h中,至于调用函数之后究竟是全缓冲还是行缓冲,那就取决于该流是与终端相关还是与文件相关了;若想关闭该流的缓冲,只需把buf设为NULL即可。
setvbuf功能就要强大一些,它多了两个参数mode和size,不仅可以实现setbuf的功能,还可以在打开缓冲时由mode指定具体的指定缓冲类型,由size指定缓冲区的长度。
关于这两个函数更详细的动作,可以看下下面这张表,这里就不再多费口舌了。
关键字restrict:
大家可能都注意到了,这两个函数的参数中都带有restrict这个关键字,查了下发现是C99新增的,它只用于限定指针,作用是告诉编译器,所有修改该指针所指向内容的操作都必须通过该指针进行,而不能通过其它途径(其它变量或指针)来修改。这样做的好处是能帮助编译器进行更好的代码优化,生成更有效率的汇编代码。
接下来,我们围绕对流的操作介绍一些函数,包括打开和关闭流、读和写流。
以下三个函数可以打开一个标准I/O流。
三个函数的区别:
(1)fopen:打开路径名为pathname的一个指定文件
(2)freopen:在一个指定的流上打开一个指定的文件,该函数一般用于将一个指定的文件打开为一个预定义的流,也就是前面说的标准输入、标准输出和标准错误。
(3)fdopen:读取一个现有的文件描述符,并使一个标准I/O流与其结合,常用于由创建管道和网络通信通道函数返回的描述符。
其中,type参数可以用来指定对该流的读写方式,如下图所示:
2、关闭流
当我们使用完一个文件之后,需要调用 fclose函数关闭该文件、释放相关的资源,否则会造成内存泄漏,但在文件关闭前,系统还会进行一些操作,如冲洗缓冲区中输出数据、丢弃缓冲区中的所有输入数据、释放为流分配的缓冲区等。
非格式化I/O又包括三种:每次一个字符I/O、每次一行I/O、每次一个结构I/O。
(一)非格式化I/O
1、每次一个字符的I/O
关于三个输入函数的返回值,注意这样一句话:若已到达文件尾或出错,返回EOF,所以如果想知道是哪一种情况,可以调用ferror或feof进行判断。
那么,ferror和feof是怎么知道条件是否为真呢?原来系统为每个流在FILE对象中维护了两个标志:出错标志和文件结束标志,所以当把FILE指针作为参数传递给这两个函数时,就可以通过这两个标志来判断条件是真还是假了。
2、每次一行I/O
我们先来分析两个输入函数:
fgets和gets都指定了缓冲区的地址,读入的行送入其中,只不过gets从标准输入读,而fgets从指定的流读而已。
gets函数并不检查输入行的长度是否超过缓冲区长度,因此有缓冲区溢出的危险,历史上的蠕虫病毒就是利用这个漏洞做的,所以gets一般不推荐使用。fgets弥补了gets的缺点,我们必须给它指定缓冲区长度n,当输入行长度超过缓冲区长度时就会出错。
关于fgets、缓冲区长度、输入行长度要注意一个小问题:
fgets读取输入行直到遇到换行符,注意fgets也读入换行符,而缓冲区总以null字节结尾,所以输入行包括换行符在内的字符数不能超过n-1,也就是说除换行符外的实际字符数最多为n-2,否则fgets返回一个不完整的行,该行的剩余部分会在下一次调用fgets时接着读取。
测试代码
运行结果可看出,缓冲区长度20,第一次输入18个字符,fgets将这些字符与换行符一起读入,fputs输出有换行;第二次输入19字符,fgets没有读入换行符,fpus输出无换行。
分析完了输入,咱们再来看输出。
puts将一个字符串写到标准输出,尾部终止符’\0’不写出,随后puts会补一个换行符到标准输出,也就是说puts会自动换行。
fputs将一个字符串写到指定的流,尾部终止符’\0’不写出,但并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非NULL字节。
3、二进制I/O(每次读写一个结构)
该方式可一次读写一个完整的结构,通过fread和fwrite实现。
这两个函数有以下常见用法:
(1)读或写一个二进制数组:例如将一个数组的第2~5个元素写到一文件上,可以使用如下代码。其中,size为每个数组元素的长度,nobj为欲写的元素个数
(2)读或写一个结构:如下代码,其中,size为结构的长度,nobj为1(要写的结构体个数)
(3)将(1)(2)结合起来就可以读或写一个结构数组:代码如下,其中size是结构的sizeof,nobj是数组的元素个数。
(二)格式化I/O
1、格式化输出
我们来比较一下这5个函数:
printf将格式化数据写到标准输出,注意它的返回值是成功打印的字符数(不包括\0字符);
fdprintf将格式化数据写到指定的流,dprintf写到指定的文件描述符,这两个函数的返回值也是成功打印的字符数(不包括\0字符);
sprintf将格式化数据写到数组buf中,并在数组尾端自动加一个\0,若成功,函数返回写入数组的字符数(不包括为数组自动添加的\0),否则返回负值。
与gets类似,sprintf也有可能造成缓冲区溢出,所以有了snprintf,需要显示指定缓冲区长度n,超过缓冲区尾端的所有字符都被丢弃。如果n足够大,返回写入buf的字符数,否则返回负值。
2、格式化输入
这三个函数的区别和上面的printf函数族类似,大家比较着看下就行了,这里不再详细阐述了。
最后再来看下面这个函数:
每个标准I/O流都有一个与其相关联的文件描述符,可以调用fileno获得其描述符并返回。
首先,我们先来看一些重要的概念。
流和文件指针
文件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获得其描述符并返回。
相关文章推荐
- UIDatePicker 日期滚轮)/时间选取器
- 提高Interface Builder高效工作的8个技巧
- UIImagePickerController 的部分用法详解
- leetcode-implement-queue-using-stacks
- iOS中UIMenuController的使用
- UIScrollView与UIPageControl+自动滑动的封装
- ios UILocalNotification的使用
- Qt .ui转换为.h文件
- STL容器-序列式容器deque
- Android——使用Handle和Message更新UI控件
- iOS开发中UIViewcontentMode的12种属性值
- ios的手势UIGestureRecognizer
- Android中UI线程与后台线程交互设计的5种方法
- 【iOS功能实现】之利用UIDocumentInteractionController打开和预览文档
- Leetcode: Binary Tree Longest Consecutive Sequence
- openstack message queue
- UILable上如何添加背景图片
- 动画特效十八:粘性动画2
- UIScrollView的delegate方法妙用之让UICollectionView滑动到某个你想要的位置
- ios--uitextfield动态限制输入的字数