fcoles()函数
fclose(fp)函数关闭fp指定的文件, 必要时刷新缓冲区。 对于较正式的程序, 应该检查是否成功关闭文件。 如果成功关闭, fclose()函数返回0, 否则返回EOF
if (fclose(fp) != 0)
printf("Error in closing file %s\n", argv[1]);
如果磁盘已满、 移动硬盘被移除或出现I/O错误, 都会导致调用fclose()函数失败。
指向标准文件的指针
stdio.h头文件把3个文件指针与3个标准文件相关联, C程序会自动打开这3个标准文件。
这些文件指针都是指向FILE的指针, 所以它们可用作标准I/O函数的参数, 如fclose(fp)中的fp。 接下来, 我们用一个程序示例创建一个新文件, 并写入内容。
一个简单的文件压缩程序
fprintf()和 printf()类似, 但是 fprintf()的第 1 个参数必须是一个文件指针。 程序中使用stderr指针把错误消息发送至标准错误, C标准通常都这么做。
为了构造新的输出文件名, 该程序使用strncpy()把名称eddy拷贝到数组name中。 参数LEN-5为.red后缀和末尾的空字符预留了空间。 如果argv[2]字符串比LEN-5长, 就拷贝不了空字符。 出现这种情况时, 程序会添加空字符。 调用strncpy()后, name中的第1个空字符在调用strcat()函数时, 被.red的.覆盖, 生成了eddy.red。 程序中还检查了是否成功打开名为eddy.red的文件。这个步骤在一些环境中相当重要, 因为像strange.c.red这样的文件名可能是无效的。
该程序同时打开了两个文件, 所以我们要声明两个 FIFL 指针。 注意,程序都是单独打开和关闭每个文件。 同时打开的文件数量是有限的, 这个限制取决于系统和实现, 范围一般是10~20。 相同的文件指针可以处理不同的文件, 前提是这些文件不需要同时打开。
文件I/O:fprintf()、fscanf()、fgets()和fputs()
I/O函数都类似于文件I/O函数。 它们的主要区别是, 文件I/O函数要用FILE指针指定待处理的文件。 与 getc()、 putc()类似, 这些函数都要求用指向 FILE 的指针(如, stdout) 指定一个文件, 或者使用fopen()的返回值。
fprintf()和fscanf函数
文件I/O函数fprintf()和fscanf()函数的工作方式与printf()和scanf()类似,区别在于前者需要用第1个参数指定待处理的文件。
fprintf()和 fscanf()的工作方式与 printf()和 scanf()类似。 但是, 与 putc()不同的是, fprintf()和fscanf()函数都把FILE指针作为第1个参数, 而不是最后一个参数。
fgets()和fputs()函数
fgets()函数。 它的第1个参数和gets()函数一样, 也是表示储存输入位置的地址(char * 类型) ; 第2个参数是一个整数, 表示待输入字符串的大小 [1]; 最后一个参数是文件指针, 指定待读取的文件。
fgets(buf, STLEN, fp);
buf是char类型数组的名称, STLEN是字符串的大小, fp是指向FILE的指针。
fgets()函数读取输入直到第 1 个换行符的后面, 或读到文件结尾, 或者读取STLEN-1 个字符(以上面的 fgets()为例) 。 然后, fgets()在末尾添加一个空字符使之成为一个字符串。 字符串的大小是其字符数加上一个空字符。如果fgets()在读到字符上限之前已读完一整行, 它会把表示行结尾的换行符放在空字符前面。 fgets()函数在遇到EOF时将返回NULL值, 可以利用这一机制检查是否到达文件结尾; 如果未遇到EOF则之前返回传给它的地址。
fputs()函数接受两个参数: 第1个是字符串的地址; 第2个是文件指针。该函数根据传入地址找到的字符串写入指定的文件中。 和 puts()函数不同,fputs()在打印字符串时不会在其末尾添加换行符。
fputs(buf, fp);
buf是字符串的地址, fp用于指定目标文件。fgets()保留了换行符, fputs()就不会再添加换行符
随机访问fseek()和ftell()
该程序使用二进制模式, 以便处理MS-DOS文本和UNIX文件。 但是,在使用其他格式文本文件的环境中可能无法正常工作。
如果通过命令行环境运行该程序, 待处理文件要和可执行文件在同一个目录(或文件夹) 中。 如果在IDE中运行该程序, 具体查找方案序因实现而异。
fseek()和ftell()的工作原理
fseek()的第1个参数是FILE指针, 指向待查找的文件, fopen()应该已打开该文件。
fseek()的第2个参数是偏移量(offset) 。 该参数表示从起始点开始要移动的距离该参数必须是一个long类型的值, 可以为正(前移)、负(后移)或0(保持不动)。
fseek()的第3个参数是模式, 该参数确定起始点。 根据ANSI标准, 在stdio.h头文件中规定了几个表示模式的明示常量(manifest constant)
旧的实现可能缺少这些定义, 可以使用数值0L、 1L、 2L分别表示这3种模式。 L后缀表明其值是long类型。 或者, 实现可能把这些明示常量定义在别的头文件中。
fseek(fp, 0L, SEEK_SET); // 定位至文件开始处
fseek(fp, 10L, SEEK_SET); // 定位至文件中的第10个字节
fseek(fp, 2L, SEEK_CUR); // 从文件当前位置前移2个字节
fseek(fp, 0L, SEEK_END); // 定位至文件结尾
fseek(fp, -10L, SEEK_END); // 从文件结尾处回退10个字节
fseek()的返回值为0; 如果出现错误(如试图移动的距离超出文件的范围) , 其返回值为-1。
ftell()函数的返回类型是long, 它返回的是当前的位置。 ANSI C把它定义在stdio.h中。 在最初实现的UNIX中, ftell()通过返回距文件开始处的字节数来确定文件的位置。 文件的第1个字节到文件开始处的距离是0, 以此类推。 ANSI C规定, 该定义适用于以二进制模式打开的文件, 以文件模式打开文件的情况不同。
第1轮迭代, 把程序定位到文件结尾的第1个字符(即, 文件的最后一个字符) 。 然后, 程序打印该字符。 下一轮迭代把程序定位到前一个字符, 并打印该字符。 重复这一过程直至到达文件的第1个字符, 并打印。
二进制模式和文本模式
程序在UNIX和MS-DOS环境下都可以运行。 UNIX只有一种文件格式, 所以不需要进行特殊的转换。
许多MS-DOS编辑器都用Ctrl+Z标记文本文件的结尾。 以文本模式打开这样的文件时, C 能识别这个作为文件结尾标记的字符。 但是, 以二进制模式打开相同的文件时, Ctrl+Z字符被看作是文件中的一个字符, 而实际的文件结尾符在该字符的后面。 文件结尾符可能紧跟在Ctrl+Z字符后面, 或者文件中可能用空字符填充, 使该文件的大小是256的倍数。 在DOS环境下不会打印空字符。
二进制模式和文本模式的另一个不同之处是: MS-DOS用\r\n组合表示文本文件换行。 以文本模式打开相同的文件时, C程序把\r\n“看成”\n。 但是,以二进制模式打开该文件时, 程序能看见这两个字符。
ftell()函数在文本模式和二进制模式中的工作方式不同。 许多系统的文本文件格式与UNIX的模型有很大不同, 导致从文件开始处统计的字节数成为一个毫无意义的值。 ANSI C规定, 对于文本模式, ftell()返回的值可以作为fseek()的第2个参数。 对于MS-DOS, ftell()返回的值把\r\n当作一个字节计数。
可移植性
fseek()和ftell()应该符合UNIX模型。 但是, 不同系统存在着差异, 有时确实无法做到与UNIX模型一致。
在二进制模式中, 实现不必支持SEEK_END模式。移植性更高的方法是逐字节读取整个文件直到文件末尾。 C 预处理器的条件编译指令提供了一种系统方法来处理这种情况。
fgetpos()和fsetpos()函数
fseek()和 ftell()潜在的问题是, 它们都把文件大小限制在 long 类型能表示的范围内。 也许 20亿字节看起来相当大, 但是随着存储设备的容量迅猛增长, 文件也越来越大。 鉴于此, ANSI C新增了两个处理较大文件的新定位函数: fgetpos()和 fsetpos()。 这两个函数不使用 long 类型的值表示位置,它们使用一种新类型: fpos_t(代表file position type, 文件定位类型) 。fpos_t类型不是基本类型, 它根据其他类型来定义。 fpos_t 类型的变量或数据对象可以在文件中指定一个位置, 它不能是数组类型, 除此之外, 没有其他限制。
ANSI C定义了如何使用fpos_t类型。 fgetpos()函数的原型如下:
int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
调用该函数时, 它把fpos_t类型的值放在pos指向的位置上, 该值描述了文件中的一个位置。 如果成功, fgetpos()函数返回0; 如果失败, 返回非0。
调用该函数时, 使用pos指向位置上的fpos_t类型值来设置文件指针指向该值指定的位置。 如果成功, fsetpos()函数返回0; 如果失败, 则返回非0。fpos_t类型的值应通过之前调用fgetpos()获得。
标准的I/O机理
通常, 使用标准I/O的第1步是调用fopen()打开文件(前面介绍过, C程序会自动打开3种标准文件) 。 fopen()函数不仅打开一个文件, 还创建了一个缓冲区(在读写模式下会创建两个缓冲区) 以及一个包含文件和缓冲区数据的结构。 另外, fopen()返回一个指向该结构的指针, 以便其他函数知道如何找到该结构。 假设把该指针赋给一个指针变量fp, 我们说fopen()函数“打开一个流”。 如果以文本模式打开该文件, 就获得一个文本流; 如果以二进制模式打开该文件, 就获得一个二进制流。
我们主要考虑文件输入。 通常, 使用标准I/O的第2步是调用一个定义在stdio.h中的输入函数, 如fscanf()、 getc()或 fgets()。 一调用这些函数, 文件中的数据块就被拷贝到缓冲区中。 缓冲区的大小因实现而异, 一般是512字节或是它的倍数, 如4096或16384(随着计算机硬盘容量越来越大, 缓冲区的大小也越来越大) 。 最初调用函数, 除了填充缓冲区外, 还要设置fp所指向的结构中的值。 尤其要设置流中的当前位置和拷贝进缓冲区的字节数。 通常, 当前位置从字节0开始。
在初始化结构和缓冲区后, 输入函数按要求从缓冲区中读取数据。 在它读取数据时, 文件位置指示器被设置为指向刚读取字符的下一个字符。 由于stdio.h系列的所有输入函数都使用相同的缓冲区, 所以调用任何一个函数都将从上一次函数停止调用的位置开始。
当输入函数发现已读完缓冲区中的所有字符时, 会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区中。 以这种方式, 输入函数可以读取文件中的所有内容, 直到文件结尾。 函数在读取缓冲区中的最后一个字符后, 把结尾指示器设置为真。 于是, 下一次被调用的输入函数将返回EOF。
输出函数以类似的方式把数据写入缓冲区。 当缓冲区被填满时, 数据将被拷贝至文件中。
其他标准I/O函数
ANSI标准库的标准I/O系列有几十个函数。 除了setvbuf(), 其他函数均可在ANSI之前的实现中使用。
int ungetc(int c, FILE*fp)函数
int ungetc()函数把c指定的字符放回输入流中。 如果把一个字符放回输入流, 下次调用标准输入函数时将读取该字符。假设要读取下一个冒号之前的所有字符, 但是不包括冒号本身, 可以使用 getchar()或getc()函数读取字符到冒号, 然后使用 ungetc()函数把冒号放回输入流中。ANSI C标准保证每次只会放回一个字符。 如果实现允许把一行中的多个字符放回输入流, 那么下一次输入函数读入的字符顺序与放回时的顺序相反。
int fflush()函数
int fflush(FILE *fp);
调用fflush()函数引起输出缓冲区中所有的未写入数据被发送到fp指定的输出文件。 这个过程称为刷新缓冲区。 如果 fp是空指针, 所有输出缓冲区都被刷新。 在输入流中使用fflush()函数的效果是未定义的。 只要最近一次操作不是输入操作, 就可以用该函数来更新流(任何读写模式)。
int setvbuf()函数
setvbuf()函数的原型是:
int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size);
setvbuf()函数创建了一个供标准I/O函数替换使用的缓冲区。 在打开文件后且未对流进行其他操作之前, 调用该函数。 指针fp识别待处理的流, buf指向待使用的存储区。 如果buf的值不是NULL, 则必须创建一个缓冲区。
声明一个内含1024个字符的数组, 并传递该数组的地址。 然而, 如果把NULL作为buf的值, 该函数会为自己分配一个缓冲区。 变量size告诉setvbuf()数组的大小。 mode的选择如下: _IOFBF表示完全缓冲(在缓冲区满时刷新) ; _IOLBF表示行缓冲(在缓冲区满时或写入一个换行符时) ; _IONBF表示无缓冲。 如果操作成功,函数返回0, 否则返回一个非零值。
假设一个程序要储存一种数据对象, 每个数据对象的大小是3000字节。可以使用setvbuf()函数创建一个缓冲区, 其大小是该数据对象大小的倍数。
二进制I/O:fread()和fwrite()
用 fprintf()函数和%f转换说明只是把数值保存为字符串。
double num = 1./3.;
fprintf(fp,"%f", num);
把num储存为8个字符: 0.333333。 使用%.2f转换说明将其储存为4个字符: 0.33, 用%.12f转换说明则将其储存为 14 个字符: 0.333333333333。 改变转换说明将改变储存该值所需的空间数量, 也会导致储存不同的值。 把num 储存为 0.33 后, 读取文件时就无法将其恢复为更高的精度。 一般而言, fprintf()把数值转换为字符数据, 这种转换可能会改变值。
为保证数值在储存前后一致, 最精确的做法是使用与计算机相同的位组合来储存。 因此, double 类型的值应该储存在一个 double 大小的单元中。如果以程序所用的表示法把数据储存在文件中, 则称以二进制形式储存数据。 不存在从数值形式到字符串的转换过程。 对于标准 I/O, fread()和 fwrite函数用于以二进制形式处理数据。
所有的数据都是以二进制形式储存的, 甚至连字符都以字符码的二进制表示来储存。 如果文件中的所有数据都被解释成字符码, 则称该文件包含文本数据。 如果部分或所有的数据都被解释成二进制形式的数值数据, 则称该文件包含二进制数据(另外, 用数据表示机器语言指令的文件都是二进制文件)。
ANSI C和许多操作系统都识别两种文件格式: 二进制和文本。 能以二进制数据或文本数据形式存储或读取信息。 可以用二进制模式打开文本格式的文件, 可以把文本储存在二进制形式的文件中。 可以调用 getc()拷贝包含二进制数据的文件。 然而, 一般而言,用二进制模式在二进制格式文件中储存二进制数据。 类似地, 最常用的还是以文本格式打开文本文件中的文本数据(通常文字处理器生成的文件都是二进制文件, 因为这些文件中包含了大量非文本信息, 如字体和格式等 。
size_t fwrite()函数
fwrite()函数的原型
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb,FILE * restrictfp);
fwrite()函数把二进制数据写入文件。 size_t是根据标准C类型定义的类型, 它是sizeof运算符返回的类型, 通常是unsigned int, 但是实现可以选择使用其他类型。 指针ptr是待写入数据块的地址。 size表示待写入数据块的大小(以字节为单位) , nmemb表示待写入数据块的数量。 和其他函数一样,fp指定待写入的文件。
fwrite()原型中的const void * restrict ptr声明。 fwrite()的一个问题是, 它的第1个参数不是固定的类型。在ANSI C函数原型中, 这些实际参数都被转换成指向void的指针类型, 这种指针可作为一种通用类型指针(在ANSI C之前, 这些参数使用char*类型, 需要把实参强制转换成char *类型)。
fwrite()函数返回成功写入项的数量。 正常情况下, 该返回值就是nmemb, 但如果出现写入错误, 返回值会比nmemb小。
size_t fread()函数
size_t fread()函数的原型
size_t fread(void * restrict ptr, size_t size, size_t nmemb,FILE * restrict fp);
fread()函数接受的参数和fwrite()函数相同。 在fread()函数中, ptr是待读取文件数据在内存中的地址, fp指定待读取的文件。 该函数用于读取被fwrite()写入文件的数据。
double earnings[10];
fread(earnings, sizeof (double), 10, fp);
该调用把10个double大小的值拷贝进earnings数组中。
fread()函数返回成功读取项的数量。 正常情况下, 该返回值就是nmemb, 但如果出现读取错误或读到文件结尾, 该返回值就会比nmemb小。
int feof(FILE*fp)和int ferror(FILE*fp)函数
如果标准输入函数返回 EOF, 则通常表明函数已到达文件结尾。 然而, 出现读取错误时, 函数也会返回EOF。 feof()和ferror()函数用于区分这两种情况。 当上一次输入调用检测到文件结尾时, feof()函数返回一个非零值, 否则返回0。 当读或写出现错误, ferror()函数返回一个非零值, 否则返回0。
一个程序示例
如setvbuf()无法创建缓冲区, 则返回一个非零值, 然后终止程序。 可以用类似的代码为正在拷贝的文件创建一块4096字节的缓冲区。 把NULL作为setvbuf()的第2个参数, 便可让函数分配缓冲区的存储空间。
该程序获取文件名所用的函数是 s_gets(), 而不是 scanf(), 因为 scanf()会跳过空白, 因此无法检测到空行。 该程序还用s_gets()代替fgets(), 因为后者在字符串中保留换行符。
temp数组具有静态存储期(意思是在编译时分配该数组, 不是在每次调用append()函数时分配) 和块作用域(意思是该数组属于它所在的函数私有)。