APUE学习笔记3——第五章——标准I/O库
学号:16340043
中山大学
本博客为《UNIX环境高级编程》的学习笔记,希望能对大家有所帮助
1.前面的废话
今天第二篇咯(过了国庆肯定做不到一天一更…一周能写两篇就不错了…)
2.博客正文
5.1 引言
本章讲述标准I/O库(终于有点熟悉的内容啦)
5.2 流和FILE对象
对于第三章中的I/O函数,所有的操作都是围绕着文件描述符的,而对于标准I/O库,它们的操作是围绕流进行的。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
流的定向决定了所读写的字符是单字节还是多字节的
只有两个函数可以改变流的定向。freopen函数(稍后讨论)清楚一个流的定向;fwide函数可用于设置流的定向:
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
其中:
- 根据mode参数的不同值,fwide函数执行不同的工作:
mode值 | 功能 |
---|---|
负 | 使指定的流字节定向 |
正 | 使指定的流宽定向 |
0 | 不设置流的定向,但返回该流定向的值 |
- fp:指向一个FILE对象的指针(文件指针)
- 返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;若流是未定向的,返回0
由于该函数没有出错返回,我们在使用它之前应把errno清除,使用后检查errno的值来判断流是否无效
5.3 标准输入、标准输出和标准错误
有3个流标准输入、标准输出和标准错误可以自动地被进程使用,通过文件指针stdin、stdout和stderr加以引用
5.4 缓冲
缓冲的目的使尽可能减少read和write的次数。标准I/O提供了以下3种类型的缓冲:
- 1)全缓冲。要填满缓冲区后才能进行实际I/O操作
-
2)行缓冲。当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。行缓冲有两个限制:
a.由于缓冲区的长度是固定的,所以只要填满了它,即使没有换行符,也会执行I/O操作
b.通过I/O库要求从一个不带缓冲的流,或者是一个行缓冲的流得到输入数据,那么会冲洗1所有行缓冲输出流 3)不带缓冲。不对字符进行缓冲存储。
很多系统默认使用下列类型的缓冲:
- 标准错误是不带缓冲的
- 若是指向终端设备的流,是行缓冲的;否则是全缓冲的
对任何一个给定的流,若我们不喜欢系统默认,可以用下列函数更改:
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
void setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
其中:
- fp:指向文件的指针
- 对于setbuf,若buf指向长度为BUFSIZE的缓冲区,设为全缓冲;若该流与终端设备相关,那么可能会被设置为行缓冲;若buf为NULL,则设为不带缓冲
- 对于setvbuf,可通过mode具体设定所需缓冲类型:
mode值 | 类型 |
---|---|
_IOFBF | 全缓冲 |
_IOLBF | 行缓冲 |
_IONBL | 不带缓冲 |
mode的优先级高于buf参数,若mode为带缓冲的,而buf为NULL那么系统会自动分配一个缓冲区给流
一般而言,应由系统自动分配缓冲区,这样在关闭流的时候缓冲区被自动释放
任何时候,我们都可以强制冲洗一个流:
#include <stdio.h>
int fflush(FILE *fp);
- 返回值:若成功,返回0;若出错,返回EOF
特殊情况:若fp为NULL,则所有输出流被冲洗
5.5 打开流
下列3个函数用来打开一个标准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);
三个函数区别如下:
- 1)fopen打开路径名为pathname的指定文件
- 2)freopen在一个指定流上打开一个指定文件。若流已经打开,则消除该流;若流已经定向,则清除该定向
- 3)fdopen使一个标准I/O流与一个文件描述符相结合
type有15种不同的值:
type | 说明 |
---|---|
r或rb2 | 为读而打开 |
w或wb | 把文件截至0长,或为写而创建 |
a或ab | 追加:为在文件尾写而打开,或为写而创建 |
r+或r+b或rb+ | 为读和写而打开 |
w+或w+b或wb+ | 把文件截至0长,或为读和写而打开 |
a+或a+b或ab+ | 在文件尾读和写而打开或创建 |
调用fclose关闭一个打开的流:
#include <stdio.h>
int fclose(FILE *fp);
在该文件被关闭之前,冲洗缓冲区中的输出数据,输入数据被丢弃
5.6 读和写流
一旦打开了流,可选择3种不同类型的I/O方式
- 1)每次一个字符的I/O
- 2)每次一行的I/O
- 3)直接I/O
先说输入函数(这一块就有交叉啦)
一次读一字符:
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
- 返回值:若成功,返回下一个字符;若出错或到达文件尾,返回EOF
- getchar()相当于getc(stdin)
- 前两个函数的区别是,前者可被实现为宏,而后者不行
为了区分上述返回值的-1是出错还是到达文件尾,必须调用ferror或feof
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
//两个函数在条件为真时返回非0;否则返回0
void clearerr(FILE *fp);
使用clearerr可以清除出错标志和文件尾标志
从流中读取数据后,可用ungetc将字符再压进流中
#include <stdio.h>
int ungetc(int c, FILE *fp);
- 返回值:若成功,返回c;若出错,返回EOF
回送的字符可以再读出(不知道这个函数有什么用…),但是读出顺序与压回顺序相反
不能回送EOF
最后是输出函数:
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c,FILE *fp);
int putchar(int c);
- 返回值:若成功,返回c;若出错,返回EOF
与输入函数之间的差别类似
5.7 每次一行的I/O
下面函数提供每次输入一行的功能:
#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
- 返回值:若成功,返回buf;若出错或达到文件尾,返回NULL
- gets3从标准输入读,fgets从指定的流读
- n:缓冲的长度(fgets读取n-1个字符,以null字节结尾)
下面函数提供每次输出一行的功能:
#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
- 返回值:若成功,返回非负值;若出错,返回EOF
- puts会在输出完后添加一个换行符
5.8 标准I/O的效率
以上几个函数在循环多次时,对内核提出的读写请求数基本相同,所以无需考虑缓冲及最佳I/O长度的选择。
使用每次一行I/O版本的效率约为每次一个版本的两倍。
5.9 二进制I/O
二进制I/O一次读或写一个完整的结构:
#include <stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
- 返回值:读或写的对象数
- ptr:指向数据的指针
- size:要读或写的元素的长度
- nobj:要读或写的元素的个数
二进制I/O只能用来读同一系统上已写的数据
在不同的系统间交换二进制数据的实际方法是使用互认的规范格式
5.10 定位流
有三种方法可以定位标准I/O流
1)ftell和fseek函数
#include <stdio.h>
long ftell(FILE *fp);
//返回值:若成功,返回当前文件位置指示;若出错,返回-1L
int fseek(FILE *fp, long offset, int whence);
//返回值:若成功,返回0;若出错,返回-1
void rewind(FILE *fp);
- ftell用于二进制文档时,返回当前位置距起始位置的字节数
- whence的值与3.6中fseek函数的相同
- offset:偏移量。作用于文本文档时,文件当前位置不能简单地用偏移量来度量,此时offset只能有两种值:0或ftell返回的值
- rewind函数用于将一个流设置到文件的起始位置
2)除了偏移量的类型是off_t而非long以外,ftello函数与ftell相同,fseeko函数与fseek相同
3)fgetpos和fsetpos函数
#include <stdio.h>
int fgetpos(FILE *fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);
- 返回值:若成功,返回0;若出错,返回非0
- fpos_t:记录文件位置的数据类型
5.11 格式化I/O
格式化输出是由5个printf函数来处理的:
#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, ...);
//返回值:若缓冲区够大,返回将要存入数组的字符数;若编码出错,返回负值
printf将格式化数据写至标准输出,fprintf写至指定的流,dprintf写至指定的文件描述符,sprintf将格式化字符送入数组buf中,并加上一个null字节4。
- snprintf中的n指定了最多读取的字符数,再多的被丢弃
至于怎么格式化输出(%d那一套东西),我就不记下来了,学过C的都知道
对,还有5中printf族的变体,在前面5个函数的前面加上一个v5,区别是可变参数(…)列表被替换成arg
格式化输入是由3个scanf函数来执行的:
#include <stdio.h>
int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format, ...);
- 返回值:赋值的输入项数;若输入出错或在任一转换前已达到文件尾端,返回EOF
scanf族也有前面加v的3个scanf,区别与printf的的类似
5.12 实现细节6
可以对一个流调用fileno函数以取得其描述符
#include <stdio.h>
int fileno(FILE *fp);
-返回值当然是文件描述符啦
5.13 临时文件
ISO C 标准I/O库提供了两个函数以帮助创建临时文件:
#include <stdio.h>
char *tmpnam(char *ptr);
//返回值:指向唯一路径名的指针
FILE *tmpfile(void);
//返回值:若成功,返回文件指针;若失败,返回NULL
- tmpnam函数产生一个与现有文件名不同的有效路径名字符串,最多调用次数为TMP_MAX7
- 若ptr为NULL,所产生的路径名放在一个静态区中,指向该静态区的指针作为函数值返回,后续调用tmpnam时,会重写该静态区,所以我们应该保存路径名的副本而不是指针的副本
- tmpfile创建一个二进制文件(UNIX中不区分)
函数mkdtemp和mkstemp可处理临时文件:
#include <stdlib.h>
char *mkdtemp(char *template);
//返回值:若成功,返回指向目录名的指针;若出错,返回NULL
int mkstemp(char *template);
//返回值:若成功,返回文件描述符;若出错,返回-1
其实这两个函数更像是创建随机文件名的非临时文件……(我的理解)只是访问权限略有不同。
5.14 内存流
内存流虽仍使用FILE进行访问,但其实没有底层文件
有三个函数可用于内存流的创建,第一个是fmemopen函数:
include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
- 返回值:若成功,返回流指针;若出错,返回NULL
fmemopen函数允许调用者提供缓冲区用于内存流
- buf:缓冲区开始位置
- size:缓冲区大小的字节数
- type:如何使用流:
type | 说明 |
---|---|
r或rb8 | 为读而打开 |
w或wb | 为写而打开 |
a或ab | 追加:为在第一个null字节处写而打开 |
r+或r+b或rb+ | 为读和写而打开 |
w+或w+b或wb+ | 把文件截至0长,为读和写而打开 |
a+或a+b或ab+ | 追加:为在第一个null字节处读和写而打开 |
- 这个type与5.5中有微小的差别
1)无论何时以追加模式打开内存流,当前文件位置设为缓冲区中第一个null字节,若缓冲区中不存在null字节,那么当前位置就是缓冲区结尾的后一个字节
2)若buf参数是null指针,打开流进行读写没有任何意义
3)任何时候需要增加流缓冲区中的数据量,以及调用fclose、fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节
用于创建内存流的其他两个函数是open_memstream和open_wmemstream:
#include <stdio.h>
FILE *open_memstream(char **bufp, size_t *sizep);
#include <wchar.h>
FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);
- 返回值:若成功,返回流指针;若出错,返回NULL
- 从名字可以看出,带w的是面向宽字节的
这两个函数与前两个的区别是:
- 1)创建的流只能写打开
- 2)不能指定自己的缓冲区,但能通过两个参数访问缓冲区地址和大小
- 3)关闭流后要自行释放缓冲区
- 4)对流添加字节会增加缓冲区大小
因为避免了缓冲区溢出,内存流非常适用于创建字符串。
5.15 标准I/O的替换软件
标准I/O库并不完善
一个不足之处是效率不高,因为要复制的次数较多(内核->标准I/O缓冲区->行缓冲区)
替代品有:快速I/O库、sfio、ASI等
5.16 小结
大多数UNIX应用程序都使用标准I/O库
应该看到,标准I/O库使用了缓冲技术,而它正是产生很多问题、引起很多混淆的部分