C文件I/O超详细教程
本文主要参考了C Primer Plus (5th & 6th Edition)
您可以选择本文的部分内容来读,有些内容对于不熟悉MS-DOS的读者可能过于晦涩难懂。
C语言文件基本知识
文件通常是在磁盘或固态硬盘上的一段已命名的存储区。所有的文件内容都以二进制形式储存。文件分为文本文件和二进制文件。
文件格式 | 定义 | 保存内容 | 示例 |
文本文件 | 文本文件就是最初使用二进制编码字符(如ASCII或Unicode)表示文本的文件 | 文本内容 | .TXT文件 |
二进制文件 | 二进制文件就是文件中的二进制值代表机器语言代码或数值数据的文件 | 二进制内容 | 图片文件,可执行文件 |
C语言提供两种途径访问文件:文本模式和二进制模式。文本模式,顾名思义就是以文本形式访问文件,该文件通常是文本文件。不同的系统,处理文本文件的方式不同。UNIX系统使用'\n'表示换行,而早期的MS-DOS使用'\r'和'\n'组合换行,用Ctrl + Z表示文件结尾。旧式的OS X Macintosh却使用'\r'表示新的一行。这给程序员在不同系统中操作使用文件带来了诸多不便。还好,C语言提供了转换机制。例如在早期的MS-DOS中以文本模式打开某个文件file1.txt,它会自动把\r\n组合转换成'\n',如果要往该文件写入内容时又会把'\n'转换成\r\n组合。当以二进制模式打开文件时,程序将访问到文件的每一个字节,程序员如果需要处理文本文件,就必须根据操作系统的不同而采取不同措施。
文件的结尾标志着文件内容的结束,C语言用宏EOF(End of File)表示文件的结尾,其值通常为-1。
C程序自动打开3个文件,它们是:
文件 | 通常使用的默认设备 | C程序中的表示法 |
标准输入 | 键盘 | stdin |
标准输出 | 显示屏 | stdout |
标准错误输出 | 显示屏 | stderr |
那么如何更改这些文件的默认设备呢?我们可以通过重定向的方法。
MS-DOS重定向
使用带有MS-DOS6.22系统的计算机,假定才C:\user中有如下文件:
文件defin.txt中有如下内容:
insort.exe是一个插入排序的程序。
重定向输入为defin.txt,然后执行程序:
发现其实重定向输入就是把defin.txt的内容与sdtin流相关联。其中'<'是重定向运算符。这样的优势是,如果遇到大量数据输入时,可以先把数据输入到文件,再重定向输入运行程序,十分方便且易于检查和修改错误。可是我们看到,程序运行后死机,原因是程序结尾处有代码getch();,而stdin流已经与defin.txt相关联,本例中defin.txt的末尾处没有可以使getch();执行的内容,且键盘不再是stdin流的默认设备,所以,程序将永远无法退出(单任务纯DOS环境)。由此可见,重定向输入也是有风险的。(同时提醒各位读者,在设计单任务纯DOS下运行的程序时,如无必要,不需要在程序末尾添加暂停指令。必须要有相应的退出指令)
同样,我们也可以重定向输出:
这样,所有的输出都被发送到defout.txt中。其中'>'是重定向输出运算符,它把defout.txt和stdout相关联。打开defout.txt我们可以看到:
这样,我们就把原本输出到stdin默认设备---显示屏上的内容都输入到文件defout.txt中了。但是,你看不到输出的内容,所以重定向输出只在特殊情况下使用,如UNIX服务器。
我们还可以使用组合重定向
其中,<defin.txt和>defout.txt可以调换位置。符号左右的空格如果系统允许可以省略。
需要注意的是,我们不能同时重定向输入或输出多个文件或重定向输入输出与可执行文件关联,使用重定向与文件关联时要保证文件有结尾EOF(End Of File),使用>重定向输出时,输出的文件内的数据将被覆盖。
文件打开fopen()和关闭fclose()函数
使用重定向输入输出来操作文件不仅十分繁琐而且还存在相应的危险。C语言为程序员提供了更加直观方便的文件访问机制,即程序员可以在程序中直接操作文件而不需要去做类似重定向这样的“低级”行为。
我们如果要在程序中操作一个文件,这个文件同该程序在相对的同目录下(如果使用IDE就要到IDE默认路径创建),就可以像这样打开文件:
FILE * fp; //声明文件指针 fp = fopen("file1.txt", "r"); //以只读模式打开文件 if(fp == NULL) exit(EXIT_FAILURE); //如果打开失败就退出
fopen()函数在成功打开后返回指向该文件指针,否则返回NULL。接受两个参数,第一个参数表示文件的名称(包含文件名的字符串地址),第二个参数表示打开模式(这与“文本模式和二进制模式”是一样的,只是细分了这些模式。),常用的打开模式如下:
此外C11还增加了x模式,为了不增加读者的负担,本文不详述。
和动态内存分配一样,我们需要检测文件是否成功打开,而且在执行完相应操作后有必要及时关闭文件。关闭文件我们用fclose(),和free()一样简单,没有返回值:
fclose(fp);//关闭文件 if(fp != 0) exit(EXIT_FAILURE);//如果关闭失败就退出
但是,有时候,文件不能正常关闭,例如程序运行时硬盘有故障或被拔出。因此,我们十分有必要在关闭之后检查一下状态。
文本模式文件I/O
操作单字符---getc()和putc()函数
C Primer Plus(Sixth Edition)对这两个函数的解释非常清楚:{
getc()和putc()函数与getchar()和putchar()函数类似,不同的是,要告诉getc()和putc()函数使用哪一个文件。下面这条语句的意思是“从标准输入中获取一个字符”:
ch = getchar();
而下面这条语句的意思是“从fp指定的文件中获取一个字符”:
ch = getc(fp);
与此类似,下面语句的意思是“把字符ch放入FILE指针fpout指定的文件中”:
putc(ch, fpout);
}
这里我们需要注意getc()的返回值是int类型的,这样是为了返回EOF,虽然有些系统把char类型默认定义signed char,但为了保证程序最大的可移植性,上面代码中ch字符实际上是int类型的。
C程序只有在读到超过文件末尾之后才会发现文件结尾(EOF),所以为了避免空文件被错误读取,我们应在执行相应操作之前尝试读取文件,再根据是否读到EOF 来执行相应操作。
这里插一段:有些读者朋友会问getc()和fgetc()以及putc()和fputc()之间有什么关系?其实在查阅相关文档之后,两组函数几乎是一模一样的,只不过或许getc()和putc()是fgetc()和fputc()的宏实现,原话如下:
所以在调用getc()和putc()时,其参数不能是具有副作用的表达式。例如get(fp++)这样的,这是不允许的!
格式化输出输入到文件---fprintf()和fscanf()函数
fprintf(),fscanf()和printf(),scanf()类似,只不过在原有基础上加了一个参数来确定输出或输入的目标文件。例如,如果有一个FILE指针fp已经指定一个有效的文件,并以"w"模式打开它,我们这样做可以将Hello World输出到这个文件上:
fprintf(fp, "Hello World");
同样,如果有一个字符串"string"被保存在FILE指针fp指定的有效文件的最开始,并以"r"模式打开它,我们这样做将"string"输入到数组st上:
fscanf(fp, "%s", st);
请留意这里的输入输出,它们并不指从标准输入输出设备获得输入输出,而是一种传递的关系:
我们通过C Primer Plus(Fifth Edition)上的一个相关示例来演示这两个函数以及rewind()的具体应用:
字符串输入输出到文件---fgets()和fputs()函数
fgets()函数和fputs()函数之前已在《字符串的输入输出》这篇博文介绍过,只不过现在将它的用途扩展到全体文件。我们假定fp是一个有效的FILE指针,那么从fp指定的文件中读取一段长度为SIZE - 1的字符串(不包括'\0')到数组st,我们可以这样:
fgets(st, SIZE, stdin); //fgets()函数读取SIZE-1个字符或遇到'\n'结束,并把最后一个字符或'\n'之后的字符赋值为'\0',也就是说在某些情况下它保存了'\n'。
同理,fputs()将字符串"Hello World\n"输出到fp所指定的文件:
fputs(fp, "Hello World\n"); //因为fgets()保存了'\n'所以fput()不会自动在字符串末尾加上'\n'
二进制模式文件I/O
文件定位---fseek()和ftell()函数
如果部分读者读到这里可能会吃不消,因为我们发现,与文件操作这块内容相关的函数非常多,很难记忆,不过没有关系,你不必去死记硬背,只要记住函数的大致功能,用到时再去查C标准库参考,久而久之就慢慢会记住了。
ftell()函数返回一个long类型的值,该值表示文件当前位置距离文件开始处的字节数目,以此来确定文件指针当前指向的位置。它接受一个参数,该参数表示一个有效的文件指针。如下代码所示,我们假定fp是一个有效的FILE指针:
long int addr; addr = ftell(fp);
addr就返回文件的当前位置,如下图所示:
由此可见,ftell()将文件当成数组来处理,我们就可以用处理数组的方法来处理文件。但是,这有一个重要的前提,就是文件必须用二进制模式打开,否则将会出现毫无意义的结果。
fseek()函数可以改变文件的当前位置,它实现的前提也应是以二进制模式打开文件。它接受三个参数,第一个参数指明要进行操作的文件指针,第二个参数我们称作偏移量,这是一个long型的参数,如果往文件开始处偏移就是负值,否则为正。第三个参数是模式,用来表示偏移的起点。模式有三种:
我们通过书中的示例来解释这两个函数的应用(因为是以二进制模式打开,所以针对不同的系统,读取文本文件就要有不同的实现代码):
二进制文件I/O---fread()和fwrite()函数
我们先前了解到,如果C程序以二进制模式访问一个文件,那么它将可以访问该文件的所有字节。文件定位的示例中演示了这个特性。那么,为什么需要以二进制模式对文件进行操作呢?假设我们要在文件中存储1/3这个值,我们选择文本模式,那么存储的精度就会大大降低,也更加地麻烦(因为要将数字转换成字符串就必须指定一个精度,读取时也必须指定同样的精度,如果将1/3以0.33存入文件,下次读取就不能恢复它原来的精度)。所以,我们最好的选择就是用相同的位格式来存储值,我们可以使用sizeof(double)个字节的空间来存储1/3,这和程序在内存中存储double变量的位格式相同,相当于拷贝了一个值为1/3的double变量到文件中,读取时按照原本写入时的精度去读取,就可以恢复最大的精度。为了完成上述操作,我们可以使用fread()和fwrite()函数。
fwrite()的原形是:
size_t fwrite(const void *
ptr, size_t
size, size_t
nmemb, FILE *
stream);
其中,ptr指定要写入的数据块的地址(原地址),size表示写入数据块的大小,nmemb表示写入数据块的个数(这能很方便地写入数组),stream指定要写入的文件(目标地址)。例如我们要把1/3这个数以最大精度写入文件pro.dat,我们需要这样做(这一节只给出核心代码):
FILE * fp; double num = 1.0 / 3.0; fp = fopen("pro.dat", "wb"); //以二进制写模式打开文件 if( !fp ) exit(EXIT_FAILURE); fwrite(&num, sizeof(double), 1, fp); //写入二进制数据 if( fclose(fp) != 0 ) //关闭文件 exit(EXIT_FAILURE);
然后,我们查看相对与程序同目录的文件夹发现pro.dat创建成功,并且大小为8字节,正是当前系统double类型变量的大小。
现在,如果我们要读取pro.dat文件里的内容,我们就要使用fread()函数了,因为以文本模式打开这个文件会出现乱码。fread()函数的原形如下:
size_t fread(void *
ptr, size_t
size, size_t
nmemb, FILE *
stream);
同样是四个参数,第一个参数指名要读取到的数据块地址(目标地址),第二三个参数与fwrite()的中对应参数相同,表示读取的大小,最后一个参数指明要读取的文件。
下面的代码演示了如何使用pro.dat文件内容:
FILE * fp; double get; fp = fopen("pro.dat", "rb"); //以二进制读模式打开文件 if( !fp ) exit(EXIT_FAILURE); fread(&get, sizeof(double), 1, fp); //读取文件内容到get if( fclose(fp) != 0 ) //及时关闭文件 exit(EXIT_FAILURE); printf("get * 3.0 = %lf",get * 3.0); //输出
如无意外,代码运行之后会显示:
结语
这篇文章我概括地写出了有关C语言文件的基本操作,如果您手头上正好有一本C Primer Plus(第5或6版),请最好结合着书看本文。
通过本文,我们知道了:
〉〉什么是文件?
〉〉MS-DOS系统下重定向的有关操作和反映问题
〉〉文本的打开和关闭fopen()和fclose()
〉〉文本模式I/O:getc()和putc(),fprintf()和fscanf(),fgets()和fputs()
〉〉文件定位:fseek()和ftell()
〉〉二进制模式I/O:fread()和fwrite()
在本文的编撰过程中,由于本文的篇幅较大,图片较多,部分内容偏难,我耗费了很长时间才完成。文中的错误也是不可避免的,如果您在读完之后发现有什么错误,或是有什么建议,欢迎批评指出!