我们已经多次用到了文件,例如源文件、目标文件、可执行文件、库文件等,现在学习如何用C标准库对文件进行读写操作,对文件的读写也属于I/O操作的一种,本节介绍的大部分函数在头文件stdio.h
中声明,称为标准I/O库函数。
fopen/fclose
在操作文件之前要用fopen
打开文件,操作完毕要用fclose
关闭文件。打开文件就是在操作系统中分配一些资源用于保存该文件的状态信息,并得到该文件的标识,以后用户程序就可以用这个标识对文件做各种操作,关闭文件则释放文件在操作系统中占用的资源,使文件的标识失效,用户程序就无法再操作这个文件了。
#include <stdio.h> FILE *fopen(const char *path, const char *mode); 返回值:成功返回文件指针,出错返回NULL并设置errno
path
是文件的路径名,mode
表示打开方式。如果文件打开成功,就返回一个FILE *
文件指针来标识这个文件。以后调用其它函数对文件做读写操作都要提供这个指针,以指明对哪个文件进行操作。FILE
是C标准库中定义的结构体类型,其中包含该文件在内核中标识I/O缓冲区和当前读写位置等信息,但调用者不必知道FILE
结构体都有哪些成员,我们很快就会看到,调用者只是把文件指针在库函数接口之间传来传去,而文件指针所指的FILE
结构体的成员在库函数内部维护,调用者不应该直接访问这些成员,这种编程思想在面向对象方法论中称为封装(Encapsulation)。像FILE *
这样的指针称为不透明指针(Opaque Pointer)或者叫句柄(Handle),FILE *
指针就像一个把手(Handle),抓住这个把手就可以打开门或抽屉,但用户只能抓这个把手,而不能直接抓门或抽屉。
下面说说参数path
和mode
,path
可以是相对路径也可以是绝对路径,mode
表示打开方式是读还是写。比如fp = fopen("/tmp/file2", "w");
表示打开绝对路径/tmp/file2
,只做写操作,path
也可以是相对路径,比如fp = fopen("file.a", "r");
表示在当前工作目录下打开文件file.a
,只做读操作,再比如fp = fopen("../a.out", "r");
只读打开当前工作目录上一层目录下的a.out
,fp = fopen("Desktop/file3", "w");
只写打开当前工作目录下子目录Desktop
下的file3
。相对路径是相对于当前工作目录(Current Working Directory)的路径,每个进程都有自己的当前工作目录,Shell进程的当前工作目录可以用pwd
命令查看:
$ pwd /home/akaedu
通常Linux发行版都把Shell配置成在提示符前面显示当前工作目录,例如~$
表示当前工作目录是主目录,/etc$
表示当前工作目录是/etc
。用cd
命令可以改变Shell进程的当前工作目录。在Shell下敲命令启动新的进程,则该进程的当前工作目录继承自Shell进程的当前工作目录,该进程也可以调用chdir(2)
函数改变自己的当前工作目录。
mode
参数是一个字符串,由rwatb+
六个字符组合而成,r
表示读,w
表示写,a
表示追加(Append),在文件末尾追加数据使文件的尺寸增大。t
表示文本文件,b
表示二进制文件,有些操作系统的文本文件和二进制文件格式不同,而在UNIX系统中,无论文本文件还是二进制文件都是由一串字节组成,t
和b
没有区分,用哪个都一样,也可以省略不写。如果省略t
和b
,rwa+
四个字符有以下6种合法的组合:
-
只读,文件必须已存在
"w"
-
只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节再重新写,也就是替换掉原来的文件内容
"a"
-
只能在文件末尾追加数据,如果文件不存在则创建
"r+"
-
允许读和写,文件必须已存在
"w+"
-
允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写
"a+"
-
允许读和追加数据,如果文件不存在则创建
"r"
在打开一个文件时如果出错,fopen
将返回NULL
并设置errno
,errno
稍后介绍。在程序中应该做出错处理,通常这样写:
if ( (fp = fopen("/tmp/file1", "r")) == NULL)
{ printf("error open file /tmp/file1!\n"); exit(1); }
比如/tmp/file1
这个文件不存在,而r
打开方式又不会创建这个文件,fopen
就会出错返回。
再说说fclose
函数。
#include <stdio.h>
int fclose(FILE *fp); 返回值:成功返回0,出错返回EOF并设置errno
把文件指针传给fclose
可以关闭它所标识的文件,关闭之后该文件指针就无效了,不能再使用了。如果fclose
调用出错(比如传给它一个无效的文件指针)则返回EOF
并设置errno
,errno
稍后介绍,EOF
在stdio.h
中定义:
/* End of file character. Some things throughout the library rely on this being -1. */ #ifndef EOF # define EOF (-1) #endif
它的值是-1。
fopen
调用应该和fclose
调用配对,打开文件操作完之后一定要记得关闭。如果不调用fclose
,在进程退出时系统会自动关闭文件,但是不能因此就忽略fclose
调用,如果写一个长年累月运行的程序(比如网络服务器程序),打开的文件都不关闭,堆积得越来越多,就会占用越来越多的系统资源。
stdin/stdout/stderr
我们经常用printf
打印到屏幕,也用过scanf
读键盘输入,这些也属于I/O操作,但不是对文件做I/O操作而是对终端设备做I/O操作。所谓终端(Terminal)是指人机交互的设备,也就是可以接受用户输入并输出信息给用户的设备。在计算机刚诞生的年代,终端是电传打字机和打印机,现在的终端通常是键盘和显示器。终端设备和文件一样也需要先打开后操作,终端设备也有对应的路径名,/dev/tty
就表示和当前进程相关联的终端设备。也就是说,/dev/tty
不是一个普通的文件,它不表示磁盘上的一组数据,而是表示一个设备。用ls
命令查看这个文件:
$ ls -l /dev/tty crw-rw-rw- 1 root dialout 5, 0 2009-03-20 19:31 /dev/tty
开头的c
表示文件类型是字符设备。中间的5, 0
是 它的设备号,主设备号5,次设备号0,主设备号标识内核中的一个设备驱动程序,次设备号标识该设备驱动程序管理的一个设备。内核通过设备号找到相应的驱动 程序,完成对该设备的操作。我们知道常规文件的这一列应该显示文件尺寸,而设备文件的这一列显示设备号,这表明设备文件是没有文件尺寸这个属性的,因为设 备文件在磁盘上不保存数据,对设备文件做读写操作并不是读写磁盘上的数据,而是在读写设备。UNIX的传统是Everything is a file,键盘、显示器、串口、磁盘等设备在/dev
目录下都有一个特殊的设备文件与之对应,这些设备文件也可以像普通文件一样打开、读、写和关闭,使用的函数接口是相同的。本书中不严格区分“文件”和“设备”这两个概念,遇到“文件”这个词,读者可以根据上下文理解它是指普通文件还是设备,如果需要强调是保存在磁盘上的普通文件,本书会用“常规文件”(Regular File)这个词。
那为什么printf
和scanf
不用打开就能对终端设备进行操作呢?因为在程序启动时(在main
函数还没开始执行之前)会自动把终端设备打开三次,分别赋给三个FILE *
指针stdin
、stdout
和stderr
,这三个文件指针是libc
中定义的全局变量,在stdio.h
中声明,printf
向stdout
写,而scanf
从stdin
读,后面我们会看到,用户程序也可以直接使用这三个文件指针。这三个文件指针的打开方式都是可读可写的,但通常stdin
只用于读操作,称为标准输入(Standard Input),stdout
只用于写操作,称为标准输出(Standard Output),stderr
也只用于写操作,称为标准错误输出(Standard Error),通常程序的运行结果打印到标准输出,而错误提示(例如gcc
报的警告和错误)打印到标准错误输出,所以fopen
的错误处理写成这样更符合惯例:
if ( (fp = fopen("/tmp/file1", "r")) == NULL)
{ fputs("Error open file /tmp/file1\n", stderr); exit(1); }
fputs
函数将在稍后详细介绍。不管是打印到标准输出还是打印到标准 错误输出效果是一样的,都是打印到终端设备(也就是屏幕)了,那为什么还要分成标准输出和标准错误输出呢?以后我们会讲到重定向操作,可以把标准输出重定 向到一个常规文件,而标准错误输出仍然对应终端设备,这样就可以把正常的运行结果和错误提示分开,而不是混在一起打印到屏幕了。
errno与perror函数
很多系统函数在错误返回时将错误原因记录在libc
定义的全局变量errno
中,每种错误原因对应一个错误码,请查阅errno(3)
的Man Page了解各种错误码,errno
在头文件errno.h
中声明,是一个整型变量,所有错误码都是正整数。
如果在程序中打印错误信息时直接打印errno
变量,打印出来的只是一个整数值,仍然看不出是什么错误。比较好的办法是用perror
或strerror
函数将errno
解释成字符串再打印。
#include <stdio.h> void perror(const char *s);
perror
函数将错误信息打印到标准错误输出,首先打印参数s
所指的字符串,然后打印:号,然后根据当前errno
的值打印错误原因。例如:
例 perror
#include <stdio.h>
#include <stdlib.h>
int main(void)
{ FILE *fp = fopen("abcde", "r");
if (fp == NULL)
{ perror("Open file abcde"); exit(1); }
return 0;
}
如果文件abcde
不存在,fopen
返回-1并设置errno
为ENOENT
,紧接着perror
函数读取errno
的值,将ENOENT
解释成字符串No such file or directory
并打印,最后打印的结果是Open file abcde: No such file or directory
。虽然perror
可以打印出错误原因,传给perror
的字符串参数仍然应该提供一些额外的信息,以便在看到错误信息时能够很快定位是程序中哪里出了错,如果在程序中有很多个fopen
调用,每个fopen
打开不同的文件,那么在每个fopen
的错误处理中打印文件名就很有帮助。
如果把上面的程序改成这样:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void)
{ FILE *fp = fopen("abcde", "r");
if (fp == NULL)
{ perror("Open file abcde");
printf("errno: %d\n", errno); exit(1); }