文件是数据的集合,这个数据集的名称就是文件名。实际上在前面的各章中我们已经多次使用了文件,例如源程序文件、目标文件、可执行文件、库文件 (头文件)等。文件通常是存放在外部介质(如磁盘等)上的,在使用时才调入内存中来。从用户的角度看,Linux系统的文件分为普通文件和设备文件两种。
普通文件是指存放在磁盘或其它外部介质上的一个有序数据集,可以是源文件、目标文件、可执行程序; 也可以是一组待处理的原始数据,或者是一组输出的结果。对于源文件、目标文件、 可执行程序可以称作程序文件,对输入输出数据可称作数据文件。
设备文件是指与主机相联的各种外部设备,如显示器、打印机、键盘等。在Linux操作系统中,把外部设备也看作是一个文件来进行管理,把它们的输入、输出等同于对磁盘文件的读和写。通常把显示器定义为标准输出文件,一般情况下在屏幕上显示有关信息就是向标准输出文件输出。如前面经常使用的printf函数就是这类输出。键盘通常被指定标准的输入文件,从键盘上输入就意味着从标准输入文件上输入数据。
根据文件中数据的组织形式的不同,可以把文件分为文本文件和二进制文件。
文本文件和二进制文件
1、文本文件
数据以字符组成,把每个字符的 ASCII 码值存入文件中。每个 ASCII 码值占一个字节,每个字节表示一个字符。所以文本文件也称作字符文件或 ASCII 文件。
2、二进制文件
把数据对应的二进制数值存储到文件中,是字节序列文件。
例如数据 123,如果按文本文件形式存储,把数据看成三个字符:'1'、'2'、'3' 的集合,文件中依次存储每个字符的 ASCII 码值,格式如下表所示。
字符 | '1' | '2' | '3' |
ASCII(十进制) | 49 | 50 | 51 |
ASCII(二进制) | 00110001 | 00110010 | 00110011 |
如果按照二进制文件形式存储,数据 123 被看成字符、短整型、短整型、长整型,存储方式分别如下:
字符型一个字节
01111011
短整型2个字节
00000000 01111011
整型4个字节
00000000 00000000 00000000 01111011
长整型8个字节
00000000 00000000 00000000 00000000 00000000 00000000 00000000 01111011
文本文件可以用vi和记事本打开,看到的都是ASCII字符,二进制文件用vi可以打开,但是看到的是乱码,没有意义。打开二进制文件之前,必须知道它的格式,一般来说,不同的二进制文件采用相应的软件打开它,例如图片文件用图片查看软件,音频文件用音乐播放器。
文本文件可以换行,例如我们写的C程序,就是文本文件,有换行。
二进制文件没有换行的说法,也没有字符串的说法,也没有以空字符结尾的说法,它是数据流。
打开文件和关闭
C 语言中对任何文件进行操作,都必须先“打开”文件,操作完成后,再“关闭”文件。
1、文件指针
打开文件的时候,C语言为打开的文件分配一个文件信息区,该信息区中包含文件描述信息、该文件所使用的缓冲区大小及缓冲区位置、该文件当前读写到的位置等基本信息。这些信息保存在一个结构体类型变量中(struct _IO_FILE),这个结构体有一个别名FILE(typedef struct _IO_FILE FILE),FILE结构体和对文件操作的库函数在 stdio.h 头文件中声明的。
打开文件的时候,调用打开文件的函数fopen时会动态分配一个FILE结构体,并把FILE结构体地址作为函数的返回值,即文件指针。调用关闭文件的函数fclose时候,除了关闭文件,还会释放文件指针占用的内存空间。
2、打开文件
我们可以使用 C语言提供的库函数fopen来创建一个新的文件或者打开一个已存的文件,调用fopen函数成功后,返回一个文件指针( FILE)。
下面是这个函数调用的声明。
FILE *fopen( const char * filename, const char * mode );
参数filename 是字符串,表示需要打开的文件名,可以包含目录名,如果不包含路径就表示程序运行的目录。实际开发中,采用文件的全路径,即包含目录名。
参数mode也是字符串,表示打开文件的模式,打开模式可以是下列值中的一个。
模式 | 含 义 | 说 明 |
rt | 只读 | 文件必须存在,否则打开失败。 |
wt | 只写 | 如果文件存在,则清除原文件内容;如果文件不存在,则新建文件。 |
at | 追加只写 | 如果文件存在,则打开文件,如果文件不存在,则新建文件。 |
rt+ | 读写 | 文件必须存在。在只读 r 的基础上加 '+' 表示增加可写的功能。 |
wt+ | 读写 | 在只写w的模式上增加可读的功能。 |
at+ | 读写 | 在追加只写a的模式上增加可读的功能。 |
如果处理的是二进制文件,则需使用下面的打开模式来取代上面的打开模式。
"rb"、"wb"、"ab"、"rb+"、"wb+"、"ab+"
如果打开的是文本文件,打开模式的字母t可以省略,rt可以写成r,打开二进制文件时,打开模式的字母b不能省略。
英文单词:read简写r、text简写t、write简写w、append简写a、binary简写b。
对于文件打开模式mode,如果理解不了,不要去死记硬背,以后用到的时候再研究。
3、关闭文件
关闭文件的函数 fclose 的声明。
int fclose(FILE *fp);
4、示例(book108.c)
对初学者来说,以下代码可能难以理解。
if ( (fp=fopen("book1.c","r")) == 0 )
其实(fp=fopen("book1.c","r"))表达式的值就是fp,我在讲if分支语句的时候就讨论过了。我们可以用一段代码来测试一下。
运行结果
如果还不理解,就这么抄吧,抄多了就熟了。
5、注意事项
1)调用fopen打开文件的时候,一定要判断返回值,如果文件不存在、或没有权限、或磁盘空间满了,都有可能造成打开文件失败。
2)文件指针是调用fopen的时候,系统动态分配的内存,如果文件操作完了、或函数返回或程序退出的时候,必须用fclose关闭文件指针,释放内存,否则后果严重。
3)如果文件指针是空的,用fclose关闭它相当于操作空指针,后果严重。
文本文件的读写
在实际开发中,文本文件以行为单位存放字符串,如C程序的源代码,一段文字等,所以一般是按行写入或读取数据。
1、向文件中写入数据
C语言向文件中写入数据库函数有fputc、fputs、fprintf,在实际开发中,fputc和fputs没什么用,只介绍fprintf就可以了。fprintf函数的声明如下:
int fprintf(FILE *fp, const char *format, ...);
fprintf函数的用法与printf相同,只是多了第一个参数文件指针,表示把数据输出到文件。
一般情况下,程序员不必关心fprintf函数的返回值。
示例(book111.c)
编译book111.c程序并执行,采用cat命令查看/tmp/test1.txt的内容,如下:
可以看到/tmp/test1.txt中有5行记录,不管执行多少次都是5行记录,因为文件打开的方式是w,每次打开文件的时候都会清空原文件中的记录。
各位可以试一下把文件打开模式设置为a,看看程序执行的效果。
2、从文件中读取数据
C语言从文件中读取数据的库函数有fgetc、fgets、fscanf,在实际开发中,fgetc和fscanf没什么用,只介绍fgets就可以了。fgets函数的声明如下:
char *fgets(char *buf, int size, FILE *fp);
fgets的功能是从文件中读取一行。
参数buf是一个字符串,用于保存从文件中读到的数据。
参数size是打算读取内容的长度。
参数fp是待读取文件的文件指针。
如果文件中将要读取的这一行的内容的长度小于size,fgets函数就读取一行,如果这一行的内容大于等于size,fgets函数就读取size-1字节的内容。
调用fgets函数如果成功的读取到内容,函数返回buf,如果读取错误或文件已结束,返回空,即0。如果fgets返回空,可以认为是文件结束而不是发生了错误,因为发生错误的情况极少出现。
示例(book113.c)
运行结果
需要重点说明的是,在读取到 size-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 size 的值多大,fgets() 最多只能读取一行数据,不能跨行。在实际开发中,可以将 size 的值设置地足够大,可以是10240,每次就可以读取到一行完整的数据。
二进制文件的读写
二进制文件没有行的概念,存放的数据也不是字符串,不存在以0结尾的情况。
我们直接把内存中的数据结构写入二进制文件,读取的时候,也是从文件中读取数据结构的大小一块数据,直接保存到数据结构中。
1、向文件中写入数据
fwrite() 库函数用来向文件中写入块数据,它的原型为:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数的说明:
ptr:为内存区块的指针,存放了要写入的数据的地址,它可以是数组、变量、结构体等。
size:固定填1。
nmemb:表示打算写入数据的字节数。
fp:表示文件指针。
函数的返回值是本次成功写入数据的字节数,一般情况下,程序员不必关心fwrite函数的返回值。
示例(book115.c)
编译并运行程序,得到数据文件,用vi命令打开文件,显示如下:
可以看到很多乱码,其实并不是文件的内容乱,而是vi无法识别文件的格式,把内容当成ASCII码显示,文件中的字符串是ASCII码,所以能正确显示,但年龄和身高是整数,就无法显示了。
2、从文件中读取数据
fread() 库函数用来从文件中读取块数据,它的原型为:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp);
ptr:用于存放从文件中读取数据的变量地址,它可以是数组、变量、结构体等。
size:固定填1。
nmemb:表示打算读取的数据的字节数。
fp:表示文件指针。
调用fread函数如果成功的读取到内容,函数返回读取到的内容的字节数,如果读取错误或文件已结束,返回空,即0。如果fread返回空,可以认为是文件结束而不是发生了错误,因为发生错误的情况极少出现。
示例(book117.c)
运行结果
3、注意事项
1)我对fread和fwrite函数的size和nmemb以及它们的返回值的解释是不正确的,我这么做的原因是为了方便大家的使用,正确的解释会把大家搞晕,等你功力够的时候,我们再讨论它的准确含义。
2)fwrite和fread函数也可以写入和读取文本文件,但是没有换行的概念,不管是0还是换行或其它的特殊字符,无区别对待。
3)二进制文件有自已的数据格式,写入数据时要按约定的格式写,读取的时候也要按约定的格式读取,book115.c写入的是超女数据结构数据,book117.c就要用超女数据结构来存放读取的数据,这道理就像图片查看软件无法打开音频文件,音频播放软件也无法打开图片文件。
4)如果程序员不知道二进制文件的格式,也可以用fread和fwrite函数读写文件,例如文件复制和文件传输程序,它不会去解析文件的数据,所以不必关心文件的格式。
示例(book119.c)
运行结果
文件定位
在文件内部有一个位置指针,用来指向当前读写的位置,也就是读写到第几个字节。在文件打开时,如果打开模式是r和w,位置指针指向文件的第一个字节,如果打开模式是a,位置指针指向文件的尾部。每当从文件里读n个字节或文件里写入n个字节之后位置指针也会向后移动n个字节。
文件位置指针与C语言中的指针不是一回事。位置指针仅仅是一个标志,表示文件读写到的位置,也就是读写到第几个字节,它不表示地址。文件每读写一次,位置指针就会移动一次,它不需要你在程序中定义和赋值,而是由系统自动设置,对程序员来说是隐藏的。
在实际开发中,偶尔需要移动位置指针,实现对指定位置数据的读写。我们把移动位置指针称为文件定位。
C语言提供了ftell、rewind和fseek三个库函数来实现文件定位功能。
1、ftell函数
ftell函数用来返回当前文件位置指针的值,这个值是当前位置相对于文件开始位置的字节数。它的声明如下:
long ftell(FILE *fp);
2、rewind函数
rewind函数用来将位置指针移动到文件开头,它的声明如下:
void rewind ( FILE *fp );
3、fseek函数
fseek() 用来将位置指针移动到任意位置,它的声明如下:
int fseek ( FILE *fp, long offset, int origin );
参数说明:
1)fp 为文件指针,也就是被移动的文件。
2)offset 为偏移量,也就是要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。offset 为正时,向后移动;offset 为负时,向前移动。
3)origin 为起始位置,也就是从何处开始计算偏移量。C语言规定的起始位置有三种,分别为:0-文件开头;1-当前位置;2-文件末尾。
fseek(fp,100,0); // 从文件的开始位置计算,向后移动100字节。
fseek(fp,100,1); // 从文件的当前位置计算,向后移动100字节。
fseek(fp,-100,2); // 从文件的尾部位置计算,向前移动100字节。
4、注意事项
当offset是向文件尾方向偏移的时候,无论偏移量是否超出文件尾,fseek都是返回0,当偏移量没有超出文件尾的时候,文件指针式指向正常的偏移地址的,当偏移量超出文件尾的时候,文件指针是指向文件尾的。并不会返回偏移出错-1值。
当offset是向文件头方向偏移的时候,如果offset没有超出文件头,是正常偏移,文件指针指向正确的偏移地址,fseek返回值为0,当offset超出文件头时,fseek返回出错-1值,文件指针还是处于原来的地址。
文件缓冲区
在操作系统中,存在一个内存缓冲区,当调用fprintf、fwrite等函数往文件写入数据的时候,数据并不会立即写入磁盘文件,而是先写入缓冲区,等缓冲区的数据满了之后才写入文件。还有一种情况就是程序调用了fclose时也会把缓冲区的数据写入文件。
在实际开发中,如果程序员想把缓冲区的数据立即写入文件,可以调用fflush库函数,它的声明如下:
int fflush(FILE *fp);
函数的参数只有一个,即文件指针,返回0成功,其它失败,程序员一般不关心它的返回值。
标准输入、标准输出和标准错误
Linux操作系统为每个程序默认打开三个文件,即标准输入stdin、标准输出stdout和标准错误输出stderr,其中0就是stdin,表示输入流,指从键盘输入,1代表stdout,2代表stderr,1,2默认是显示器。
printf("Hello world.\n");
等同于
fprintf(stdout,"Hello world.\n");
这几个文件指针没什么用,让大家了解一下就行。在实际开发中,我们一般会关闭这几个文件指针。
课后作业
1、编写示例程序,从界面上输入五名超女的数据,存放在struct st_girl结构体数组中,然后把结构体数组以二进制的方式写入文件。
2、编写示例程序,把上一题写入的数据从二进制文件中读取出来,存入struct st_girl结构体中,然后在界面上显示出来。
3、编写示例程序,从界面上输入五名超女的数据,存放在struct st_girl结构体数组中,然后把结构体数组以xml字符串的方式写入文本文件。文件内容的格式如下:
<name>美女1</name><age>20</age><height>166</height><sc>一般</sc><yz>漂亮</yz>
<name>美女2</name><age>18</age><height>160</height><sc>火辣</sc><yz>一般</yz>
<name>美女3</name><age>22</age><height>177</height><sc>一般</sc><yz>漂亮</yz>
<name>美女10</name><age>26</age><height>159</height><sc>火辣</sc><yz>不行</yz>
4、编写示例程序,把上一题写入的数据从文本文件中读取出来,并解析xml,存入struct st_girl结构体中,然后在界面上显示出来。
5、文本文件和二进制文件存放数据的方式有区别,但是,在Linux平台下,打开文件的模式,文本文件模式和二进制文件模式没有区别。(在windows平台下,如果以“文本”方式打开文件,当读取文件的时候,系统会将所有的"/r/n"转换成"/n";当写入文件的时候,系统会将"/n"转换成"/r/n"写入。 如果以"二进制"方式打开文件,则读/写都不会进行这样的转换。)
用fgets和fprintf可以以行的方式读写文本文件,但不能读写二进制文件。
用fread和fwrite可以读写文本文件和二进制文件,但是没有行的说法。