目录
一、流和FILE对象
在前面讲到的文件IO的操作对象是文件描述符,而本章讲述的标准I/O的操作对象是流(stream)。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
对于ASCII码,一个字节表示一个字符,标准I/O文件流可用于单字节或多字节(“宽”)字符集。流的定向决定了读、写的字符是单字节还是多字节的。当一个流被创建时并没有定向,若在未定向的流上使用一个多字节I/O函数(stdio.h中的函数,如fprintf、fscanf等函数),则将该流的定向设置为宽定向的(使用普通的char类型字符);若在未定向的流上使用一个单字节I/O函数(wchar.h中的函数,如fwprintf、fwscanf函数),则将该流的定向设置为字节定向的(使用宽字符wchar_t)。
流一旦被确定,就不能再改变。除了上述的,先使用哪个函数打开就确定为哪种定向之外,还可以用fwide函数显式的设置流的定向(后续讲解如何清除一个流的定向):
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp,int mode);
mode:
- 若mode参数为负,fwide将试图使指定的流是字节定向的。
- 若mode参数为正,fwide将试图使指定的流是宽定向的。
- 若mode参数为0,fwide查询指定的流的定向。
注意,fwide无出错返回。
当打开一个流时,标准I/O函数fopen返回一个FILE对象的指针。该对象通常是一个结构,它包含了标准I/O库为管理该流所需要的信息,包括:用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前再缓冲区中的字符数以及出错标志等等。我们将指向FILE对象的指针(FILE*)为文件指针。
对一个进程,定义了三个流:标准输入、标准输出和标准出错。这三个标准I/O流通过预定义文件指针stdin、stdout、stderr加以引用,定义在stdio.h中。
二、缓冲
标准I/O库提供缓冲的目的是尽可能少的调用read和write,提供了三种类型的缓冲:
1.全缓冲:这种情况下,在填满标准I/O缓冲区后才进行实际的I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。其使用malloc获得所需的缓冲区。冲洗指的是标准I/O缓冲区的写操作,缓冲区由标准I/O例程自动冲洗或使用fflush冲洗一个流。
2.行缓冲:在这种情况下,当输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(fputs),但只有在写了一行之后才进行实际的I/O操作。对于行缓冲的限制:第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以如果缓冲区满了,即使没有换行符也会输出。第二,任何时候只要通过标准I/O库要求从一个不带缓冲的流,或者一个行缓冲的流得到数据,那么就会造成冲洗所有的行缓冲输出流。
3.不带缓冲:标准I/O库不对字符进行缓冲存储。标准出错流stderr通常是不带缓冲的,使得错误信息可以尽快显示出来。
对于一个给定的流,可以调用一下函数更改缓冲类型,.这些函数一定要在流被打开后调用:
#include <stdio.h>
void setbuf(FILE *restrict fp,char *restrict buf);
int setvbuf(FILE *restrict fp,char *restrict buf,int mode,size_t size);
关键字*restrict:
它是c99标准引入的,称为限制指针。被它描述的指针的含义是:告诉编译器,这个指针是访问它所指向的内存区域的唯一方式,没有其他指针会指向同一块内存区域,编译器会进行相应的优化
setvbuf函数,我们可以通过mode指定流的缓冲模式:
- _IOFBF 全缓冲
- _IOLBF 行缓冲
- _IONBF 不带缓冲
如果指定一个不带缓冲的流,则忽略buf和size参数;若指定全缓冲和行缓冲,则buf和size可以指定一个缓冲区及其长度。如果该流是带缓冲的,而buf是NULL,则标准IO库将自动的为该流分配合适的缓冲区(常量BUFSIZE决定,该常量在<stdio.h>中定义)
setbuf函数,与setvbuf相似,但是只能指定该流是否有缓冲,对于有缓冲的情况,具体是行缓冲还是全缓冲是不确定的。
- 当buf为NULL时,设置该流为不带缓冲
- 当buf不为NULL时(buf必须指向一个长度为BUFSIZE的缓冲区,BUFSIZE在<stdio.h>中定义),buf作为该流的缓冲区,一般情况下是全缓冲,当流与终端设备相关时,一般是行缓冲

有些实现会将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区的字节数少于size。一般而言应当由系统选择缓冲区的长度,并自动分配缓冲区,这种情况下关闭此流,标准IO库会自动释放缓冲区
使用fflush强制冲洗一个流:
#include <stdio.h>
int fflush(FILE *fp);
注意,若fp是NULL,则导致所有输出流都被冲洗!
三、打开流
以下三个函数打开一个标准IO流:
#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 filedes,const cahr *type);
//成功则返回文件指针,失败则返回NULL
区别:
(1)fopen打开一个指定的文件
(2)ffreopen在一个指定的流上打开一个指定的文件,若该流已经打开,则先关闭该流。若该流已经定向,则freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流(标准输入、标准输出、标准出错)
(3)fdopen获取一个现有的文件描述符,并使一个标准的IO流与该文件描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符,因为这些特殊类型的文件不能用标准IO的fopen函数打开,所以我们必须事先调用设备专用的函数以获得一个文件描述符,然后使用fdopen使一个标准IO流与该描述符相关联
type参数指定对该IO流的读写方式:

当以读和写类型打开一文件时(type中+符号),具有下列限制:
- 如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输出
- 如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达文件的尾端,则在输入操作之后不能直接跟随输出(总之不论是从读->写还是从写->读,都需要清空缓冲区,防止数据丢失)
注意,在使用w或a类型创建新文件时,无法指定该文件的权限位,而前面讲到的open和creat则可以。除非流引用终端设备,否则按系统默认情况,流被打开为全缓冲
调用fclose关闭一个已经打开的流:
#include <stdio.h>
int fclose(FILE *fp);
//成功返回0,失败返回EOF
四、读和写流
一旦打开了流,则可在三种不同类型的非格式化IO中进行选择,对其进行读写操作:
(1)每次一个字符的IO。
(2)每次一行的IO。如果想要一次读或写一行,则调用fputs或fgets,每行以一个换行符为终止。当调用fgets时应该说明能处理的最大长度(后续会讲到)
(3)直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作独活写某种数量的对象。
每次读取一个字符:
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
//若成功则返回下一个字符,若已经达到文件结尾或出错则返回EOF
函数getchar等价于getc(stdin)。
getc和fgetc的区别是:getc可以被实现为一个宏,而fgetc不可以,这意味着
(1)getc的参数不应当是具有副作用的表达式
(2)因为fgetc一定是一个函数,所以可以得到其地址。这就允许fgetc的肚子鼓作为一个参数传递给另一个函数
(3)调用fgetc所需的时间更久
什么叫可以被实现为一个宏?
c语言中,宏的含义是展开,即在头文件中用#define定义的文本替换,由预处理器在对应位置展开,本质是代码替换;而函数则被编译成机器代码,存放在库文件中,通过函数调用来使用。
那么为什么getc函数可以被实现为一个宏而fgetc不行?
因为getc的实现非常简单:
#define getc(fp) \ ((fp)->_cnt > 0 ? (int)(*(fp)->_ptr++) : _fillbuf(fp)而fgetc函数包含有循环和多个条件判断,逻辑复杂,不适合用宏实现
宏实现的优缺点:
优点:
- 性能:无函数调用开销
- 内联展开:编译器可以更好的优化
- 类型通用:宏可以处理不同的类型
缺点:
- 多次求值:参数可能被多次求值
- 类型不安全:没有类型检查
- 调试困难:调试器看到的是展开后的代码
- 作用域问题:可能意外捕获变量
- 代码膨胀:每次调用都复制代码
为什么上面说,getc的输入参数不能是一个带有副作用的值?
要弄清楚这个问题,我们要先知道什么是带有副作用的值。假设我们输入参数a,a是一个int型变量,那么这就是不带副作用的参数。如果输入参数为a++,那么这就是带副作用的参数,它的副作用是使a递增。假如我们有一个函数,定义如下:
#include <stdio.h> int add(int a,int b){ return a+b; } int main(){ int x = 5; int result = add(x++, x++); // 执行过程: // 1. 计算两个参数:都是5(x的初始值) // 2. 调用函数 add(5, 5) // 3. 返回 10 // 4. x 自增两次,变成7 // 注意:虽然结果是未定义行为,但实际编译器通常按顺序求值 }当我调用add(i++,j)时,函数会将i++的值与j相加。那么为什么在使用宏实现的函数不能使用带有副作用的参数呢?这是因为宏展开的参数不一定只被调用一次。假如有一个宏展开的定义如下:
#define ADD(a, b) ((a) + (b)) int main(){ int x = 5; int result = ADD(x++, x++); // 展开后: // int result = ((x++) + (x++)); // 这明显是未定义行为!不同编译器结果不同: // 可能是 5+6=11,也可能是 5+5=10,还可能是其他值 }
回到正文,前面讲述的三个函数返回值是一个整形,当返回EOF时,可能是读取错误,也可能是已经到达文件结尾,我们必须调用ferror或feof函数进行区分:
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
//条件为真则返回非0值,否则返回0
void clearerr(FILE *fp);
在大多数实现中,为每个流在FILE对象中维持了两个标志位:
- 出错标志
- 文件结束标志
调用clearerr则清除这两个标志
从流中读取数据以后再调用ungetc可以将字符再押送回流中:
#include <stdio.h>
int ungetc(int c,FILE *fp);
//成功则返回被推回的字符c,失败则返回EOF
压送回到流中的字符以后又可以从流中读取,但取出的字符顺序与压送回的顺序相反。
对应于之前讲的三个输入函数,还有三个输出函数:
#include <stdio.h>
int putc(int c,FILE *fp);
int fputc(int c,FILE *fp);
int putchar(int c);
五、每次一行IO
#include <stdio.h>
char *fgets(char *restrict buf,int n,FILE *restrict fp);
char *gets(char *buf);
//若成功则返回buf,若达到文件末尾或出错则返回NULL
这两个函数将读入的数据存入buf缓冲区中,gets从标准输入读取,fgets从指定流中读取
对fgets,无论是缓冲区满之后返回,还是输入换行之后返回,缓冲区总是以null结尾
gets是一个不推荐使用的函数,因为调用者再使用gets时不能指定缓冲区的长度,这样可能造成缓冲区溢出,写到缓冲区之后的存储空间。
fputs和puts提供每次输出一行的功能:
#include <stdio.h>
int fputs(const char *restrict str,FILE *restrict fp);
int puts(const char *str);
//若成功返回非负值,失败返回NULL
fputs将一串以null结尾的字符写道指定流(注意是null不是换行),终止符null不写出。
puts将一个以null结尾的字符串写到标准输出,终止符不写出,但随后puts将换行符写道标准输出。
六、二进制IO
#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:要读取/写入的数据项数量
fp:要读取/写入的文件流
对于返回值,假如有如下程序:
int numbers[100]; size_t items_read = fread(numbers, sizeof(int), 100, fp);返回值返回的是对象数,即int的数量,而不是字节数
给出以下示例:
(1)将一个数组写入文件,再读取到数组:
#include <stdio.h>
#include <stdlib.h>
int main(){
int data[] = {10,20,30,40};
size_t count = sizeof(data)/sizeof(data[0]);
FILE *fp = fopen("number.bin","wb");
if(!fp) exit(0);
size_t written = fwrite(data,sizeof(int),count,fp);
fclose(fp);
if(written == count)
{
printf("写入成功\n");
}
int *loaded_data = NULL;
size_t loaded_count = 0;
FILE *fp1 = fopen("number.bin","rb");
if(!fp1) exit(0);
fseek(fp1,0,SEEK_END);
long file_size = ftell(fp1);
rewind(fp1);
count = file_size / sizeof(int);
loaded_data = malloc(file_size);
if(!loaded_data){
fclose(fp1);
exit(0);
}
size_t read = fread(loaded_data,sizeof(int),count,fp1);
fclose(fp1);
if(read == count){
printf("读取成功\n");
}
free(loaded_data);
return 0;
}
使用二进制IO时只能用于读再同一系统上已写的数据:
(1)在一个结构中,同一成员的偏移量可能因为编译器和系统而异,因为某些编译器设置有一个选项,选择它的不同值,或者使结构中的各成员紧密包装;或者准确对齐,这意味着即使在同一个系统上一个结构的二进制存放方式也可能因为编译器的选项而不同
(2)用来存储多字节整数和浮点值的二进制格式在不同的机器体系结构之间也可能不同
七、定位流
有三种方法定位标准IO流:
(1)ftell函数和fseek函数。它们假定文件的位置可以存放在一个长整型中
(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,失败返回非0值
void rewind(FILE *fp);
对于二进制文件,其文件位置是从文件起始位置开始度量,并且以字节为计量单位。ftell函数返回值就是这样的文件位置。对于fseek,其whence参数与前面讲到的lseek函数相同。使用rewind函数将一个流设置到文件的起始位置。
除了offset的类型是off_t 而非long之外,ftello与ftell相同,fseek与fseeko相同
#include <stdio.h>
off_t ftello(FILE *fp);
int fseeko(FILE *fp,off_t offset,int whence);
八、格式化IO
格式化输出:
#include <stdio.h>
int printf(const char *restrict format , ...);
int fprintf(FILE *restrict fp,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写至指定流,sprintf将格式化字符送入数组buf中(buf相当于用户缓冲区,数据->buf->输出)。sprintf在该数组的末尾加一个null,但不计入返回值的计数
sprintf函数可能会造成由buf指向的缓冲区的溢出,调用时需要确保缓冲区足够大。snprintf被设计用来解决这个问题,显式的输入缓冲区长度(n),超过这个长度的任何字符都会被丢弃。
对于格式化输出,转换说明很关键。转换说明以%开头,可选部分有四个,如下:
% [flags] [fldwidth] [precision] [lenmodifier] convtype
(1)flags:

fldwidth说明转换的最小字段宽度。如果转换得到的字符较少,则用空格填充它。字段宽度是一个非负的十进制数,或者是一个*
precision说明整形转换后最少输出数字位数、字符串转换后的最大字符数。精度是一个句点 . ,后可以接一个可选的非负十进制整数或一个*
lenmodifier说明参数长度:

convtype不是可选的,它控制如何解释参数

三个格式化输入函数:
#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族用于分析输入字符串,并将自负床序列转换成指定类型的变量。格式之后的个参数包含了变量的地址,以用转换结果初始化这些变量
转换说明:
% [*] [fldwidth] [lenmodifier] convtype
可选的前导*用于抑制转换。按照转换说明的其余部分对输入进行转换,但转换结果并不存放在参数中。
fldwidth说明转换的最小字段宽度。lenmodifier说明要用转换结果初始化的参数大小。
convtype字段与printf中的类似,其中一个差别是:存储在无符号类型中的结果可在输入时带上符号

九、实现细节
在UNIX中标准IO库最终都要调用前面讲的文件IO,每个标准IO流都有一个与其相关联的文件描述符,使用fileno函数获得流对应的的文件描述符:
#include <stdio.h>
int fileno(FILE *fp);
可以查看你的系统中的头文件<stdio.h>,其中包含:FILE对象是如何定义的、每个流标志的定义以及定义为宏的各个标准IO例程
十、临时文件
以下函数帮助创建临时文件:
#include <stdio.h>
cahr *tmpnam(char *ptr);
//返回指向唯一路径名的指针
FILE *tmpfile(void);
//若成功则返回文件指针,出错则返回NULL
tmpnam产生一个唯一的文件名,并不产生文件。对于tmpnam,若ptr为null,则所产生的路径名存放在一个静态区,指向该静态区的指针作为函数值返回。下次再调用tmpnam时该静态区会被重写,所以我们应当保存路径名而不是复制指针(指针指向的静态区会被重写)
tmpfile创建一个临时的二进制文件(类型wb+),在关闭该文件或程序结束时自动删除。
除此之外还有其他标准规定的创建临时文件的函数如mkstemp、tempnam等等

2203

被折叠的 条评论
为什么被折叠?



