输入/输出

To understand a program you must become both the machine and the program. [^1]

目录


[TOC]

输入\输出


零 前言

C 语言的输入\输出库是标准库中最大且最重要的部分。

我们除过继续深入讨论 printf函数和scanf函数以及相关函数之外,还会讨论:

  • 每次读写一个字符的函数:getc函数和putc函数以及相关函数。
  • 每次读写一行字符的函数:gets函数和puts函数以及相关函数。
  • 读写数据块的 fread函数和fwrite函数

本章涵盖了<stdio.h>中的大部分函数,但忽略了 8 个函数。于<errno.h>头相关的函数和依赖va_list类型的函数我们也许会在后面介绍。

在 C89 中,所有标准输入输出函数都属于<stdio.h>头。但是 C99 有些输入输出函数在头<wchar.h>中声明。

<stdio.h>中用于读或写数据的函数称为字节输入\输出函数<wchar.h>中的类似函数称为宽字符输入\输出函数

相关的文章之前我写过一些,推荐和本文一起阅读,重复的内容有些会不再提及。参考文章:

https://mp.weixin.qq.com/s/H1Yp5miEf8NP4HdP8OECqg

一 流

流(stream)表示任意输入的源或任意输出的目的地。

许多小型程序都是通过一个流(通常与键盘相关)获得全部的输入,并且通过另一个流(通常和屏幕相关)写出全部的输出。

1. 文件指针

C程序中对流的访问是通过文件指针实现的。此指针类型为FILE*(FILE*类型在<stdio.h>中声明),声明流:

FILE *fp1, *fp2 

点击并拖拽以移动

2. 标准流和重定向

<stdio.h>提供了3个标准流。这三个标准流可以直接使用——我们不需要对其进行声明,也不用打开或者关闭它们。

文件指针 默认的含义
stdin 标准输入 键盘
stdout 标准输出 屏幕
stderr 标准错误 屏幕

printf,scanf,putchar,getchar,puts,gets 都是通过stdin进行输入,stdout进行输出的。默认情况下,stdin表示键盘,而stdout,stderr表示屏幕。然而,许多操作系统允许通过一种称为**重定向(redirection)**的机制来改变这些默认的含义。

输入重定向(input redirection)

强制程序从文件而不是键盘获得输入,本质是使stdin流表示文件而非键盘。

重定向的绝妙之处在于,demo程序不会意识到正在从文件in.dat中读取数据,他会认为从stdin获得的任何数据都是从键盘上录入的。

**方法:**demo < in.dat

输出重定向 (output redirection)

方法:demo > out.dat

注意:

重定向输出入与输出可以结合使用,< 和 > 不需要与文件名相临,重定向文件顺序也是无关紧要的。

所以下面两种表示方法的效果是一样的:

demo < in.dat > out.dat
demo >out.dat <in.dat

点击并拖拽以移动

3. 文本文件与二进制文件

<stdio.h>支持两种文件类型:文本文件二进制文件

**文本文件(text file)**中,字节表示字符。(C程序的源代码就储存在文本文件中)

**二进制文件(binary file)**中,字节不一定表示字符;字节组还可以表示其它类型的数据,比如整数与浮点数(可执行C程序存贮在二进制文本中)

文本文件中二进制文件没有的特性:

1.文本文件分为若干行

文本文件的每一行通常以一两个特殊字符结尾。特殊字符与操作系统有关:

Windows中,行末标记是回车符(‘\x0d’)+ 换行符(‘\x0a’)

Unix 与新的MacOS中,行末是一个单独的换行符

2.文本文件可以包含一个特殊的“文件末尾”标记

一些操作系统允许在文本文件的末尾使用一个特殊字节作为标记。在Windows中,标记为 ‘\x1a’ (Ctrl + Z)。

Ctrl + Z 不是必须的,但如果存在, 它就标志着文件的结尾,其后所有的字节都被忽略。

大多数其他操作系统(包括UNIX)没有专门的文件末尾符

二进制文件不分行,也没有行末标记和文件末尾标记 ,所有字节都是平等对待的

例1:向文件写入数据时,我们需要考虑按文本格式存储还是按二进制格式进行存储。为了搞清其中的差别,考虑在文件中存储数32767的情况。

img点击并拖拽以移动

从上例可以看出:

二进制形式存储数据可以节省相当大的空间

总结:

在屏幕上显示文件内容的程序可能要把文件视为文本文件。但是,文件复制程序就不能认为要复制的文件为文本文件(考虑到文本文件可能含有文件末尾字符,这样就不能复制完全)。

在无法确定文件是文本文件还是二进制文件时,安全的做法是把文件假定为二进制文件。

二 文件操作

简单性是输入和输出重定向的魅力之一:不需要打开文件,关闭文件或执行任何其他的显示文件操作。可惜的是,重定向在许多应用中受到限制。当程序依赖重定向时,它无法控制自己的文件,甚至无法知道这些文件的名字。如果程序需要在同一时间读入两个文件或者写出两个文件时,重定向都无法做到。

当重定向无法满足需要时,我们需要使用<stdio.h>提供的文件操作。

1. 打开文件

如果要把文件用作流,打开时需要使用 fopen函数。

FILE *fopen( const char *filename, const char *mode );(C99 前)

FILE *fopen( const char *restrict filename, const char *restrict mode );(C99 起)

filename - 关联到文件系统的文件名
mode - 确定访问模式的空终止字符串

若成功,则返回指向新文件流的指针。流为完全缓冲,除非 filename 表示一个交互设备。错误时,返回空指针

第一个参数 filename 可能包含文件位置信息,如驱动器符或路径。

第二个参数:比如字符串 “r” 表示只读

注意:C99 中,函数原型有 restrict关键字,表明 filename 和 mode 所指向的字符串内存单元不共享。 C89 中不包含 restrict,但是也要有这样的要求。restrict 对 fopen 操作没有影响,所以通常可以忽略。


Windows 中:fopen 调用的文件名中有字符\时,一定要小心。因为 C 会把它看为是转义序列开始的标志:

fopen("c:\project\test1.dat", "r");

这个调用会失败。编译器会把 \p\t看为转义字符。(\p 并不是有效的转义字符。)

正确的用法:

fopen("c:\\project\\test1.dat", "r");

另一种方法更简单——只需要用/替代\即可:

fopen("c:/project/test1.dat", "r");

Windows 会把/接受为目录分隔符。


把 fopen 的返回值存储在变量中:

FILE* fp = fopen("1.dat", "r");//opens 1.dat for reading

当程序稍后调用输入函数从文件 1.dat 中读取数据时,将会把 fp 作为一个实际参数。

如果返回的是空指针,可能:

  • 文件不存在
  • 文件路径错误
  • 我们没有打开文件的权限

注意:永远不要假设可以打开文件,每次都要测试 fopen 函数的返回值确保不是空指针。

2. 模式

文本文件:

二进制文件:需要在模式字符串中包含字母 b

"rb" "wb" "ab" , "rb+" (或 "r+b",后同),"wb+""ab+"

3. 关闭文件

int fclose( FILE *stream )
**头文件:**stdio.h
参数:
stream - 需要关闭的文件流
返回值:
成功时为 0 ,否则为 EOF 。

fclose文件的参数必须是文件指针,此指针来自于 fopen 函数或 freopen 函数的调用。

下面给出了一个程序的框架。此程序打开文件 example.dat 进行读操作,并检查打开是否成功,让后在程序终止前把文件关闭:

#include<stdio.h>
#include<stdlib.h>

#define FILE_NAME "example.dat"

int main(void) {

FILE* fp;

fp = fopen(FILE_NAME, "r");
if (fp == NULL) {
printf("Can't open %s\n", FILE_NAME);
exit(EXIT_FAILURE);
}

// 执行操作

fclose(fp);

return 0;
}

可以将 fp 的声明与函数调用结合:

FILE* fp = fopen(FILE_NAME, "r");

还可以将函数调用与 NULL 判定结合:

if((fp = fopen(FILE_NAME, "r")) == NULL) ...

4. 为打开的流附加文件

FILE *freopen( const char *filename, const char *mode, FILE* stream ); (C99 前)

FILE *freopen( const char *restrict filename, const char *restrict mode, FILE *restrict stream );(C99起)

首先,试图关闭与 stream 关联的文件,忽略任何错误。然后,若 filename 非空,则试图用 mode 打开 filename 所指定的文件,如同用 fopen ,然后将该文件与 stream 所指向的文件流关联。若 filename 为空指针,则函数试图重打开已与 stream 关联的文件(此情况下是否允许模式改变是实现定义的)。

参数:
filename - 要关联到文件流的文件名
mode - 确定文件访问模式的空终止字符串
stream - 要修改的文件流

**返回值:**成功时为 stream 值的副本,失败时为空指针。

freopen函数为已经打开的流附加上一个不同的文件。最常见的用法是把文件与一个标准流(stdin, stdout, stderr)相关联。例如,为了使程序向文件 foo 中写数据:

if((freopen("foo", "w", stdout)) == NULL){
...
}

在关闭了先前(通过命令行重定向或之前的 freopen 函数调用)与 stdout 相关联的所有文件之后,freopen 函数打开文件 foo,并将其与 stdout 相关联。

5. 从命令行获取文件名

当正在编写的程序需要打开文件时,马上会出现一个问题:如何把文件名提供给程序呢?把文件名嵌入程序不太灵活,提示用户输入文件名的做法也很笨拙。通常最好的解决方法是让程序通过命令行获取文件的名字。

例如,执行 demo 程序时,将文件名放入命令行:

demo names.dat dates.dat

定义带有参数的 main 函数:

int main(int argc, char* argv[]){
...
}

指针数组 argv 的元素如图:

程序:检查文件是否可以打开

下面程序判断文件是否存在,如果存在是否可以打开进行读入。在运行程序时,用户给出要检查的文件的名字:

canopen file

然后程序将显示 :

file can be opend.

file can't be opend.

如果在命令行中录入的实际参数的数量不对,那么程序将显示出消息:

usage: canopen filename

来提示用户 canopen 需要一个文件名。

canopen.c

#include<stdio.h>
#include<stdlib.h>

int main(int argc, char* argv[]) {

FILE* fp;

if (argc != 2) {
printf("usage: canopen filename\n");
exit(EXIT_FAILURE);
}

if ((fp = fopen(argv[1], "r")) == NULL) {
printf("%s can't be opend.\n", argv[1]);
exit(EXIT_FAILURE);
}

printf("%s can be opend.\n", argv[1]);

fclose(fp);

return 0;
}

注意,可以使用重定向来丢弃 canopen 的输出,并简单的测试它返回的状态值。

六 临时文件

现实世界中的程序经常需要产生临时文件,即只在程序运行时存在的文件。例如,C 编译器就常常产生临时文件(中间文件)。一旦程序完全通过了编译,就不再需要保留那些含有程序中间形式的文件了。<stdio.h>提供了两个函数用来处理临时文件,即 tmpfile函数和tmpnam函数。

FILE *tmpfile(void);

创建并打开一个临时文件。该文件作为二进制文件、更新模式(如同为 fopen"wb+" 模式)打开。该文件的文件名保证在文件系统中唯一。至少可以在程序的生存期内能打开 TMP_MAX 个文件(此极限可能与 tmpnam 共享,并可能为 FOPEN_MAX 所进一步限制)。

**返回值:**指向与文件关联的文件流的指针,或若出现错误则为空指针。

该临时文件一直存在,除非关闭它或程序终止。函数返回文件指针,此指针可以用于稍后访问该文件:

FILE* tempptr;
...
tempptr = tmpfile();

tmpfile 虽然容易使用,但是有两个缺点:

  • 无法知道 tmpfile 函数创建的文件名
  • 我发在以后决定使文件成为永久性的。

如果这些限制产生了问题,可以使用 fopen 函数产生临时文件。为了避免此文件和前面已经存在的文件具有相同的名字,所以就需要一种方法来产生新的文件名。这就是 tmpnam 函数出现的原因。

char *tmpnam( char *filename );

创建独有的合法文件名(长度不长于 L_tmpnam )并将它存储于 filename 所指向的字符串。此函数足以生成至多 TMP_MAX 个独有文件名,但它们中的一部分可能正在文件系统中使用,从而不适合作为返回值。

**参数:**指向足以保有至少 L_tmpnam 字节的字符数组的指针,将以数组为结果缓冲区。若传递空指针,则返回指向内部静态缓冲区的指针。

**返回值:**若 filename 不是空指针则为 filename 。否则为指向内部静态缓冲区的指针。若不能生成适合的文件名,则返回空指针。

tmpnam函数为临时文件产生名字。

如果它的实参为空指针,那么 tmpnam 函数会把文件名字存储到一个静态变量中,并返回指向此变量的指针:

char* filename = tmpnam(NULL);

否则,tmpnam 函数会把文件名复制到程序提供的字符数组中:

char filename[L_tmpnam] = tmpnam(filename);

tmpnam 函数返回指向数组第一个字符的指针。L_tmpnam<stdio.h>中的一个宏,它指明了保存临时文件名的字符数组的长度。

注意:

  • 如果 filename 作为 tmpnam 的参数,它指向至少 L_tmpnam 字节的字符数组的指针
  • 函数生成至多 TMP_MAX 个独有文件名(TMP_MAX 指明了程序期间由 tmpnam 函数产生的临时文件的最大数量。)
  • 如果生成文件名失败,返回空指针

七 文件缓冲

向磁盘驱动器传入数据或者从磁盘驱动器传出数据都是相对较慢的操作。因此,在每次程序想读或写字符时都直接访问磁盘文件是不可行的。获得较好性能的诀窍就是缓冲(buffering):把写入流的数据存储在内存的缓冲区域内;当缓冲区满了(或者关闭流)时,对缓冲区进行“清洗”(写入实际的输出设备)。输入流可以用类似的方法进行缓冲:缓冲区包含来自输入设备的数据,从缓冲区读数据而不是从设备本身读数据。缓冲在效率上可以取得巨大的收益,因为从缓冲区读字符或者在缓冲区内存储字符几乎不花什么时间。当然,把缓冲区的内容传递给磁盘,或者从磁盘传递给缓冲区是需要花时间的,但是一次大的“块移动”比多次小字节移动要快很多。

<stdio.h>中的函数会在缓冲有用时自动进行缓冲操作。缓冲发生在后台,我们通常不需要关心它的操作。然而,极少情况需要我们起到更主动的作用。

当程序向文件中写输出时,数据通常会向放在缓冲区。当缓冲区满了或关闭了文件时,缓冲区会自动清洗。

int fflush(FILE *stream );

对于输出流(及最后操作为输出的更新流),从 stream 的缓冲区写入未写的数据到关联的输出设备。

对于输入流(及最后操作为输入的更新流),行为未定义。

stream 是空指针,则冲入所有输出流,包括操作于库包内者,或在其他情况下程序无法直接访问者。

参数:stream - 要写出的文件流

返回值:成功时返回零。否则返回 EOF

调用:

fflush(fp);

为和 fp 相关联的文件清洗了缓冲区。调用:

fflush(NULL);

清洗了全部输出流。

int setvbuf(FILE *stream, char *buffer, int mode, size_t size );(C99 前)

int setvbuf(FILE *restrict stream, char *restrict buffer, int mode, size_t size );(C99 起)

mode 所指示值更改给定文件流 stream 的缓冲模式。另外,

  • buffer 为空指针,则重设内部缓冲区大小为 size
  • buffer 不是空指针,则指示流使用始于 buffer 而大小为 size 的用户提供缓冲区。必须在 buffer 所指向的数组的生存期结束前(用 fclose )关闭流。成功调用 setvbuf 后,数组内容不确定,而任何使用它的尝试是未定义行为。

参数:

stream - 要设置缓冲的文件流
buffer - 指向要使用的流缓冲区的指针,或若仅更改大小和模式则为空指针
mode - 使用的缓冲模式。它能是下列值之一:_IOFBF全缓冲:当缓冲区为空时,从流读入数据。或者当缓冲区满时,向流写入数据。_IOLBF行缓冲:每次从流中读入一行数据或向流中写入一行数据。_IONBF无缓冲:直接从流中读入数据或直接向流中写入数据,缓冲设置无效。
size - 缓冲区的大小

**返回值:**成功时为 0 ,失败时为非零。

例如,下面这个 setvbuf 函数的调用利用 buffer 数组中的 N 个字节作为缓冲区,而把 stream 的缓冲变成了满缓冲:

char buffer[N];
...
setvbuf(stream, buffer, _IOFBF, N);

**注意:**此函数仅可在已将 stream 关联到打开的文件后,但要在任何其他操作前使用。

void setbuf(FILE *stream, char *buffer );(C99前)

void setbuf(FILE *restrict stream, char *restrict buffer );(C99起)

设置用于流操作的内部缓冲区。其长度至少应该为 BUFSIZ 个字符。

buffer 非空,则等价于 setvbuf(stream, buffer, _IOFBF, BUFSIZ) 。

buffer 为空,则等价于 setvbuf(stream, NULL, _IONBF, 0) ,这会关闭缓冲。

参数:

stream - 要设置缓冲区的文件流
buffer - 指向文件流所用的缓冲区的指针。若提供 NULL ,则关闭缓冲。

我们把 setbuf 看作是陈旧的内容,不建议大家在新程序中使用。

注意:使用 setvbuf 函数或 setbuf 函数时,一定要确保在释放缓冲区之前已经关闭了流。特别是,如果缓冲区是局部函数的,并且具有自动存储期限,一定要确保在函数返回前关闭流。

八 其他文件操作

int remove( const char *fname );

删除 fname 所指向的字符串所标识的文件。

参数:

fname - 指向空终止字符串的指针,字符串含标识待删除文件的路径

**返回值:**成功时为 0 ,错误时为非零值。

remove("foo");

如果程序使用 fopen 函数来创建临时文件,那么它可以调用 remove 函数在程序终止前删除此文件。一定要确保已经关闭了要移除的文件,因为对于当前打开的文件,移除文件的效果是由实现定义的。

int rename( const char *old_filename, const char *new_filename );

更改文件的文件名。该文件以 old_filename 所指向的字符串标识。新文件名以 new_filename 所指向的字符串标识。

new_filename 存在,则行为是实现定义的。

返回值:

成功时为 0 ,失败时为非零值。

rename("foo", "bar");

对于用 fopen 函数创建的临时文件,如果程序需要决定使文件变为永久的,那么用 rename 函数改名是很方便的。

**注意:**如果打开了要改名的文件,一定要确保在调用 rename 函数之前关闭了此文件。对打开的文件执行改名操作会失败。

三 格式化的输入输出

1. printf系

头文件: stdio.h
(1)
int printf( const char *format, ... )(C99 前)

int printf( const char *restrict format, ... );(C99 起)

(2)

int fprintf( FILE *stream, const char *format, ... );(C99 前)

int fprintf( FILE *restrict stream, const char *restrict format, ... );(C99 起)

(3)

int sprintf( char *buffer, const char *format, ... );(C99 前)

int sprintf( char *restrict buffer, const char *restrict format, ... );(C99 起)

(4)

int snprintf( char *restrict buffer, int bufsz, const char *restrict format, ... );(C99 起)

定义:
从给定位置加载数据,转换为字符串等价物,并写结果到各种池。

  1. 写结果到 stdout 。

  2. 写结果到文件流 stream 。

  3. 写结果到字符串 buffer 。

  4. 写结果到字符串 buffer 。至多写 buf_size - 1 个字符。产生的字符串会以空字符终止,除非 buf_size 为零。若 buf_size 为零,则不写入任何内容,且 buffer 可以是空指针,然而依旧计算返回值(会写入的字符数,不包含空终止符)并返回。

参数:
stream - 要写入的输出文件流

buffer - 指向要写入的字符串的指针

bufsz - 最多会写入 bufsz - 1 个字符,再加空终止符

format - 指向指定数据转译方式的空终止多字节字符串的指针。

返回值:
1,2) 传输到输出流的字符数,或若出现输出错误或编码错误(对于字符串和字符转换指定符)则为负值。

  1. 写入到 buffer 的字符数(不计空终止字符),或若输出错误或编码错误(对于字符串和字符转换指定符)发生则为负值。

  2. 假如忽略 bufsz 则本应写入到 buffer 的字符数(不计空终止字符),或若出现输出错误或编码错误(对于字符串和字符转换指定符)则为负值。

printf函数始终向 stdout 写入内容,而fprintf函数向它自己的第一个实际参数指定的流中写入内容:

printf("Total: %d\n", total);
fprintf(fp, "Total: %d\n", total);

printf 等价于将 stdout 作为第一个参数时的 fprintf 。

sprintf前面我们写程序时使用过,它可以向我们创建的数组中写入数据。

比如:

sprintf(date, "%d/%d/%d", 9, 20, 2010);

把 “9/20/2010” 复制到 date 中。当完成字符串写入时,sprintf 函数会添加一个空字符 。

snprintf 和 sprintf 一样,但是增加了一个参数 bufz,写入字符串的字符不会超过 bufz - 1,结尾的空字符不算;只要 bufz 不为 0,都会有空字符。例如:

snprintf(name, 13, "%s, %s", "Einstein", "Albert");

会把 “Einistein, Al” 写入到 name 中。

2. scanf系

·int scanf( const char *format, ... );(C99 前)

int scanf( const char *restrict format, ... );(C99 起)
(2)
int fscanf( FILE *stream, const char *format, ... );(C99 前)

int fscanf( FILE *restrict stream, const char *restrict format, ... );(C99 起)
(3)
int sscanf( const char *buffer, const char *format, ... );(C99 前)

int sscanf( const char *restrict buffer, const char *restrict format, ... );(C99 起)
定义
从各种资源读取数据,按照 format 转译,并将结果存储到指定位置。

  1. 从 stdin 读取数据

  2. 从文件流 stream 读取数据

  3. 从空终止字符串 buffer 读取数据。抵达字符串结尾等价于 fscanf 的抵达文件尾条件

参数:
stream - 要读取的输入文件流

buffer - 指向要读取的空终止字符串的指针

format - 指向指定读取输入方式的空终止字符串的指针。

返回值:
成功赋值的接收参数的数量(可以为零,在首个接收用参数赋值前匹配失败的情况下),者若输入在首个接收用参数赋值前发生失败,则为EOF。

scanf始终从标准输入流 stdin 中读取内容。 fscanf从它的第一个参数指定的流中读入内容:

scanf("%d", &i);
fscanf(fp, "%d", &i);

scanf 等价于将 stdin 作为第一个参数时的 fscanf。

在 C 程序中测试 scanf 的返回值的循环很普遍。下面的循环逐个读入一串整数,在首个遇到问题的符号处停止:

while(scanf("%d", &i) == 1){
...
}

sscanf函数对于从其他输入函数读入的字符串中提取数据非常方便。例如可以使用 fgets 函数来获取一行输入,然后把此行数据传递给 sscanf 函数进一步处理:

fgets(str, sizeof(str), stdin);
sscanf(str, "%d%d", &i, &j);

使用 sscanf 的好处之一是,可以按需要多次检验输入行,而不再是一次。

下面的程序可以实现既可以按照 “月/日/年” 的格式也可以按照 "月-日-年"的格式读取日期:

if(sscanf(str, "%d/%d/%d", &month, &day, &year) == 3)
...
else if(sscanf(str, "%d-%d-%d", &month, &day, &year) == 3)
...
else
printf("Date not in proper form.\n");

如果在找到第一个数据项之前到达了字符串末尾(用空字符标记),那么 sscanf 函数返回 EOF 。

3. 检测文件末尾和错误条件

void clearerr(FILE *stream );

头文件: <stdio.h>

**定义:**重置给定文件流的错误标志和 EOF 指示器。

参数:stream - 要重置错误标志的文件流

int feof(FILE *stream );

头文件: <stdio.h>

**定义:**检查是否已抵达给定文件流的结尾。

**参数:**stream - 要检验的文件流

返回值: 若已抵达流尾则为非零值,否则为 0

int ferror(FILE *stream );

头文件: <stdio.h>

**定义:**检查给定文件流的错误。

**参数:**stream - 要检验的文件流

返回值: 若文件流已出现错误则为非零值,否则为 0

如果要求 ...scanf函数读入并存储 n 个数据项,那么希望它的返回值就是 n 。如果返回值小于 n ,那么一定是出错了。一共有 3 种可能:

  • 文件末尾 。函数再完全匹配格式字符串之前遇到了文件末尾。
  • 读取错误 。函数不能从流中读取字符。
  • 匹配失败 。数据项的格式是错误的。

每个流都有与之相关的两个指示器:错误指示器(error indicator)和文件末尾指示器(end-of-file indicator),当打开流时会清除这些指示器。遇到文件末尾就设置文件末尾指示器,遇到读错误就设置错误指示器。(输出流上的写错误也设置错误指示器。)匹配失败不会改变任何一个指示器。

一旦设置了错误指示器和文件末尾指示器,它就会保持这种状态直到被显示清除(可通过 clearerr 函数)。clearerr 会同时清除两个指示器:

clearerr(fp);

我们可以调用 feof函数和 ferror函数来测试的指示器,从而确定出先前在流上的操作失败的原因。

为了清楚这两个函数的用法,我们现在来编写一个函数。此函数用来搜索文件以整数起始的行。下面时函数调用的方式:

n = find_int("foo");

其中,"foo"要搜索的文件的名字。

int find_int(const char* filename) {

FILE* fp = fopen(filename, "r");
int n;

if (fp == NULL)
return -1; // can't open file

while (fscanf(fp, "%d", &n) != 1) {
if (ferror(fp)) {
fclose(fp);
return -2; // input error
}
if (feof(fp)) {
fclose(fp);
return -3; // interger not find
}
fscanf(fp, "%*[^\n]"); // skips rest of lines
}

fclose(fp);

return n;
}

注意转换说明%*[^\n]跳过全部字符直到下一个换行符为止的用法。

四 字符的输入\输出

本节种的函数把字符作为 int 型而非 char 型的值来处理。这样做的原因是输入函数通过返回 EOF 来说明文件末尾(或错误)情况的,而 EOF 又是一个负的整数常量。

0. 输出函数

int fputc(int c, FILE* stream);

int putc(int c, FILE* stream);

int putchar(int c);

putchar函数向标准输入流 stdout 写入一个字符:

putchar(ch);

fputcputc是 putchar 向任意流写字符的通用版本:

fputc(ch, fp);
putc(ch, fp);

虽然 putc 和 fputc 工作原理相同,但是 putc 通常定义为宏,而 fputc 则只作为函数实现。putchar 本身也定义为宏:

#define putchar(c) putc((c), stdout);

宏有一个潜在的问题(比如有参数有副作用)。程序员偏好使用 putc,因为它的速度较快。

如果出现错误,那么上述三个函数都会为流设置错误指示器并返回 EOF;否则,它们都会返回写入的字符。

1. 输入函数

int fgetc(FILE *stream);

int getc(FILE *stream);

int getchar(void);

int ungetc(int c, FILE *stream);

前三个函数和上面类似。

这三个函数都把字符看作 unsigned char 类型的值(返回之前转换为 int)。因此它们不会返回除 EOF 外的负值。

#define getchar() getc(stdin);

同样的,getc 执行速度更快。

这三个函数:若文件尾条件导致失败,则另外设置 stream 上的文件尾指示器 。若某些其他错误导致失败,则设置 stream 上的错误指示器。可以使用 feof 和 ferror 分别这两种情况。

惯用法:

while((ch = getc(fp)) != EOF){

}

ungetc函数把从流中读入的字符“返回”并清除流的文件末尾指示器。如果在程序中需要多向前看一个字符,这种能力可能会十分有效。比如,读入一系列数字,并在首个非数字时停止:

while(isdigit(ch = getc(fp))){

}
ungetc(ch, fp);

调用文件定位函数(fseek, fsetpos 或 rewind)会导致放回的字符丢失。

ungetc 返回要求放回的字符。如果试图放回 EOF 或试图放回超过最大允许数量的字符数,ungetc 会返回 EOF 。

我认为还是有必要再来仔细看看 ungetc 定义的:

int ungetc( int ch, FILE *stream );

**定义:**若 ch 不等于 EOF ,则推入字符 ch (转译为 unsigned char )到与流 stream 关联的输入缓冲区,方式满足从 stream 的后继读取操作将取得该字符。不修改与流关联的外部设备。

流重寻位操作 fseekfsetposrewind 弃去 ungetc 的效果。

若调用 ungetc 多于一次,而无中间读取或重寻位,则可能失败(换言之,保证大小为 1 的回放缓冲区,但任何更大的缓冲区是实现定义的)。若成功进行多次 ungetc ,则读取操作以 ungetc 的逆序取得回放的字符。

ch 等于 EOF ,则操作失败而不影响流。

ungetc 的成功调用清除文件尾状态标志 feof

在二进制流上对 ungetc 的成功调用将流位置指示器减少一(若流位置指示器为零,则行为不确定)。

在文本流上对 ungetc 的成功调用以未指定方式修改流位置指示器,但保证在以读取操作取得所有回放字符后,流位置指示器等于其在 ungetc 之前的值。

参数:

ch - 要推入输入流缓冲区的字符

stream - 要回放字符到的文件流

返回值:

成功时返回 ch

失败时返回 EOF ,而给定的流保持不变。

**示例:**展示 ungetc 的原目的:实现 scanf

#include <ctype.h>
#include <stdio.h>

void demo_scanf(const char* fmt, FILE* s) {
if(*fmt == '%') {
int c;
switch(*++fmt) {
case 'u': while(isspace(c=getc(s))) {} // 跳过空白符
unsigned int num = 0;
while(isdigit(c)) {
num = num*10 + c-'0';
c = getc(s);
}
printf("%%u scanned %u\n", num);
ungetc(c, s); // 重处理非数字
case 'c': c = getc(s);
printf("%%c scanned '%c'\n", c);
}
}
}

int main(void)
{
FILE* f = fopen("input.txt", "w+");
fputs("123x", f);
rewind(f);
demo_scanf("%u%c", f);
fclose(f);
}

输出:

%u scanned 123
%c scanned 'x'

程序:复制文件

下面的程序用来进行文件的复制操作。当程序执行时,在命令行上指定原始文件名和新文件名。例如,将文件 f1.c 复制给文件 f2.c,使用命令:

fcopy f1.c f2.c

如果命令行上的文件名不是两个,或者至少有一个文件无法打开,那么程序 fcopy 都将产出出错消息。

fcopy.c

#include<stdio.h>
#include<stdlib.h>

int main(int argc, char* argv[]) {

FILE* src_fp, * dest_fp;
int ch;

if (argc != 3) {
fprintf(stderr, "usage: fcopy source dest\n");
exit(EXIT_FAILURE);
}

if ((src_fp = fopen(argv[1], "rb")) == NULL) {
fprintf(stderr, "Can't open file %s\n", argv[1]);
exit(EXIT_FAILURE);
}

if ((dest_fp = fopen(argv[2], "wb")) == NULL) {
fprintf(stderr, "Can't open file %s\n", argv[2]);
fclose(src_fp);
exit(EXIT_FAILURE);
}

while ((ch = getc(src_fp)) != EOF)
putc(ch, dest_fp);

fclose(src_fp);
fclose(dest_fp);

return 0;
}

采用"rb""wb"作为文件模式使 fcopy 既可以复制文本文件也可以复制二进制文件。

五 行的输入\输出

0. 输出函数

int fputs( const char *str, FILE *stream );(C99前)

int fputs( const char *restrict str, FILE *restrict stream );(C99起)

头文件:<stdio.h>

**定义:**将以NULL结尾的字符串 str 的每个字符写入到输出流 stream ,如同通过重复执行 fputc

不将 str 的空字符写入。

参数:

str - 要写入的空终止字符串

stream - 输出流

返回值:

成功时,返回非负值。

失败时,返回 EOF 并设置 stream 上的错误指示器。

注意:

相关函数 puts 后附新换行符到输出,而 fputs 写入不修改的字符串。

不同的实现返回不同的非负数:一些返回最后写入的字符,一些返回写入的字符数(或若字符串长于 INT_MAX 则为该值),一些简单地非负常量,例如零。

对注意的理解:

puts("Hello ");
puts("World");

fputs("Hello ", stdout);
fputs("World", stdout);

输出:

Hello
World
Hello World

int puts( const char *str );

头文件:<stdio.h>

定义:写入每个来自空终止字符串 str 的字符及附加换行符 '\n’ 到输出流 stdout ,如同以重复执行 putc 写入。

不写入来自 str 的空终止字符。

参数: str - 要写入的字符串

返回值:

成功时返回非负值

失败时,返回 EOF 并设置 stdout错误指示器

注意:

puts 函数后附一个换行字符到输出,而 fputs 不这么做。

不同的实现返回不同的非负数:一些返回最后写入的字符,一些返回写入的字符数(或若字符串长于 INT_MAX 则返回它),一些简单地返回非负常量。

在重定向 stdout 到文件时,导致 puts 失败的典型原因是用尽了文件系统的空间。

1. 输入函数

char* fgets( char *str, int count, FILE *stream );(C99前)

char* fgets( char *restrict str, int count, FILE *restrict stream );(C99起)

**定义:**从给定文件流读取最多 count - 1 个字符并将它们存储于 str 所指向的字符数组。若文件尾出现或发现换行符则终止分析,后一情况下 str 将包含一个换行符。若读入字节且无错误发生,则紧随写入到 str 的最后一个字符后写入空字符。

参数:

str - 指向 char 数组元素的指针

count - 写入的最大字符数(典型的为 str 的长度)

stream - 读取数据来源的文件流

返回值:

成功时为 str ,失败时为空指针。

若遇到文件尾条件导致了失败,则设置 stream 上的文件尾指示器(见 feof() )。这仅若它导致未读取字符才是失败,该情况下返回空指针且不改变 str 所指向数组的内容(即不以空字符覆写首字节)。

若某些其他错误导致了失败,则设置 stream 上的错误指示器(见 ferror() )。 str 所指向的数组内容是不确定的(甚至可以不是空终止)。

fgets(str, sizeof(str), fp);

char *gets( char *str );

**定义:**从 stdin 读入 str 所指向的字符数组,直到发现换行符或出现文件尾。在读入数组的最后一个字符后立即写入空字符。换行符被舍弃,但不会存储于缓冲区中。

参数: str - 要被写入的字符串

返回值:

成功时为 str ,失败时为 NULL

若文件尾条件导致了失败,则附加设置 stdin 的文件尾指示器。若其他某些原因导致了失败,则设置 stdin 的错误指示器。

注意:gets() 函数不进行边界检查,从而此函数对缓冲区溢出攻击极度脆弱。无法安全使用它(除非程序运行的环境限定能出现在 stdin 上的内容)。因此,此函数在 C99 的第三次勘误中被弃用,而在 C11 标准发布时被移除。推荐的替代品是 fgets()gets_s()

绝对不要用 gets()

用 fgets 代替 gets:

fgets(str, sizeof(str), stdin);

六 块的输入/输出

size_t fread( void *restrict buffer, size_t size, size_t count, FILE *restrict stream );
定义于头文件 <stdio.h>
参数
buffer - 指向要读取的数组中首个对象的指针
size - 每个对象的字节大小
count - 要读取的对象数
stream - 读取来源的输入文件流
返回值
成功读取的对象数,若出现错误或文件尾条件,则可能小于 count 。
若 size 或 count 为零,则 fread 返回零且不进行其他动作。
fread 不区别文件尾和错误,而调用者必须用 feof 和 ferror 鉴别出现者为何。


定义
从给定输入流 stream 读取至多 count 个对象到数组 buffer 中,如同以对每个对象调用 size 次 fgetc ,并按顺序存储结果到转译为 unsigned char 数组的 buffer 中的相继位置。流的文件位置指示器前进读取的字符数。
若出现错误,则流的文件位置指示器的结果值不确定。若读入部分的元素,则元素值不确定。

写整个数组 a 的内容到文件 fp 中:

fwrite(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);

size_t fwrite( const void *restrict buffer, size_t size, size_t count, FILE *restrict stream );
定义于头文件 <stdio.h>
参数
buffer - 指向数组中要被写入的首个对象的指针
size - 每个对象的大小
count - 要被写入的对象数
stream - 指向输出流的指针
返回值
成功写入的对象数,若错误发生则可能小于 count 。
若 size 或 count 为零,则 fwrite 返回零并不进行其他行动。


定义
写 count 个来自给定数组 buffer 的对象到输出流stream。如同转译每个对象为 unsigned char 数组,并对每个对象调用 size 次 fputc 以将那些 unsigned char 按顺序写入 stream 一般写入。文件位置指示器前进写入的字节数。

从文件 fp 中读入整个数组:

fread(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);

七 文件定位

int fgetpos( FILE *restrict stream, fpos_t *restrict pos );(C99)

**定义:**获得文件流 stream 的文件位置指示器和当前分析状态(若存在),并将它们存储于 pos 所指向的对象。存储的值仅在作为 fsetpos 的输入的情况有意义。

参数: stream - 要检验的文件流

​ pos - 指向要存储文件位置指示器到的 fpos_t 对象的指针

**返回值:**成功时为 0 ,否则非零值。

int fsetpos( FILE *stream, const fpos_t *pos );(C99)

**定义:**按照 pos 所指向的值,设置文件流 stream 的文件位置指示器和多字节分析状态(若存在)。

除了建立新的分析状态和位置,调用此函数还会撤销 ungetc 的效果,并若设置了文件尾状态则清除之。

若读或写出现错误,则设置流的错误指示器( ferror )。

**参数:**stream - 要修改的文件流

​ pos - 指向 fpos_t 对象的指针,用作文件位置指示器的新值

**返回值:**成功时为 0 ,否则为非零值。

int fseek( FILE *stream, long offset, int origin );
定义于头文件 <stdio.h>
参数
stream - 要修改的文件流
offset - 相对 origin 迁移的字符数
origin - offset 所加上的位置。它能拥有下列值之一: SEEK_SET 、 SEEK_CUR 、 SEEK_END
返回值
成功时为 0 ,否则为非零。


定义
设置文件流 stream 的文件位置指示器为 offset 所指向的值。

若 stream 以二进制模式打开,则新位置准确地是文件起始后(若 origin 为 SEEK_SET )或当前文件位置后(若 origin 为 SEEK_CUR ),或文件结尾后(若 origin 为 SEEK_END )的 offset 字节。不要求二进制流支持 SEEK_END ,尤其是是否输出附加的空字节。

若 stream 以文本模式打开,则仅有的受支持 offset 值为零(可用于任何 origin )和先前在关联到同一个文件的流上对 ftell 的调用的返回值(仅可用于 SEEK_SET 的 origin )。

若 stream 为宽面向,则一同应用对文本和二进制流的限制(允许 ftell 的结果与 SEEK_SET 一同使用,并允许零 offset 以 SEEK_SET 和 SEEK_CUR 但非 SEEK_END 为基准)。

除了更改文件位置指示器, fseek 还撤销 ungetc 的效果并清除文件尾状态,若可应用。

若发生读或写错误,则设置流的错误指示器( ferror )而不影响文件位置。

SEEK_SET 从头开始
SEEK_CUR 从当前位置开始
SEEK_END 从尾开始

long ftell( FILE *stream );
定义于头文件 <stdio.h>
参数:stream - 要检验的文件流
返回值
成功时为文件位置指示器,若失败发生则为 -1L 。
失败时,设 errno 对象为实现定义的正值。


定义
返回流 stream 的文件位置指示器。
若流以二进制模式打开,则由此函数获得的值是从文件开始的字节数。
若流以文本模式打开,则由此函数返回的值未指定,且仅若作为 fseek() 的输入才有意义。

void rewind( FILE *stream );

**定义:**移动文件位置指示器到给定文件流的起始。

函数等价于 fseek(stream, 0, SEEK_SET); ,除了它清除文件尾和错误指示器。

此函数丢弃任何来自先前对 ungetc 调用的效果。

参数: stream - 要修改的文件流

此例演示如何读文件两次

#include <stdio.h>

char str[20];

int main(void)
{
FILE *f;
char ch;

f = fopen("file.txt", "w");
for (ch = '0'; ch <= '9'; ch++) {
fputc(ch, f);
}
fclose(f);

f = fopen("file.txt", "r");
fread(str, 1, 10, f);
puts(str);

rewind(f);
fread(str, 1, 10, f);
puts(str);
fclose(f);

return 0;
}

每个流都有相关联的文件位置(file position)。打开文件时,会将文件位置设置在文件的起始处。(但如果文件按“追加”模式打开,初始的文件位置可以在文件起始处也可以在文件末尾,这依赖于具体的实现。)然后,在执行读或写操作时,文件位置会自动推进,并且允许按照顺序贯穿整个文件。

虽然对许多应用来说顺序访问是很好的,但是某些程序需要具有在文件中跳跃的能力,即可以在这里访问一些数据又可以到那里访问其他数据。例如,如果文件包含一系列记录,我们可能希望直接跳到特定的记录处,并对其进行读或更新。<stdio.h>通过提供5个函数来支持这种形式的访问,这些函数允许程序确定当前的文件位置或者改变文件的位置。

fseek 函数改变与第一个参数(即文件指针)相关的文件位置。第三个参数说明新位置是根据文件的起始处、当前位置还是文件末尾来计算。<stdio.h>为此定 义了三种宏:

第二个参数是个(可能为负的)字节计数。例如,为了移动到文件的起始处,搜索的方向将为SEEK SET,而且字节计数为零:

fseek(fp, oL, SEBK SET); /* moves to beginning of */

为了移动到文件的末尾,搜索的方向则应该是SEEK_ END:

fseek(fp, OL, SEEK END);/* moves to endof file */ 

为了往回移动10个字节,搜索的方向应该为SEK CUR,并且字节计数要为10:

fseek(fp, -10L, SEERK _CUR); /* moves back 10 bytes */

注意,字节计数是long int类型的,所以这里用 0L 或 -10作为实参。(当然, 用 0 和 10 也可以, 因为参数会自动转化为正确的类型。)

通常情况下,fseek 函数返回零。如果产生错误(例如,要求的位置不存在),那么fseek函数就会返回非零值。

顺便提一句,文件定位函数最适合用于二进制流。C语言不禁止程序对文本流使用这些定位函数,但是由于操作系统的差异要小心使用。fseek 函数对流是文本的还是二进制的很敏感。对于文本流而言,要么offset (fseek的第 二个参数)必须为零,要么 origin (fseek的第三个参数)必须是SEK_ SET,且 offset 的值 通过前面的 ftell 函数调用获得。(换句话说,我们只可以利用 fseek 函数移动到文件的起始处或者文件的末尾处,或者返回前面访问过的位置。)

对于二进制流而言,fseek 函数不要求支持 origin 是 SEEK_ END 的调用。ftell 函数以长整数返回当前文件位置。(如果发生错误,ftell 函数会返回 -1L,并且把错误码存储到 errno 中。) ftell 可能会存储返回的值并且稍后将其提供给 fseek 函数调用,这也使返回前面的文件位置成为可能:

long file pos;
...
file pos = ftell(fp); /* saves current position */
...
fseek (fp, file. pos, SEEK SET); /* returns to old position */

如果fp是二进制流,那么 ftell(fp) 调用会以字节计数来返回当前文件位置,其中零表示文件的起始处。 但是,如果 fp 是文本流,ftell(fp) 返回的值不 一定是字节计数,因此最好不要对 ftell 函数返回的值进行算术运算。例如,为了查看两个文件位置的距离而把 ftell 返回的值相减不是个好做法。

rewind 函数会把文件位置设置在起始处,调用 rewind(fp) 几乎等价于fseek( fp, 0L, SEEK_SET),两者的差异是 rewind函数不返回值,但是会为fp清除错误指示器。

fseek 函数和 ftell 函数都有一个问题:它们只能用于文件位置可以不存储在长整数中的文件。为了用于非常大的文件,C语言提供了另外两个函数:fgetpos 函数和 fsetpos 函数。两个函数可以用于处理大型文件,因为它们用 fpos_t 类型的值来表示文件位。fpos_ t 类型值不一定就是整数,比如,它可以是结构。

调用 fgetpos(fp, &file_pos)会把与 fp 相关的文件位置存储到 file_pos变量中。调用 fsetpos(fp, &file_pos)会为 fp 设置文件的位置,此位置是存储在 file_pos 中的值。(此值必须通过前面 fgetpos 调用获得。)如果 fgetpos 函数或者 fsetpos 函数调用失败,那么都会把错误码存储在 errno 中。当调用成功时,这两个函数都会返回零;否则,都会返回非零值。

下面是使用 fgetpos 和 fsetpos 保存文件位置并且稍后返回该位置的方法:

fpos_t file_pos;
...
fgetpos(fp, &file_pos); // saves current position
...
fsetpos(fp, &file_pos); // returns to old position

程序:修改零件记录文件

下面这个程序打开包含 part 结构的二进制文件,把结构读到数组中,把每个结构的成员 on_hand 置为 0,然后再把此结构写回到文件中。注意,程序使用 "rb+"模式打开文件,因此既可以读又可以写。

invclear.c

#include<stdio.h>
#include<stdlib.h>

#define NAME_LEN 25
#define MAX_PARTS 100

struct part {
int number;
char name[NAME_LEN + 1];
int on_hand;
}inventory[MAX_PARTS];

int num_part;

int main() {

FILE* fp;
int i;

if ((fp = fopen("inventory.dat", "rb+")) == NULL) {
fprintf(stderr, "Can't open inventory file.\n");
exit(EXIT_FAILURE);
}

num_part = fread(inventory, sizeof(struct part), MAX_PARTS, fp);

for (i = 0; i < num_part; i++)
inventory[i].on_hand = 0;

rewind(fp);
fwrite(inventory, sizeof(struct part), num_part, fp);

fclose(fp);

return 0;
}

这里调用 rewind 函数是很关键的。在调用完 fread 函数之后,文件位置是在文件的末尾。如果不调用 rewind 就调用 fwrite ,那么 fwrite 将会在文件末尾添加新数据,而不会覆盖旧数据。

[^1]: 要理解一段程序,你得同时成为机器和这段程序。 Epigrams on Programming 编程警句

参考资料:cppreference.com《C语言程序设计:现代方法》