文件
一个文件通常就是磁盘上的一段命名的存储区。但对于操作系统来说,文件就会更复杂一些。例如,一个大文件可以存储在一些分散的区段中,或者还会包含一些使操作系统可以确定其文件类型的附加数据。
C将文件看成是连续的字节序列,其中每一个字节都可以单独地读取。ANSI C提供了文件的两种视图:文本视图和二进制视图。
1° 文本视图和二进制视图
ANSI要求提供的两种文件视图是文本视图和二进制视图。在二进制视图中,文件中的每个字节都可以为程序所访问。在文本视图中,程序看到的内容和文件的内容有可能不同。举例说明:使用文本视图读取文件时,将把行尾的本地环境表示法映射为C视图。与之类似,在输出的时候,也会将C视图中的行尾表示映射为本地环境表示法。例如,MS-DOS文本文件用回车符和换行符的组合 来表示行尾。Macintosh文本文件只用一个回车符 来表示行尾。C程序使用一个 来表示行尾。所以,如果C程序以文本视图模式处理一个MS-DOS文本文件,在读取文件时它会将 转换为 ,在写入文件的时候它会将 转换为 ;而对于Macintosh文本文件的文本视图,在读取文件时它会将 转换为 ,在写入文件的时候它会将 转换为 。
处理一个MS-DOS文本文件不必局限于仅仅使用文本视图。对这样的文件还可以使用二进制视图。如果是这样,程序将看到文件中的 和 字符,没有任何映射发生。MS-DOS区分文本文件和二进制文件,但C提供的是文本和二进制视图。通常,对于文本文件使用文本视图,对于二进制文件使用二进制视图。但是,您可以使用任何一种视图处理认一种文件,尽管用文本视图处理二进制文件的效果很糟。
ANSI C提供了两种打开文件的模式:①二进制模式②文本模式
2° 标准文件
C程序自动为您打开3个文件。
- 标准输入(standard input)。默认的标准输入是系统的一般输入设备,通常为键盘
- 标准输出(standard output)。
- 标准错误输出(standard error output)。默认的标准输出和标准错误输出是系统的一般输出设备,通常为显示器。
exit()函数
exit()函数关闭所有打开的文件并终止程序。exit()函数的参数会被传递给一些操作系统,包括UNIX、Linux和MS-DOS,以供其他程序使用。通常的约定是正常终止的程序传递值0,非正常终止的程序传递非0值。不同的退出值可以用来标识导致程序失败的不同原因。但并非所有的操作系统都识别相同范围内的可能返回值。所以,ANSI C标准规定使用一个相当有限的最小范围。具体地,该标准要求使用值0或宏EXIT_SUCCESS来指示程序成功终止,使用宏EXIT_FAILURE指示程序非成功终止。这些宏和exit()原型在stdlib.h头文件中都可以找到。
按照ANSI C,在最初调用的main()中使用return和调用exit()的效果相同。所以我们在main()中一直使用语句:(为什么要返回0?)
return 0;
等价于
exit(0);
注意:我们所说的是“最初调用”。如果main()在一个递归程序中,exit()仍然会终止程序;但return将控制权移交给递归的前一级,直到最初的那一级,此时return才会终止程序。return和exit()的再一个区别在于,即使在除main()之外的函数中调用exit(),它也将终止程序。
文件指针
文件指针是一种指向FILE的指针,FILE是stdio.h中定义的一种派生类型。指针并不指向实际的文件,而是指向一个关于文件的信息的数据包,其中包括文件I/O使用的缓冲区信息。主要是:
- 缓冲区的位置
- 缓冲区的当前缓冲能力
- 所使用的文件
程序如果成功地打开一个文件后,fopen()函数返回一个文件指针;如果不能打开文件,fopen()函数返回空指针。
①磁盘已满
②文件名非法
③存储权限不够
④硬件问题——等等都会导致fopen()函数执行失败。
fclose(fp)函数关闭由指针fp指向的文件,同时根据需要刷新缓冲区。更正规的程序也许还要检查是否关闭了文件。如果文件成功关闭,fclose()函数将返回值0,否则返回EOF。
①磁盘已满
②磁盘被移走
③出现I/O错误——等等都会导致fclose()函数执行失败。
getc()函数和putc()函数
需要告诉getc()和putc()函数它们使用的文件。
从标准输入获得一个字符:
ch = getchar();
↓ 等价于
ch = getc(stdin);
输出一个字符到标准输出:
putchar(ch);
↓ 等价于
putc(ch, stdout);
如果在尝试读入字符时发现已经到达文件结尾,getc()函数会返回一个特殊值EOF,所以C程序只有在读取超出文件结尾以后才会发现文件的结尾。
stdio.h文件把3个文件指针与3个C程序自动打开的标准文件进行了关联。如下所示:
标准文件 | 文件指针 | 一般使用的设备 |
标准输入 | stdin | 键盘 |
标准输出 | stdout | 显示器 |
标准错误 | stderr | 显示器 |
文件I/O:fprintf()/fscanf()/fgets()/fputs()
文件I/O函数fprint()和fscanf()的工作方式与printf()和scanf()相似,区别在于前者需要第一个参数来指定要操作的文件。
rewind()
rewind()命令使程序回到文件开始处,注意:rewind()函数接受一个文件指针参数。
fseek()和ftell()
fseek()函数允许您像对待数组那样对待一个文件,在fopen()打开的文件中直接移到任意字节处。
fseek()接受3个参数,返回一个int值。第一个参数是一个指向被搜索文件的FILE指针,应该已经使用fopen()打开了该文件。第二个参数称为偏移量(offset),表示从起始点开始要移动的距离,这个参数必须是一个long类型的值,可以为正(前移→)、负(后移←),也可以为0(保持不动)。第三个参数是模式,用来标识起始点。在ANSI下,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。如果出现有错误出现,例如试图移动超出文件范围,则fseek()的返回值为-1。
ftell()函数以一个long类型值返回一个文件的当前位置。ftell()函数通过返回距文件开始处的字节数目来确定文件的位置。文件的第一个字节到文件开始处的距离是字节0,以此类推。在ANSI C下,这种定义适用于以二进制模式打开的文件。
在DOS下不打印空字符。
标准I/O内幕(最难理解的)
通常使用标准I/O的第一步就是用fopen()打开一个文件,fopen()函数不仅打开一个文件,而且建立了一个缓冲区(在读/写模式下将建立两个缓冲区),还创建了一个包含文件和缓冲区相关数据的数据结构,不仅如此,fopen()函数还返回一个指向该结构的指针,供其他函数知道如何找到该结构。假设把这个值赋给了一个名为fp的指针变量,称fopen()函数打开了一个流。
这个数据结构通常包括一个文件位置指示器,以确定在流中的当前位置。它还包括错误指示器和文件结尾指示器、一个指向缓冲区起始处的指针、一个文件标识符,和一个记录实际复制到缓冲区中的字节数。
着重考虑文件输入,通常,下一步就是调用stdio.h头文件中声明的某个输入函数,比如fscanf()/getc()/fgets()。调用这些函数中的任意一个都会把一块数据从文件复制到缓冲区中。除了填充缓冲区外,初次函数调用还将设置fp所指的结构中的值。特别地,将设置流的当前位置和复制到缓冲区中的字节数。通常当前位置从字节0开始。
数据结构和缓冲区初始化以后,输入函数将从缓冲区中读取所请求的数据。同时,文件位置指示器被置为紧随最后一个被读取字符的位置。因为stdio.h定义的所有输入函数都使用同一个缓冲区,所以任何一个被调函数都将在前一次任何函数调用停止的地方继续开始。
当输入函数检测到已经读取了缓冲区中的全部字符时,它会请求系统将下一块缓冲区大小的数据复制到缓冲区。通过这种方式,输入函数可以读入文件中的全部内容,直到文件结尾。函数在读入最后一缓冲区数据中的最后一个字符后,会将文件结尾指示器的值设置为真,于是下一个被调用的输入函数将返回EOF。
以类似的方式,输出函数将数据写入缓冲区,当缓冲区已满时,就将数据复制到文件中。
int ungetc(int c, FILE *fp)函数
int ungetc(int c, FILE *fp)函数将c指定的字符放回输入流中。如果向输入流中放入一个字符,下一次调用标准输入函数就会读入那个字符。ANSI C标准保证每次只会放回一个字符。
int fflush(FILE *fp)函数
调用fflush()函数可以将缓冲区中任何未写的数据发送到一个由fp指定的输出文件中去。这个过程称为刷新缓冲区。如果fp是一个空指针,将刷新掉所有的输出缓冲。对一个输入流使用fflush()函数的效果没有定义。只要最近一次使用流的操作不是输入操作,就可以对一个更新流(任何读-写模式的流)使用这个函数。
int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size)函数
setvbu()函数建立一个供标准I/O函数使用的替换缓冲区。打开文件以后,在没有对流进行任何操作以前,可以调用这个函数。由指针fp来指定流,buf指向将使用的存储区。如果buf的值不是NULL,就必须创建这个缓冲区。但是,如果buf的值为NULL,函数会自动为自己分配一个缓冲区。size变量为setvbuf()函数指定数组的大小,mode将从下列选项中选取:
- _IOFBF:完全缓冲(缓冲区满的时候刷新)
- _IOLBF:行缓冲(缓冲区满的时候或者一个新行写入的时候刷新)
- _IONBF:无缓冲
如果成功执行,函数会返回0值,否则返回一个非0值。
二进制I/O:fread()/fwrite()函数
如果把数据存储在一个使用与程序具有相同表示方法的文件中,就称数据以二进制形式存储。
如果文件中的全部数据都以字符编码的形式被解读,我们才称该文件包含文本数据。如果这些数据的部分或者全部以二进制形式的数字数据被解读,就称文件包含二进制数据。
通常情况下,还是使用二进制模式将二进制数据存储到二进制格式的文件中,使用以文本模式打开的文本文件中的文本数据。
size_t fwrite(const void * restirct ptr, size_t size, size_t nmemb, FILE * restrict fp)函数
fwrite()函数将二进制数据写入文件。指针ptr是要写入的数据块的地址。size表示要写入的数据块的大小(以字节为单位)。nmemb表示数据块的数目。像一般函数那样,fp指定要写入的文件。例如,要保存一个256字节大小的数据对象(如一个数组)
char buffer[256]; fwrite(buffer, 256, 1, fp);
这一调用将一块256字节大小的数据块从缓冲区写入到文件。再者,要保存一个包含10个double值的数组
double earnings[10]; fwrite(earnings, sizeof(double), 10, fp);
fwrite()函数返回成功写入的项目数。正常情况下,它与nmemb相等,不过如果有写入错误的话,返回值就会小于nmemb。
size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * fp)函数
ptr为读入文件数据的内存存储地址,fp指定要读取的文件。使用这一函数来读取通过fwrite()写入的文件数据。例如,要恢复前一例子中保存的包含10个double值的数组
double earnings[10]; fread(earnings, sizeof(double), 10, fp);
fread()函数返回成功读入的项目数。正常情况下,它与nmemb相等,不过如果有读取错误的话,返回值就会小于nmemb。
一个结构保存的实例
例:把书名保存到一个名为book.dat的文件中。如果该文件已经存在,程序显示文件当前内容,然后允许您向文件中添加内容。
#include <stdio.h> #include <stdlib.h> #define MAXTITL 40 #define MAXAUTL 40 #define MAXBKS 10 struct book { char title[MAXTITL]; char author[MAXAUTL]; float value; }; int main(void) { struct book library[MAXBKS]; int count = 0; int index, filecount; FILE * pbooks; int size = sizeof(struct book); if((pbooks = fopen("book.dat", "a+b")) == NULL) { fputs("Can't open book.dat file ", stderr); exit(1); } rewind(pbooks); while(count < MAXBKS && fread(&library[count], size, 1, pbooks) == 1) { if(count == 0) puts("Current contents of book.dat: "); printf("%s by %s: $%.2f ", library[count].title, library[count].author, library[count].value); count++; } filecount = count; if(count == MAXBKS) { fputs("The book.dat file is full.", stderr); exit(2); } puts("Please add new book titles."); puts("Press [enter] at the start of a line to stop."); while(count < MAXBKS && gets(library[count].title) != NULL && library[count].title[0] != '