【C-Primer-Plus读书笔记】第13章:文件输入/输出
与文件通信
- 文件是什么
文件通常是在磁盘或固态硬盘上的一段已命名存储区。C把文件看作是一系列连续的字节,每个字节都能被单独读取。由于某些环境中可能无法完全对应这个模型,C提供两种文件模式:文本模式和二进制模式 文本模式和二进制模式
首先要区分文本内容和二进制内容、文本文件格式和二进制格式、以及文件的文本模式和二进制模式。所有文件的内容都以二进制形式(0或1)存储。
- 如果文件最初使用二进制编码的字符(如ASCII或Unicode)表示文本;该文件是文本文件,其中包含文本内容;
- 如果文件中的二进制值代表机器语言代码或数值数据或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容。
Unix用同一种文件格式(文本和二进制是同一种格式)处理文本文件和二进制文件的内容。
- C和Unix在文本中都使用
\n
(换行符)表示换行,程序可使用Unix目录中统计文件大小的计数来确定是否读到文件结尾; - OS X Macintosh文件用
\r
(回车符)表示新的一行; - MS-DOS文件用
\r\n
组合表示新的一行,用嵌入的Ctrl+Z
字符表示文件结尾。
- C和Unix在文本中都使用
C提供两种访问文件的途径:二进制模式和文本模式
- 在二进制模式中,程序可以访问文件的每个字节。
- 在文本模式中,程序所见的内容和文件的实际内容不同。程序会在读取文件时将本地环境的模式转换成C模式以便处理(如上面的换行标识符),反之写入文件时会将C模式转换回本地环境的模式。除了文本模式还能以二进制模式读写文本文件,其不会发生上述转换,显示文件实际内容。
I/O的级别
可以选择I/O的两个级别(即处理文件访问的两个级别):- 底层I/O,使用操作系统提供的基本I/O服务。
- 标准I/O,使用C库的标准包和stdio.h头文件定义。
因为无法保证所有操作系统都使用相同的底层I/O模型,C标准只支持标准I/O包。
标准文件
C程序会自动打开3个文件,它们如下:- 标准输入,系统的普通输入设备,通常为键盘;
- 标准输出,系统的普通输出设备,通常为显示屏;
- 标准错误输出,同上。
标准I/O
相比底层I/O,标准I/O的两个好处:
- 有许多专门的函数简化了处理不同I/O的问题。
- 输入输出都是缓冲的。
检查命令行参数
- 显式的使用
argv[0]
而不是程序名 exit()
函数关闭所有打开的文件并结束程序。exit()
的参数被传递给一些操作系统,以供其他程序使用。通常的惯例是:正常结束的程序传递0,异常结束的程序传递非零值。标准要求0
或宏EXIT_SUCCESS
用于表示成功结束程序,宏EXIT_FAILURE
用于表明程序失败。exit()
与return
的区别是,exit()
直接退出整个程序(控制权交给操作系统),return
把控制权交给上一级调用(如果当前是main函数,上一层就是操作系统,递归另算)。
- 显式的使用
- fopen()函数
fopen()
函数用于打开文件,该函数声明在stdio.h中,它的第一个参数是打开文件的名称,第二个参数是指定待打开文件的模式。
模式字符串 | 含义 |
---|---|
"r" | 读模式 |
"w" | 写模式,覆盖(将文件长度截为0),文件不存在则新建 |
"a" | 写模式,追加,文件不存在则新建 |
"r+" | 更新模式,即可以读写 |
"w+" | 更新模式,即可以读写,覆盖,文件不存在则新建 |
"a+" | 更新模式,即可以读写,追加,文件不存在则新建 |
"rb"、"wb"、"ab"、"ab+"、"a+b"、"wb+"、"w+b" | 同上,但是以二进制模式打开 |
"wx"、"wxb"、"w+x"、"wb+x"、"w+bx" | (C11)类似非x模式,但是如果文件已存在或以独占模式打开文件,则打开文件失败 |
- 如同上面提到的:Unix用同一种文件格式(文本和二进制是同一种格式)处理文本文件和二进制文件的内容。即只有一种文件类型的系统,所以Unix Like系统,带
b
与不带b
模式相同。 C11新增了带
x
字母的写模式,有两点特性:- 如果以传统的一种写模式打开一个现有文件,
fopen()
会把该文件的长度截为0,这样就丢失了该文件的内容。但是使用带x
字母的写模式,即使fopen()
操作失败,源文件的内容也不会被删除。 - 如果环境允许,
x
模式的独占特性使得其他程序或线程无法访问正在被打开的文件。
- 如果以传统的一种写模式打开一个现有文件,
程序成功打开文件后,fopen()
将返回文件指针,其他IO函数可以使用这个指针指定该文件。
- 文件指针的类型是指向
FILE
的指针,FILE
是一个定义在stdio,h
中的派生类型。 - 文件指针并不指向实际的文件,它指向一个包含文件信息的数据对象(C结构),其中包含操作文件的I/O函数所用的缓冲区信息。
getc()
和putc()
函数getc()
和putc()
函数与getchar()
和putchar()
函数类似,不同的是,要告诉getc()
和putc()
函数使用哪一个文件(使用文件指针)。- 文件结尾
如果getc()
函数在读取一个字符时发现是文件结尾,它将返回一个特殊值EOF
。 fclose()
函数fclose(fp)
函数关闭fp
指定的文件,必要时刷新缓冲区。如果关闭成功返回0
,否则返回EOF
。- 指向标准文件的指针
标准文件 | 文件指针 | 通常使用的设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 显示器 |
标准错误 | stderr | 显示器 |
这些文件指针都是指向FILE指针,所以它们可用作标准I/O函数的参数。
文件I/O:fprintf()
、fscanf()
、fgets()
和fputs()
- 文件IO函数
fprintf()
和fscanf()
函数的工作方式与printf()
和scanf()
类似,区别在于前者需要用第1个参数指定待处理的文件。 fgets()
和fputs()
函数
第11章已经讲过,此处不再赘述。rewind()
函数让程序回到文件开始处。该函数接受一个文件指针作为参数。
随机访问:fseek()
和ftell()
- 使用
fseek()
函数,便可把文件看作是数组,在fopen()
打开的文件中直接移动到任意字节处。此函数有3个参数,返回一个int
类型的值。 ftell()
函数返回文件中的当前位置。返回值是一个long
类型数据。
fseek()
和ftell()
的工作原理fseek()
的第1个参数是FILE
指针,指向待查找的文件,fopen()
应该已打开该文件。
fseek()
的第2个参数是偏移量。该参数表示从起点开始要移动的距离。该参数必须是一个long
类型。fseek()
的第3个参数是模式,该参数确定起始点。根据ANSI C标准,在stdio.h
头文件中规定了几个表示模式的明示常量:
模式 | 偏移量的起始点 |
---|---|
SEEK_SET | 文件开始处 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件末尾 |
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,它返回的是当前的位置。
二进制模式和文本模式
一个不同之处在于:- Unix只有一种文件格式,所以不需要进行特殊的转换
- MS-DOS都用
Ctrl-Z
标记文本文件的结尾
以文本模式打开时,C能识别
Ctrl-Z
作为文件结尾标记的字符。但是,以二进制模式打开相同的文件时,Ctrl-Z
字符被看作是文件中的一个字符,而实际的文件结尾符在该字符后面。
另一个不同是:
- Unix使用`\n`表示换行符
- MS-DOS用`\r\n`组合表示文本换行符
> 以文本模式打开相同的文件,C程序把`\r\n`看成`\n`,但是,以二进制模式打开该文件时,程序能看见这两个字符。
可移植性
C模型与Unix模型一致,因为历史上C就是因为Unix而生,但是其他系统不能保证与Unix模型一致。因此,ANSI对这些函数降低了要求,下面是一些限制:- 在二进制模式中,实现不必支持
SEEK_END
模式。 - 在文本模式中,只有以下调用能保证其相应的行为。
- 在二进制模式中,实现不必支持
fgetpos()
和fsetpos()
函数fseek()
和ftell()
的问题在于,它们都把文件大小限制在long类型能表示的范围内。鉴于此,ANSI C新增了两个处理较大文件的新定位函数:fgetpos()
和fsetpos()
。- 这两个函数使用一种新的类型:fpos_t(代表file position type,文件定位类型)。fpos_t类型的变量或数据对象可以在文件中指定一个位置,它不能是数组类型,除此之外没有其他限制。
标准I/O的机理
使用标准I/O的步骤:
调用
fopen()
打开文件。fopen()
不仅打开一个文件,还创建了一个缓冲区(在读写模式下会创建两个缓冲区)以及一个包含文件和缓冲区数据的结构。fopen()
返回一个指向该结构的指针,以便其他函数知道该如何找到该结构。- 假设把该指针赋给一个指针变量,我们就说
fopen()
打开了一个流。如果以文本模式打开该文件,就获得一个文本流;如果以二进制模式打开该文件,就获得一个二进制流。 - 这个结构通常包含一个指定流中当前位置的文件位置指示器。除此之外,它还包含错误和文件结尾的指示器、一个指向缓冲区开始处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)。
调用一个定义在
stdio.h
中的输入函数,如fscanf()
、getc()
或fgets()
。- 调用这些函数,文件中的数据块就被拷贝到缓冲区中。最初调用函数,除了填充缓冲区外,还要设置指针变量所指向的结构中的值。
- 在初始化结构和缓冲区后,输入函数按要求从缓冲区中读取数据。在他读取数据时,文件位置指示器被设置为指向刚读取字符的下一字符。
- 当输入函数发现已读完缓冲区中的所有字符时,会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区中。
- 输出函数以类似的方式把数据写入缓冲区。当缓冲区被填满时,数据将被拷贝至文件中。
其他标准I/O函数
int ungetc(int c, FILE *fp)
函数ungetc()
函数把C指定的字符放回输入流中。如果把一个字符放回输入流,下次调用标准输入函数时将读取该字符。int fflush()
函数
调用fflush()
函数引起输出缓冲区中所有的未写入数据被发送到指针变量指定的输出文件。这个过程称为刷新缓冲区。如果该指针变量是空指针,所有输出缓冲区都被刷新。int setvbuf()
函数setvbuf()
函数创建了一个供标准I/O函数替换使用的缓冲区。在打开文件后且未对流进行其他操作之前,调用该函数。二进制I/O:
fread()
和fwrite()
对于标准I/O,fread()
和fwrite()
函数用于以二进制形式处理数据。实际上,所有的数据都是以二进制形式存储的,甚至连字符都以字符码的二进制表示来存储。- 如果文件中的所有数据都被解释成字符码,则称该文件包含文本数据。
- 如果部分或所有的数据都被解释成二进制形式的数值数据,则称该文件包含二进制数据。
size_t fwrite()
函数fwrite()
函数把二进制数据写入文件。size_t fread()
函数fread()
函数从文件读取二进制数据,该函数返回成功读取项的数量。int feof(FILE *fp)
和int ferror(FILE *fp)
函数
如果标准输入函数返回EOF
,则通常表明函数已到达文件结尾。然而,出现读取错误时,函数也会返回EOF
。feof()
和ferror()
函数用于区分这两种情况。- 当上一次输入调用检测到文件结尾时,
feof()
函数返回一个非零值,否则返回0。 - 当读或写出现错误,
ferror()
函数返回一个非零值,否则返回0。
- 当上一次输入调用检测到文件结尾时,