1. 文件类型

在程序设计中,我们一般将文件分为两类:文本文件二进制文件

简单地说,文本文件就是把数据通过某种编码存到硬盘中。比如要存储9这个数字,我们会先将字符'9'通过ASCII编码成57,然后把57的二进制 0011 1001 存到内存中,当我们想从硬盘中再读取这个数据的时候,电脑取出 0011 1001,然后根据编码时候的编码方案ASCII再把这串二进制解码成字符9。

而对于二进制文件,可以理解为它存储的就是数据本身,而不通过任何一种编码。比如说我要存储数字9,我会直接把9的二进制 0000 1001 原样存储在硬盘中,当我们想从硬盘中读取这个数据,电脑取出 0000 1001,直接就是数字9。

从上面的描述可以看出,文本文件和二进制文件的差别并不是体现在物理存储方面的,它们都是在硬盘中存储的二进制。差别更多的是体现在逻辑上的,体现在如何组织数据、存储数据、解释数据。文本文件要通过特定的字符集解码后来得到信息,比如一个.txt文件。而二进制文件存储的就是信息本身,比如一个图片文件中存储了每个像素的像素值以及一些关于图片的控制信息。

文件名

不管是在Linux系统中还是Windows系统中,一个文件都要有一个唯一的文件标识,以便用户识别和引用。在Windows下,文件名包含3部分:文件路径 + 文件名主干 + 文件后缀。例如:

C:/code/test.txt

文件路径一般可以分为绝对路径和相对路径。绝对路径在windows下就像上面一样从盘符开始,一层一层往下直到找到该文件。而相对路径表示从使用这个相对路径的所在位置为基准,一层一层找到对应文件。

注意用Visual Studio编写的C语言程序对于相对路径的解释并不统一。

这是一个VS工程的目录,源程序如.c文件存储在AddressBook文件夹里,而编译后生成的.exe可执行文件存储在Debug文件夹里。此时,如果.c文件里使用了相对路径,那么在VS里直接按F5运行程序,此时的相对路径的基准就是AddressBook文件夹,而如果直接双击运行Debug文件夹里的.exe的话,相对路径的基准就成了Debug文件夹,这就造成了不统一,为了解决这个情况可以先使用“..”退出到上一级目录,这样就统一了。

2. 文件缓冲区

ANSIC标准采用“缓冲文件系统”处理数据文件,所谓缓冲文件系统是指系统自动的在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机写入数据,则从磁盘文件中读取数据输入到内存缓冲区,然后再从缓冲区逐个的将数据送到程序数据区。缓冲区的大小根据C编译系统决定。

3. 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态以及文件的当前位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE

在C语言中打开一个文件会返回一个FILE 类型的指针,即这个指针就指向这个存储文件信息的结构体,从而也就可以通过这个指针来定位一个文件。其他那些操作文件的函数的参数都有这个FILE 指针,所以可以这么说,在C语言中我们通过FILE 指针操作文件,也就可以把这个FILE 称作句柄

4. 文件的打开和关闭

打开文件和关闭文件就像malloc和free一样,必须成对出现,否则就会出现“泄露”。

ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。

文件打开

函数原型:

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

打开filename所表示的文件,并把这个文件和一个流建立联系,这个流可以通过返回的FILE 指针访问,而把FILE 作为参数传入读写函数就可以完成相应文件的读写。

第一个参数是文件的路径,可以采用相对路径或者绝对路径。

第二个参数是打开的方式,即是以读的方式打开文件还是以写的方式打开文件。

mode取值

:-|:-
r|read: 为了读操作打开一个文件,该文件必须存在。
w|write: 为了写操作新建一个空文件,如果文件本身就存在则直接清空原内容。
a|append: 为了在文件末尾追加内容而打开一个文件,如果文件不存在则新建一个空文件。重定位操作(fseek、fsetpos、rewind)被忽略。
r+|read/update: 为了读和更新操作打开一个文件,该文件必须存在。可理解为光标在文件开头,写一个字符就覆盖一个字符。
w+|write/update: 为了读写和更新操作新建一个空文件,如果文件本身就存在则清空原内容。光标同样在开头,写一个字符就覆盖一个字符。
a+|append/update: 为了在文件末尾追加内容而打开一个文件,允许使用重定位操作(fseek、fsetpos、rewind),允许读写。但经测试在调用写函数的时候光标必须在文件末尾(通过fseek设置).

注意上面这些mode都是以文本方式打开文件,如果想要想要以二进制方式打开文件需要在mode里加一个b,例如"rb+"或者"r+b"

文本方式打开文件和二进制方式打开文件的区别

首先了解一个知识点,windows下操作系统用两个字符来表示换行符,即CRLF(\r\n),而Linux用单一的LF(\n)表示换行符,不熟悉的可以通过这个博客了解一下:

而用文本方式打开文件和用二进制方式打开文件的区别就在于对换行符的解释上。二进制方式下,读文件时,会原封不动的读出文件的全部內容,写的時候,也是把內存缓冲区的內容原封不动的写到文件中。但文本方式下就有所不同了,在读文件的时候,会将换行符CRLF转换成单一的LF,而在写文件的时候,会将LF转换成CRLF再写入文件。这就说明我们最好在读写文件的时候采用相同的打开方式,以防数据解释出错。而这也其实只是为了兼容windows而制定的一些繁文缛节,像在Linux下换行符只用一个LF表示,此时不管使用文本方式打开文件还是二进制方式打开文件就没什么区别了。

文件关闭

函数原型:

int fclose(FILE* stream);

解除stream所表示的流和对应的文件的联系。

和这个流相关的读写缓冲区被刷新,写缓冲区内没有写入的数据立刻写入,读缓冲区里没有读取的数据直接丢弃。

5. 文件的顺序读写

顺序读写的意思是只能从文件打开时候默认的位置开始读写,例如a和a+默认的位置是文件末尾,其他的mode是文件开头。

函数名 函数作用 函数返回值
char fgets(char str, int num, FILE* stream); 从流中读取num - 1个字符并把这些字符作为C字符串存储在str字符数组里。如果还没读取够num - 1个字符就遇到了换行符或者EOF就停止读取。最终在str的最后加上一个换行符。 如果读取成功,则返回str。如果在读取到任何字符之前就读取到了EOF,则返回NULL。
int fputs(const char str, FILE stream); 把str所指向的字符数组中的数据写入流中,直到遇到'\0''\0'不被写入。 如果写入成功,则返回一个非负数。如果写入失败,返回EOF,并设置ferror。
int fscanf(FILE stream, const char format, ...); 从流中读取格式化的数据,并将内容保存到format后面的参数所指向的地址中,类似于scanf。 返回成功读取到的参数个数。
int fprintf(FILE stream, const char format, ...); 把格式化的数据写入流中,类似于printf。 返回成功写入的字符个数。
size_t fread(void ptr, size_t size, size_t count, FILEstream); 从流中读取count个大小为size字节的数据(或称为元素)并存储到ptr所指向的内存空间中。 返回成功读取了多少个元素,如果返回值和count不相等,则有可能是读取出错或者读到了EOF,此时perror被设置
size_t fwrite(const void ptr, size_t size, size_t count, FILEstream); 将ptr指向的,count个大小为size字节的元素输入到流中。这在内部是以字节为单位进行的,可以看作把从ptr开始,size * count个unsigned char的二进制被输入到了流中。 返回成功写入了多少个元素,如果返回值与count不相等,则一定是文件写入出错。

首先如果在只读模式下写文件或者只写模式下读文件都会发生错误,perror设置。

注意,根据函数作用就可以看出来,fgets、fputs、fscanf、fprintf处理的是文本文件,fread、fwrite处理的是二进制文件。例如fprintf往文件写入100其实写入的是字符串"100",而fwrite可以写入数据本身,下面是个实例:

此时文件中前三个字节是经ASCII编码后的字符串”100“,后四个字节是100在内存中的存储形式。

还有要注意的一点,上面的每一个函数在读写文件的时候,都会改变流位置标志(相当于光标)的位置,所以对于需要连续使用这些函数的场景,记得要考虑好某一时刻光标的位置。

下面还有两个比较特殊的函数:

函数名称 函数作用 函数返回值
int sscanf(const char s, const char format, ...); 从字符串s中读取格式化的数据并存储在后面的附加参数中,就像在用scanf一样,只是这个函数从字符串中读取数据而不是从标准输入stdin中读取。 如果读取成功,返回成功匹配到了附加参数中的多少个参数。如果在成功匹配到任何一个附加参数前就发生了错误,返回EOF。
int sprintf(char str, const char format, ...); 向字符指针str中写入格式化字符串,就像在用printf一样,只是将格式化字符串写入字符指针而不是输出到屏幕。 如果写入成功,返回写入了多少字节。如果写入失败,返回一个负值。

这两个函数就特殊在可以用于数值类型和字符串类型的相互转换。比如用sscanf可以把字符串转换成数值类型,用sprintf可以把数值类型转换成字符串。

6. 文件的随机读写

这里的随机读写就跟RAM一样,表示的是可以任选位置读写。

fseek

int fseek(FILE* stream, long int offset, int origin);

这个函数用来重定位流位置标志/文件读写指针/文件读写偏移量(相当于光标)。

对于用二进制文件方式打开的文件来说,这个函数把光标移动到以origin为基准,向后偏移offset个比特的位置。

对于用文本文件方式打开的文件来说,这里的offset必须是0或者一个用ftell()函数返回的数值,而origin必须是SEEK_SET。如果这两个参数不是要求的值,运行结果取决于具体的实现。

参数origin必须是下面的三个宏中的一个:

Constant Reference postion
SEEK_SET Beginning of file
SEEK_CUR Current position of the file pointer
SEEK_END End of file

注意:可以用fseek把流位置标志移动到文件起始位置之前,但这其实就相当于移动到起始位置。也可以把流位置标志移动到文件末尾之后之后,在这之后继续调用写函数会把中间空的位置填成0,然后往后写。(经测试只在r+、w、w+模式下出现)(存疑)。

ftell

long int ftell(FILE* stream);

返回现在流位置标志(相当于光标)所在的位置。

对于二进制方式打开的文件,这个值是光标相对文件开头的偏移,单位为字节。

对于文本方式打开的文件,这个值可以用来做fseek函数的参数。

rewind

void rewind(FILE* stream);

将流位置标志置到文件开头。

7. 总结

用了半天时间总算是写完了这篇博客。今天查了无数次文档上了很多遍百度做了很多测试,但是仍然感觉有很多东西还是没有写到,例如文件读写错误相关的以及一些其他的细节问题比如什么是流。但是我觉得再写下去这篇博客就要变成文档了,所以为了防止这种情况出现,这篇博客的脚步暂且就停在这里,日后如果需要使用没有涉及到的知识的话,再查文档也不迟。

Last modification:November 10th, 2019 at 10:07 am