这一章字数很多,就这部分都快到一万字了(我都不知道我在干嘛)。为了方便您的阅读,我把重点标粗以及背景标绿了(您甚至可以只读标注部分)。
下半部分我将为您展示图形的使用。
-
1. 文件的基本概念
- 一个文件 (file) 通常就是磁盘上的一段命名的存储区。比如 stdio.h 就是一个包含一些有用信息的文件的名称。但对操作系统来说,文件就会更复杂一些。例如,一个大文件可以存储在一些分散的区段中,或者还会包含一些使操作系统可以确定其文件类型的附加数据,但是这些是操作系统而不是程序员 (除非您是在编写操作系统) 要考虑的。您需要考虑的是如何在 C 程序中处理文件。
C 将文件看成是连续的字节序列,其中每一个字节都可以单独地读取。这与 UNIX 环境 (C 的发源地) 中的文件结构是一致的。因为其他的环境中的文件模型可能会有所不同,所以ANSI C 提供了文件的两种视图: 文本视图和二进制视图。 -
1.1 文本文件与二进制文件
- ANSI 要求提供的两种文件视图是文本视图和二进制视图。在二进制视图中,文件中的每个字节都可以为程序所访问。在文本视图中,程序看到的内容和文件的内容有可能不同。例如,使用文本视图读取文件时,将把行尾的本地环境表示法映射为 C 视图。与之类似,在输出的时候,也会将 C 视图中的行尾表示映射本地环境表示法。例如,MS-DOS 文本文件用回⻋符和换行符的组合 \r\n 来表示行尾。Macintosh 文本文件只用一个回⻋符 \r 来表示行尾 。C程序使用一个 \n 表示行尾。所以,如果 C 程序以文本视图模式处理一个 MS-DOS 文本文件,在读取文件时它会将 \r\n 转换为 \n,在写入文件的时候它会将 \n 转换成为 \r\n;而对于Macintosh 文本文件的文本视图,在读取文件时它会将 \r 转换成为 \n,在写入文件的时候它会将 \n 转换成为 \r 。
处理一个 MS-DOS 文本文件不必局限于仅仅使用文本视图。对这样的文件还可以使用二进制视图。如果是这样,程序将看到文件中的 \r 和 \n 字符,没有任何映射发生 (请参考程序 1)。MS-DOS 区分文本文件和二进制文件,但 C 提供的是文本和二进制视图。通常,对于文本文件使用文本视图,对于二进制文件使用二 进制视图。但是,您也可以使用任一种视图处理任一种文件,尽管用文本视图处理二进制文件的效果很糟。 尽管 ANSI C 提供了文本视图和二进制视图,但这两种视图的实现可以是相同的。例如,由于UNIX 仅采用一种文件结构,所 以这两种视图在 UNIX 实现中就是相同的。 - 程序 1:
//一个 MS-DOS 文本文件 Rebecca clutched the\r\n jewel-encrusted scarab\r\n to her heaving bosun.\r\n ^Z //二进制模式打开文件时 C 程序看到的内容 Rebecca clutched the\r\n jewel-encrusted scarab\r\n to her heaving bosun.\r\n ^Z //文本模式打开文件时 C 程序看到的内容 Rebecca clutched the\n jewel-encrusted scarab\n to her heaving bosun.\n
-
1.2 缓冲型文件系统
- 所谓 “缓冲型文件系统” 是指系统自动地在内存区为每个正在使用的文件(如图 1 的 File1)开辟一个缓冲区(如图 1 的 Buffer1)。从磁盘向内存读取数据,则一次从磁盘文件中将一批数据读人到内存级冲区,然后再从级冲区逐个地将数据送到程序中变量所对应的内存空间内。
如果从内存向磁盘输出数据,必须将数据先送到内存中的缓冲区(如图 1 的 Buffer2)集中,待装满级冲区后才将级冲区的全部数据一次送到磁盘中保存。 -
1.3 标准文件
- C 程序自动为您打开 3 个文件。这 3 个文件被称为标准输入 (standard input),标准输出(standard output) 和标准错误输出 (standard error output)。默认的标准输入是系统的一般输入设备,通常为键盘;默认的标准输出和标准错误输出是系统的一般输出设备,通常为显示器。 用标准输入为您的程序提供输入是很自然的事情,它是 getchar()、gets() 和 scanf() 读取的文件。 标准输出是常用的程序输出对象,为 putchar()、puts() 和 printf () 所使用。标准错误输出提供一个可供发送错误消息的逻辑上不同的位置。例如,如果使用重定向方法将输出发送到一个文件中而不是屏幕上,发送到标准错误输出的输出内容仍然会被发送到屏幕上。这样很好,因为如果错误信息被发送到文件的话,那么只有打开文件才可以看到。
-
1.4 文件类型指针
- 在 C 语言中,无论是磁盘文件还是设备文件,都可以通过文件结构类型的数据集合进行输人输出操作。该结构类型是由系统定义的,取名为 FILE。通过文件指针就可对它所指的文件进行各种操作。定义说明文件指针的一般形式为:
其中 FILE 应为大写,它实际上是由系统定义的一个结构,该结构中含有文件名、文件状态和文件当前位置等信息。在编写源程序时不必关心 FILE 结构的细节。习惯上,我们笼统地把 FILE*fp 中的 fp 称为指向一个文件的指针。FILE * 指针变量标识符;
-
表 1:标准文件及与其相关联的文件指针
标准文件 文件指针 一般使用的设备 标准输入 stdin 键盘 标准输出 stdout 显示器 标准错误 stderr 显示器 -
2. 文件的操作
- 文件的处理过程通常要经历如下三个步骤:
打开文件 ——> 文件的读/写 ——> 关闭文件
-
2.1 文件打开函数 fopen()
- 程序使用 fopen() 打开文件。这一函数在 stdio.h 中声明。它的第一个参数是要打开的文件名;更确切地说,是包含该文件名的字符串的地址。第二个参数是用于指定文件打开模式的一个字符串。C 库提供了一些可能的模式,如表 1。
如果不能打开文件,fopen() 函数返回空指针(也是在 stdio.h 中定义的)。如果 fp 为NULL。程序将退出。磁盘已满、文件名非法、存取权限不够或者硬件问题等都会导致 fopen() 函数执行失败。 这就需要查找故障,一个小问题的查找也可能颇费周折。 -
表 2:fopen() 函数的模式字符串
"r" 读:打开文件进行输入操作。该文件必须存在。 "w" 写:为输出操作创建一个空文件。如果已存在同名文件,则丢弃其内容,并将该文件视为新的空文件。 "a" 附加:在文件末尾打开文件进行输出。输出操作始终在文件末尾写入数据,并展开它。重新定位操作(fseek、fsetpos、rewind)将被忽略。如果文件不存在,则创建该文件。 "r+" 阅读/更新:打开文件进行更新(输入和输出)。该文件必须存在。 "w+" 写入/更新:创建一个空文件并打开它进行更新(用于输入和输出)。如果已存在同名文件,则其内容将被丢弃,并将该文件视为新的空文件。 "a+" 附加/更新:打开一个文件进行更新(用于输入和输出),所有输出操作都在文件末尾写入数据。重新定位操作(fseek、fsetpos、rewind)会影响下一个输入操作,但输出操作会将位置移回文件末尾。如果文件不存在,则创建该文件。 "xb" 与前面的模式相似,只是用二进制模式而非文本模式打开文本。( x 表示上述的如 r、w、a、r+ 等字符串)
-
2.2 文件关闭函数 fclose()
- fclose(fp) 函数关闭由指针 fp 指定的文件,同时根据需要刷新缓冲区。更正规的程序也许还要检查 是否成功关闭了文件。如果文件成功关闭,fclose() 函数将返回值 0,否则返回EOF。
磁盘已满、磁盘被移走或者出现 I/O 错误等等都会导致 fclose() 函数执行失败。if (fclose (fp) != 0) printf ("Error in closing file %s\n", argv[1]);
-
3. 文件的 I/O
-
3.1 fprintf 函数和 fscanf 函数
- 文件 I/O 函数 fprintf() 和 fscanf() 的工作方式与 printf() 和 scanf() 相似,区别在于前两者需要第一个参数来指定合适的文件。程序 1 为您演示了这两个文件 I/O 函数以及 rewind() 函数的使用。
- 程序 1:
/*使用 fprintf()、fscanf() 和 rewind() 函数*/ #include<stdio.h> #include<stdlib.h> #define MAX 40 int main(void){ FILE*fp; char words[MAX]; if((fp=fopen("words","a+"))==NULL){ fprintf(stdout,"Can't open \"words\" file.\n"); exit(1); } puts("Eenter words to add to the file: press the Enter"); puts("key at the beginning of a line to terminate."); while(gets(words)!=NULL&&words[0]!='\0') fprintf(fp,"%s ",words); puts("File contents: "); rewind(fp);/*回到文件的开始处*/ while (fscanf(fp,"%s",words)==1) puts(words); if(fclose(fp)!=0) fprintf(stderr,"Error closing file\n"); return 0; }
结果:
Eenter words to add to the file: press the Enter
key at the beginning of a line to terminate.
The fabulous programmer【Enter】
【Enter】
File contents:
The
fabulous
programmer(第一次运行)
Eenter words to add to the file: press the Enter
key at the beginning of a line to terminate.
enchanted the【Enter】
large【Enter】
【Enter】
File contents:
The
fabulous
programmer
enchanted
the
large(第二次运行) -
3.2 fget 函数和 fput 函数
-
char * fgets ( char * str, int num, FILE * stream );
从 steam 中获取字符串
从 steam 中读取字符并将它们作为 C 字符串存储到 str 中,直到读取 (num-1) 个字符或到达换行符或文件末尾,以先发生者为准。
换行符使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。
终止 null 字符会自动附加到复制到 str 的字符之后。
请注意,fgets 与 gets 有很大不同:fgets 不仅接受流参数,还允许指定 str 的最大大小,并在字符串中包含任何结束换行符。
返回成功后,该函数返回 str。
如果在尝试读取字符时遇到文件末尾,则设置 eof 指示符 (feof)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(并且 str 的内容保持不变)。
如果发生读取错误,则设置错误指示符 (ferror),并返回空指针(但 str 指向的内容可能已更改)。
省流:str——输入的字符串;num——读取的个数;stream——文件指针。 -
int fputs ( const char * str, FILE * stream );
将字符串写入 steam
将 str 指向的 C 字符串写入 steam。
该函数从指定的地址 (str) 开始复制,直到到达终止 null 字符 ('\0')。此终止 null 字符不会复制到流中。
请注意,fput 不仅与 put 的不同之处在于可以指定目标流,而且 fput 不会写入其他字符,而 put 会自动在末尾附加一个换行符。
返回成功后,将返回一个非负值。
出错时,该函数返回 EOF 并设置错误指示器 (ferror)。
省流:str——输出字符串;stream——文件指针。 -
3.3 fread 函数和 fwrite 函数
-
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
从 stream 中读取数据块
从 stream 中读取 count 元素的数组,每个元素的 size 为字节大小,并将它们存储在 ptr 指定的内存块中。
stream 的位置指示器按读取的总字节数前进。
如果成功,读取的总字节数为(size*count)。
返回成功读取的元素总数。
如果此数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都设置了正确的指标,可以分别使用 ferror 和 feof 进行检查。
如果 size 或 count 为零,则该函数返回零,并且 ptr 指向的 stream 状态和内容保持不变。
size_t 是无符号整数类型。
省流:ptr——数据块;size——单个读取的字节数;count——读取的个数;stream——输出的文件指针。 -
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
写入要 stream 式传输的数据块
从 ptr 指向的内存块写入一个计数元素数组,每个元素的大小为字节大小。
stream 的位置指示器按写入的总字节数前进。
在内部,该函数将指向的块解释为一个类型的元素数组,并按顺序将它们写入,就好像为每个字节调用了一样。
返回成功写入的元素总数。
如果此数字与 count 参数不同,则写入错误会阻止函数完成。在这种情况下,将为stream设置错误指示器 (ferror)。
如果 size 或 count 为零,则函数返回零,错误指示器保持不变。
省流:ptr——数据块;size——单个读取的字节数;count——读取的个数;stream——输入的文件指针。*3.4 ungetc 函数
-
int ungetc ( int character, FILE * stream );
从 stream 中取消获取字符
一个 character 实际上被放回输入 stream 中,从而减少其内部文件位置,就好像之前的 getc 操作被撤消一样。
此字符可能是也可能不是从前面的输入操作中的 stream 中读取的字符。在任何情况下,从 stream 中检索到的下一个字符是传递给此函数的 character,独立于原始字符。
但请注意,这仅影响该 stream 上的进一步输入操作,而不会影响与其关联的物理文件的内容,该物理文件不会通过对此函数的任何调用进行修改。
某些库实现可能支持多次调用此函数,从而使字符以放回的相反顺序可用。尽管此行为没有标准的可移植性保证,并且在第一次调用之后的任意数量的调用都可能失败。
如果成功,该函数将清除 stream 的文件末尾指示器(如果当前已设置),并在二进制模式下运行时递减其内部文件位置指示器;在文本模式下,位置指示器具有未指定的值,直到读取或丢弃所有使用 ungetc 放回原处的字符。
在 stream 中调用 fseek、fsetpos 或 rewind 将丢弃之前使用此函数放回其中的任何字符。
如果为 character 参数传递的参数为 EOF,则操作将失败,并且输入 stream 保持不变。
返回成功后,返回放回的字符。
如果操作失败,则返回 EOF。
省流:character——将 stream 所指向的字符替换成该字符;stream——文件指针。 -
*3.5 fflush 函数
-
int fflush ( FILE * stream );
刷新 stream
如果给定的 stream 是打开的以供写入(或者如果它是为更新而打开的,并且最后一个 I/O 操作是输出操作),则其输出缓冲区中的任何未写入数据都将写入文件。
如果 stream 为 null 指针,则刷新所有此类 stream。
在所有其他情况下,行为取决于特定的库实现。在某些实现中,刷新打开 stream 以供读取会导致其输入缓冲区被清除(但这不是可移植的预期行为)。
此调用后,stream 将保持打开状态。
当文件关闭时,无论是由于关闭调用还是程序终止,都会自动刷新与其关联的所有缓冲区。
零值表示成功。
如果发生错误,则返回EOF并设置错误指示器(参见ferror)。 -
*3.6 setvbuf 函数
-
int setvbuf ( FILE * stream, char * buffer, int mode, size_t size );
更改 stream 缓冲指定 stream 的缓冲区。该函数允许指定缓冲区的 mode 和 size_t(以字节为单位)。
如果 buffer 为 null 指针,则该函数会自动分配缓冲区(使用 size 作为要使用的大小的提示)。否则,buffer 指向的数组可以用作大小字节的缓冲区。
一旦 stream 与打开的文件关联,但在对它执行任何输入或输出操作之前,应调用此函数。
stream 缓冲区是一个数据块,充当 I/O 操作与与流关联的物理文件之间的中介:对于输出缓冲区,数据将输出到缓冲区,直到达到其最大容量,然后刷新( flushed)(即:所有数据一次发送到物理文件并清除缓冲区)。同样,输入缓冲区从物理文件填充,数据从物理文件发送到操作,直到耗尽,此时从文件中获取新数据以再次填充缓冲区。
可以通过调用 fflush 显式刷新 stream 缓冲区。它们也会通过 fclose 和 freopen 或程序正常终止时自动刷新。
如果已知所有文件不引用交互式设备,则使用默认分配的缓冲区(完全缓冲)打开。此函数可用于重新定义缓冲区大小或模式、定义用户分配的缓冲区或禁用流的缓冲区。
默认情况下,默认流 stdin 和 stdout 是完全缓冲的,如果已知它们不引用交互式设备。否则,默认情况下,它们可能是行缓冲的,也可能是无缓冲的,具体取决于系统和库实现。stderr 也是如此,默认情况下,它始终是行缓冲或无缓冲。
如果缓冲区已正确分配给文件,则返回零值。
否则,将返回非零值;这可能是由于 mode 参数无效或分配缓冲区时出现的其他一些错误。
省流:stream——文件指针;buffer——缓冲区;mode——缓冲模式;size——缓冲区最大容量。表 3:mode 的参数
_IOFBF 全缓冲:在输出时,一旦缓冲区已满(或刷新),就会写入数据。在输入时,当请求输入操作且缓冲区为空时,缓冲区将被填充。 _IOLBF 线路缓冲:在输出时,当换行符插入到流中或缓冲区已满(或刷新)时,无论先发生什么,都会写入数据。在输入时,当请求输入操作且缓冲区为空时,缓冲区将填充到下一个换行符。 _IONBF 无缓冲:不使用缓冲区。每个 I/O 操作都会尽快编写。在这种情况下,将忽略缓冲区和大小参数。 -
4. 文件的函数
-
4.1 随机存取:fseek 函数和 ftell 函数
-
int fseek ( FILE * stream, long int offset, int origin );
重新定位 stream 位置指示器
将与 stream 关联的位置指示器设置为新位置。
对于以二进制模式打开的 stream,新位置是通过向 origin 指定的参考位置添加 offset 来定义的。
对于在文本模式下打开的 steam,offset 应为零或上一次调用 ftell 返回的值,并且必须SEEK_SET源。
如果使用这些参数的其他值调用函数,则支持取决于特定的系统和库实现(不可移植)。
成功调用此函数后,将清除 stream 的文件末尾内部指示器,并删除之前调用此 stream上 ungetc 的所有效果。
在打开更新(读+写)的流上,对 fseek 的调用允许在读取和写入之间切换。
如果返回成功,该函数将返回零。
否则,它将返回非零值。
如果发生读取或写入错误,则设置错误指示符 (ferror)。
省流:stream——文件指针;offset——相对于 origin 的向后偏移量;origin——开始偏移的地方。origin 参考位置 SEEK_SET 文件开头 SEEK_CUR 文件指针的当前位置 SEEK_END 文件末尾 -
long int ftell ( FILE * stream );
获取 stream 中的当前位置
返回 stream 的位置指示器的当前值。
对于二进制 stream,这是从文件开头开始的字节数。
对于文本 stream,数值可能没有意义,但仍可用于稍后使用 fseek 将位置恢复到同一位置(如果使用 ungetc 放回的字符仍在等待读取,则行为未定义)。
返回成功后,返回位置指标的当前值。
失败时,返回 -1L,并将 errno 设置为特定于系统的正值。
省流:stream——文件指针。 -
4.2 fgetpos 函数和 fsetpos 函数
-
fseek() 和 ftell() 的一个潜在的问题是它们限制文件的大小只能在 long 类型的表示范围之内。可能 20 亿字节看起来是一个很大的数字,但是日益增⻓的存储设备容量使得更大的文件也成为可能。ANSI C 引入了两个用来处理较大文件的新的定位函数。这两个函数不是采用long 类型值,而是使用一种称为 fpos_t (代表 file position type,文件定位类型) 的新类型来代表位置。fpos_t 不是一种基本类型,而是通过其他类型定义的。 一个 fpos_t 类型的变量或者数据对象可以用来指定文件中的一个位置,它不能是一种数组类型, 但除此之外不再有其他限制。因此 C 实现可以提供一种满足特殊平台需要的类型:例如,这种类型可以作为结构来实现。
-
int fgetpos ( FILE * stream, fpos_t * pos );
获取 stream 中的当前位置
检索 stream 中的当前位置。
该函数用 stream 的位置指示器所需的信息填充 pos 指向的 fpos_t 对象,以便通过调用 fsetpos 将 stream 恢复到其当前位置(和多字节状态,如果是宽向的)。
函数可用于以整数值的形式检索流中的当前位置。
返回成功后,该函数返回零。
如果出现错误,errno 将设置为特定于平台的正值,并且该函数返回非零值。
省流:stream——文件指针;pos——待填充的位置容器。 -
int fsetpos ( FILE * stream, const fpos_t * pos );
设置 stream 的位置指示器
将 stream 中的当前位置恢复到位置。
与 stream 关联的内部文件位置指示器设置为 pos 表示的位置,pos 是指向 fpos_t 对象的指针,该对象的值应事先通过调用 fgetpos 获得。
成功调用此函数后,将清除 stream 的文件末尾内部指示器,并删除之前调用此流上 ungetc 的所有效果。
在打开更新(读+写)的流上,对 fsetpos 的调用允许在读取和写入之间切换。
类似的函数 fseek 可用于在以二进制模式打开的流上设置任意位置。
如果返回成功,该函数将返回零。
失败时,将返回一个非零值,并将 errno 设置为特定于系统的正值。
省流:stream——文件指针;pos——位置。 -
4.3 feof 函数和 ferror 函数
-
int feof ( FILE * stream );
检查文件结束指示器检查是否设置了与 stream 关联的文件结束指示符,如果设置了,则返回一个不同于零的值。
此指示器通常由尝试读取文件末尾或经过文件末尾的流上的先前操作设置。
请注意,steam 的内部位置指示器可能指向下一个操作的文件末尾,但是,在操作尝试读取该点之前,可能不会设置文件末尾指示器。
通过调用 clearerr、rewind、fseek、fsetpos 或 freopen 来清除此指标。尽管如果位置指标没有被这样的调用重新定位,那么下一个 I/O 操作很可能会再次设置指标。
如果设置了与流关联的文件结束指示符,则返回非零值。
否则,将返回零。 -
int ferror ( FILE * stream );
检查错误指示器检查是否设置了与 stream 关联的错误指示器,如果设置了,则返回一个不同于零的值。
此指示器通常由对失败的 stream 的先前操作设置,并通过调用 clearerr、rewind 或 freopen 来清除。
如果设置了与流关联的错误指示器,则返回非零值。
否则,将返回零。
参考书籍:《C Primer Plus》【美】 Stephen Prata 著
《程序设计教程 用 C/C++ 语言编程》 周纯杰 何顶新 周凯波 彭刚 张惕远 编著