5.2 流和对象
只有两个函数可以改变流的定向: freopen函数清楚一个流的定向,fwide函数可用于设置一个流的定向。
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
return: 若流是宽定向的,返回正值,若是字节定向的,放回负值;若是未定向的,返回0.
注意:它没有出错返回,所以调用前先清楚errno,从fwide返回后在检查errno值。
#ifndef _FILE_DEFINED
struct _iobuf {
char *_ptr; //文件输入的下一个位置
int _cnt; //当前缓冲区的相对位置
char *_base; //指基础位置(即是文件的起始位置)
int _flag; //文件标志
int _file; //文件描述符id
int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取
int _bufsiz; //文件缓冲区大小
char *_tmpfname; //临时文件名
};
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif
5.3 标准输入、标准输出和标准错误
为一个进程定义了三个文件流: stdin,stdout,stderr(包含在<stdio.h>
),而文件描述符有对应的三个:
STDIN_FIENO,STDOUT_FILENO,STDERR_FILE(包含在<unistd.h>
中)
5.4 缓冲
1、标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。
2、标准I/O提供了三种类型缓冲:
- 全缓冲: 在填满标准I/O缓冲区后才进行实际I/O操作。
* 行缓冲: 在输入和输出中遇到换行符时才进行I/O操作。
- 不带缓冲: 标准I/O库不对字符进行换从存储。
3、术语冲洗(flush)说明标准I/O缓冲区的写操作
在UNIX环境中,flush由两种意思
- (1)在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。
- (2)在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中的数据。
4、对于行缓冲有两个限制:
- (1)因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行I/O操作。
- (2)任何时候只要通过标准I/O库要求从(a),(b)得到输入数据,那么就会冲洗所有行缓冲输入流。
(a)一个不带缓冲的流
(b)一个行缓冲的流(它从内核请求需要数据)
5、标准错误流stderr通常是不带缓冲的
6、ISO C要求缓冲的特征:
- 当且仅当标准输入和标准输出并不指向交互式设备时,他们才是全缓冲的。
- 标注错误标准错误绝不会时全缓冲的
7、很多系统默认使用下列类型的缓冲:
- 标准错误是不带缓冲的
- 若是指向终端设备的流,则是行缓冲的;否则时全缓冲的。
8、对任何一个给定的流,如果自己想定自己的缓冲类型,可用
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
return: 0; error: !0
(1) 使用setbuf函数打开或关闭缓冲机制。为了带缓冲进行I/O,参数buf必须指向一个长度为BUFSIZ(定义在
#include <stdio.h>
int fflush(FILE *fp);
return: 0; error: EOF。
此函数使该流所有未写的数据都被传送至内核,作为一种特殊情形,如若fp是NULL,则此函数将导致所有输出流被冲洗。
5.5 打开流
1、下列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);
FILE *fdopen(int fd, const char *restrict type);
3个函数的返回值:若成功,返回文件指针;若出错,返回NULL。
3个函数的区别如下:
- (1)fopen函数打开路径名为pathname的一个指定的文件
- (2)freopen函数在一个指定的流上打开一个指定的文件,如若流已经打开,则先关闭该流。
若该流已经定向,则使用freopen清楚该定向。 - (3)fdopen函数取一个已有的文件描述符,并使一个标准的I/O流与该描述符相结合。
2、type参数指定对该I/O流的读、写方式。
3、对于fdopen,type参数的已有稍有区别。
- (1)因为该描述符已被打开,所以fdopen为写而打开并不截断该文件。fdopen函数不能截断它为写打开的任一文件。
- (2)另外,标准I/O追加写方式也不能用于创建文件(因为一个描述符引用一个文件,则该文件一定存在)。
4、当读和写类型打开一个文件时,(type中+号),具有下列限制
- (1)如果中间没有 fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。
- (2)如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
5、打开一个流的6种不同的方式:
6、注意,在指定w或a类型创建一个新文件时,我们无法说明该文件的访问权限位。可以通过调整umask值来限制这些权限。
7、否则按系统默认,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。
5.6 读和写流
1、一旦打开了流,则可在3种不同类型的格式化I/O中进行选择,对其进行读、写操作:
- (1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
- (2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。
- (3)直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。
2、输入函数:
(1)以下3个函数可用于一次读一个字符。
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
3个函数的返回值:若成功,返回下一个字符;若已达到文件尾端或出错,返回EOF
(2)函数getchar等同于getc(stdin)。前两个函数的区别是,getc可被实现为宏,而fgetc不能实现为宏。这意味着几点:
1. getc的参数不应当是具有副作用的表达式,因为它可能会被计算多次。
2. 因为fgetc一定是一个函数,所以得到其地址。可以作为函数指针传递给另一个函数。
3. 调用fgetc所需的时间很可能比调用getc更长,因为调用函数所需的时间长于调用宏。
(3)这3个函数在返回下一个字符时,将其unsigned char类型转换为int类型
1. 说明为无符号的理由是,如果最高位是1也不会使返回值为负。
2. 要求整型返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已到达文件尾端的指示值。在
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
两个函数返回值:若条件为真,返回非0(真);否则,返回0(假)
void clearerr(FILE *fp);
1)大多数实现中,为每个流在FILE对象中维护了两个标志:
1, 出错标志
2, 文件结束标志
2)调用clearerr可以清楚这两个标志。
4、从流中读取数据以后,可以调用ungetc将字符再压送回流中
#include <stdio.h>
int ungetc(int c, FILE *fp);
返回值:若成功,返回c;若出错,返回EOF。
- (1)压送回到流中的字符以后又可从流中读出,但读出字符的顺序与压送回去的顺序相反。
- (2)回送的字符,不一定必须是上一次读到的字符。不能回送EOF。但是当已经到达文件尾端时,仍可以回送一个字符。下次读将返回该字符,再读则返回EOF。之所以能这样做的原因是,一次成功的ungetc调用会清除该流的文件结束标志。
- (3)用ungetc压送回字符时,并没有将它们写到底层文件中或设备上,只是将它们写回标准I/O库的流缓冲区中。
5、输出函数
对应于上面所述的每个输入函数都有一个输出函数
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
3个函数返回值:若成功,返回c;若出错,返回EOF。
putc可被实现为宏,而fputc不能实现为宏。
5.7 每一次I/O
下面两个函数提供每次输入一行的功能。
虽然ISO C提供了gets,但已被弃用,推荐使用fgets。
#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
return: buf; error:NULL
fputs和puts提供每次输出一行的功能:
#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
return: 非负值; error: EOF.
函数fputs将一个以null字节终止的字符串写到指定的流,尾端的终止符null不写出。puts虽不像gets那样不安全,但也应避免使用,并且他在输出时最后添加了一个换行符。如果经常使用fgets和fputs,那么就会熟知每行终止处我们必须自己处理换行符。
5.8 标准I/O的效率
- 1、用getc和putc将标准输入复制到标准输出
- 2、读、写行版本:
- 3、以fgetc和read相比,系统调用与普通函数调用相比花费更多的时间
5.9 二进制I/O
1、下列两个函数以执行二进制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);
两个函数的返回值:读或写的对象数。
size为结构长度,nobj为需要对象的个数。
2、对于读,如果出错或到达文件尾端,则此数字可以少于nobj。在这种情况下,应该调用ferror或feof以判断究竟是哪一种情况。
3、对于写,如果返回值少于所要求的nobj,则出错。
4、二进制I/O的基本问题是,它只能用于读在同一系统上已写的数据。原因是:
- (1)在一个结构中,统一成员的偏移量可能随编译程序和变异系统的不同而不同(由于不同的对齐要求)。
- (2)用来存储多字节数的腹地安置的二进制格式在不同的系统结构间也可能不同。
5.10 定位流
1、有3种定位标准I/O流:
- (1)ftell和seek函数,它们假定文件的位置可以存放在一个长整型中。
- (2)ftello和fseeko函数。它们使用off_t作为文件偏移量的数据类型。
- (3)fgetpos和fsetpos函数,它们使用fpos_t记录文件的位置。这种数据足够大以记录文件位置。
需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos。
#include <stdio.h>
long ftell(FILE *fp);
返回值:若成功,返回当前文件指示;若出错,返回-1L。
int fseek(FILE *fp, long offset, int whence);
返回值:若陈宫,返回0;若出错,返回-1。
void rewind(FILE *fp);
2、对于一个二进制文件,其文件指示器是从文件起始位置开始度量,并以字节为度量单位的。
3、为了用fseek定位一个二进制文件,必须指定一个字节offset。whence值与lseek函数的相同:
- (1)SEEK_SET表示从文件起始位置开始
- (2)SEEK_CUR表示当前文件位置开始
- (3)SEEK_END表示从文件尾端开始
4、为了定位一个文本文件,whence一定要是SEEK_SET,而且offset只有两种可能值:0(后退到文件的起始位置),或是对该文件的ftell所返回的值。
5、使用rewind函数也可将一个流设置到文件的起始位置。
#include <stdio.h>
void rewind(FILE * stream);
rewind()函数用于将文件指针重新指向文件的开头,同时清除和文件流相关的错误和eof标记,相当于调用fseek(stream, 0, SEEK_SET)
.
6、除了偏移量类型是off_t而非long以外,ftello函数与ftell相同,fseeko函数与fseek相同。
#include <stdio.h>
off_t ftello(FILE *fp);
int fseeko(FILE *fp, off_t offset, int whence);
返回值:若成功,返回0。若出错,返回-1。
7、fgetpos和fsetpos函数:
#include <stdio.h>
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *restrict fp, const fpos_t *pos);
两个函数返回值:若成功,返回0;若出错,返回非0.
5.11 格式化I/O
1、格式化输出
格式化输出是由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, ...);
3个函数返回值:若成功,返回输出字符数;若出错,返回负值。
int sprintf(char *restrict buf, const char *restrict format, ...);
返回值:若成功,返回存入数组的字符数;若编码出错,返回负值。
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
返回值:若缓冲区足够大,返回将要存入数组的字符数;若编码出错,返回负值。
- (1)printf将格式化数据写到标准输出。
- (2)fprintf写至指定的流。
- (3)dprintf写至指定的文件描述符
- (4)sprintf将格式化字符送入数组buf中。
- (5)sprintf在该数组尾端加上一个null字节,但该字符不包括在返回值中。
2、sprintf函数可能会造成由buf指向的缓冲区的溢出。为了解决这种缓冲区溢出问题,引入了snprintf函数。
3、格式化说明控制其余参数如何编写:
(1)转换说明以百分号%开始。
(2)一个转换说明有4个可选择的部分:
flags各标志如下:
这里写图片描述
fldwidth说明最小字段宽度:转换后参数字符若小于宽度,则多余字符位置用空格填充。
字段宽度是一个非负十进制数,或是一个星号(*)。
precssion说明整型转换后最少输出数字位数、浮点数转换后小数点后的最少位数。字符串转换后最大字节数。精度是一个点(.),其后跟随一个可选的非负十进制或一个星号(*)。
lenmodifier说明参数长度,其可能的值如下:
这里写图片描述
convtype是不可选的。它控制如何解释参数:
这里写图片描述
4、下列5种printf族辩题类似于上面的5种,但是可变参数表(…)替换成了arg
#include <stdarg.h>
#include <stdio.h>
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);
int vdprintf(int fd, const char *restrict format, va_list arg);
所有3个函数返回值:若成功,返回输出字符数;若输出出错,返回负值。
int vsprintf(char *restrict buf, const char *restrict format, va_list arg);
int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);
函数返回值:若缓冲区足够大,返回存入数组的字符数;若编码出错,返回负值。
5、格式化输入
执行格式化输入处理的是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, ...);
3个函数返回值:赋值的输入项数;若输入出错或在任一转换前到达文件尾端,返回EOF。
- (1)scanf族用于分析输入字符串,并将字符序列转换成指定类型的变量。
- (2)一个转换说明由3个可选部分:
fldwith说明最大宽度(即最大字符数)。
lenmodifier说明要转换结果赋值的参数大小。
在字段宽度和长度修饰之间的可选项m是赋值分佩服。它可以用于%c、%s以及%[转换符,迫使内存缓冲区分配空间以接纳转换字符串。
scanf族函数支持的转换类型:
这里写图片描述
6、与printf族相同,scanf族也使用由
#include <stdarg.h>
#include <stdio.h>
int vscanf(const char *restrict format, va_list arg);
int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg);
int vsscanf(const char *restrict buf, const char *restrict format, va_list arg);
3个函数返回值:指定的输入项目数;若输入出错或在任一转换前文件结束,返回EOF。
5.12 实现细节
1、每个标准I/O流都有一个与其相关的文件描述符,可以对一个流调用fileno函数获得其描述符。
#include <stdio.h>
int fileno(FILE *fp);
返回值:与该流相关联的文件描述符
2、程序为3个标准流以及一个普通文件相关联的流打印有关缓冲的状态信息
5.13 临时文件
1、ISO C 标准I/O库提供了两个函数以帮助创建临时文件:
#include <stdio.h>
char *tmpnam(char *ptr);
返回值:指向唯一路径名的指针
FILE *tmpfile(void);
返回值:若成功,返回文件指针;若出错,返回NULL。
(1)tmpnam 函数产生一个与现有文件不同的一个有效路径名字符串。每次调用它时,都会产生一个不同的路径名,最多调用次数是 TMP_MAX(定义在
3、Single UNIX Specification为了处理临时文件定义了另外两个函数:mktemp和mksetup,它们是XSI的扩展部分。
#include <stdlib.h>
char *mkdtemp(char *template);
返回值:若成功,返回指向目录名的指针;若出错,返回NULL
int mkstemp(char *template);
返回值:若成功,返回文件描述符;若出错,返回-1
(1)mkdtemp函数创建了一个目录,该目录有一个唯一的名字。
(2)mkstemp函数创建了一个文件,该文件有一个唯一的名字。
(3)名字是通过template字符串进行选择的,这个字符串是后6位设置为XXXXXX的路径名。函数将这些占位符替换成不同的字符来构建一个唯一的路径名。如果成功的话,这两个函数将修改template字符串反映临时文件的名字。
(4)由mkdtemp函数创建的目录使用下列访问权限位集:
S_IRUSR | S_IWUSR | S_IXUSR
(5)mkstemp函数以唯一名字创建一个普通文件病打开该文件,该函数的文件描述符以读写方式打开。由mkstemp创建文件使用访问权限位
S_IRUSR | S_IWUSR。
(6)与tempfile不同,mkstemp创建的临时文件并不会自动删除。必须自己对它解除链接。
(7)tmpnam和tempnam至少有一个缺点,在返回唯一的路径名和使用名字创建文件之间存在一个时间窗口。在这个时间窗口可能以相同的名字创建文件。
5.14 内存流
1、有3个函数可以用于内存流的创建,第一个fmemopen函数:
#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
返回值:若成功,返回流指针;若错误,返回NULL。
(1)fmemopen函数允许调用者提供缓冲区用于内存流。buf参数指向缓冲区的开始位置,size参数指定了缓冲区大小的字节数。
(2)如果buf参数为空,fmemopen函数分配size字节数的缓冲区。在这种情况下,当流关闭时缓冲区会被释放。
2、type参数控制如何使用流。
type | 说明 |
---|---|
r 或 rb | 为读而打开 |
w 或 wb | 为写而打开 |
a 或 ab | 追加:为在第一个null字节处写而打开 |
r+ 或 r+b 或 rb+ | 为读和写而打开 |
w+ 或 w+b 或 wb+ | 把文件截断至0长,为读和和写而打开 |
a+ 或 a+b 或 ab+ | 追加:为在第一个null字节处读和写而打开 |
与基于文件的标准I/O流的type参数取值,其中有些差别
- (1)无论何时以追加方式打开内存流时,当前文件位置设置为缓冲区的第一个null字节。如果缓冲区中不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流不是以追加方式打开时,当前位置设为缓冲区的开始位置。
- (2)如果buf参数是一个null指针,打开刘进行读或者写没有任何意义。
- (3)任何时候需要增加缓冲区中数据以及调用fclose、fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节。
3、程序反映了内存流的写入是如何在我们提供的缓冲区上进行操作的。
4、创建内存流的其他两个函数分别是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。
(1)open_memstream函数创建的流是面向字节的,open_wmemstream函数创建的流是面向宽字节的。
5、这两个函数与fmemopen的不同在于:
- (1)创建的流只能写打开
- (2)不能指定自己的缓冲区,但可以通过bufp和sizep参数访问缓冲区地址和大小。
- (3)关闭流后需要自行释放缓冲区。
- (4)对流添加字节会增加缓冲区大小。
6、缓冲区地址和大小使用上由已写原则:
- (1)缓冲区地址和长度只有在调用fclose或fflush后才有效
- (2)这些值只有在下一次流写入或调用fclose前才有效。因为缓冲区可以增长,可能需要重新分配。
7、因为避免了缓冲区溢出,内存刘非常适用于创建字符串。因为内存流只能访问主存,不访问磁盘上的文件,所以对于把标准I/O流作为参数用于临时文件的函数来说,会有很大的性能提升。
总结
文件I/O一章讲了不带缓冲的I/O,本章讲的是带缓冲的I/O。不带缓冲针对的是内核的系统调用,而带缓冲针对的是用户空间的标准库函数,是基于带缓冲的I/O实现的。不带缓冲的I/O通过文件描述符的方式来引用一个文件,而带缓冲的I/O则通过文件流(stream)的方式来引用文件。至于为什么要用流的方式,原因就是带缓冲区,这样文件的读写就要经过缓冲区做缓冲,就像水流一样。
引入标准IO库的目的是为了提高IO的效率,避免频繁的进行read/write系统调用,而系统调用会消耗较多的资源。因此标准IO库引入了IO缓存,通过累积一定量的IO数据后,然后集中写入到实际的文件中来减少系统调用,从而提高IO效率。标准IO库会自动管理内部的缓存,不需要程序员介入。然而,也正是因为我们看不到标准IO库的缓存,有时候会给我们带来一定的迷惑性。