原文链接:http://www.orlion.ga/936/
1、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 *这样的指针称为不透明指针(Opaque Pointer)或者叫句柄(Handle)。
mode参数:
"r":只读,文件必须已存在
"w":只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节再重新写,也就是替换掉原来的文件内容
"a":只能在文件末尾追加数据,如果文件不存在则创建
"r+":允许读和写,文件必须已存在
"w+":允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写
"a+":允许读和追加数据,如果文件不存在则创建
fclose函数:
#include <stdio.h> int fclose(FILE *fp); 返回值:成功返回0,出错返回EOF并设置errno
EOF在stdio.h中定义:
/* End of file character. Some things throughout the library rely on this being -1. */ #ifndef EOF # define EOF (-1) #endif
EOF的值是-1。
2、stdin/stdout/stderr
用printf和scanf这些都属于IO操作,但不是对文件的操作而是对终端设备做IO操作。所谓终端是指人机交互的设备,也就是可以接收用户输入并输出信息给用户的设备。终端设备和文件一样也需要先打开后操作,终端设备也有对应的路径名,/dev/tty就表示和当前进程相关联的终端设备,/dev/tty不是一个普通的文件,它不表示磁盘上的一组数据,而是表示一个设备,用ls查看这个文件:
开头的c表示文件类型是字符设备,中间的5, 0是它的设备号,主设备号5,次设备号0,主设备号标示内核中的一个设备驱动程序,次设备号标示该设备驱动程序管理的一个设备。内核通过设备号找到相应的驱动程序,完成对设备的操作。设备文件没有文件尺寸这个属性,因为设备文件在磁盘上不保存数据,对设备文件做读写操作并不是读写磁盘上的数据,而是在读写设备。
程序启动时(在main函数还没开始执行之前)会自动把终端设备打开三次,分别赋给三个FILE *指针stdin、stdout和stderr,这三个文件指针是libc中定义的全局变量,stdio.h中声明,printf向stdout写,而scanf从stdin读。用户程序也可以使用这三个文件指针。这三个文件指针的打开方式都是可读可写的,但通常stdin只用于读操作,称为标准输入,stdout只用于写操作,称为标准输出,stderr也只用于写操作,称为标准错误输出,通常程序的运行结果打印到标准输出,而错误提示(如gcc报的警告和错误)打印到标准错误输出,所以fopen的错误处理写成这样更符合惯例:
if ( (fp = fopen("/tmp/file1", "r")) == NULL) { fputs("Error open file /tmp/file1 ", stderr); exit(1); }
3、errno与perror函数
很多系统函数在错误返回时将错误原因记录在libc定义的全局变量errno中。各种错误对应一个错误码,errno在errno.h中声明,是一个整型变量,所有错误码都是正整数。
直接打印errno只会打印出一个整数值看不出什么错误,可以用perror或strerror函数将errno解释成字符串再打印。
#include <stdio.h> void perror(const char *s);
perror函数将错误信息打印到标准错误输出,首先打印参数s所指的字符串然后打印":"号,然后根据当前errno的值打印错误原因。
strerror函数可以根据错误号返回错误原因字符串。
#include <string.h> char *strerror(int errnum); 返回值:错误码errnum所对应的字符串
有些函数的错误码并不保存在errno中而是通过返回值返回,这时strerror就可以用到了:
fputs(strerror(n), stderr);
4、以字节为单位的IO函数
fget函数从指定的文件中读一个字节,getchar从标准输入读一个字节,调用getchar()相当于调用fgetc(stdin)。
#include <stdio.h> int fgetc(FILE *stream); int getchar(void); 返回值:成功返回读到的字节,出错或者读到文件末尾时返回EOF
标准IO库操作的文件有时叫做流(Stream)。
fputc函数向指定的文件写一个字节,putchar向标准输出写一个字节,调用putchar(c)相当于调用fput(c, stdout)。
#include <stdio.h> int fputc(int c, FILE *stream); int putchar(int c); 返回值:成功返回写入的字节,出错返回EOF
从终端设备读有些特殊,当调用getchar()或fgetc(stdin)时,如果用户没有输入字符,getchar函数就阻塞等待,所谓阻塞是指这个函数调用不返回,也就不能执行后边的代码,这个进程阻塞了,操作系统可以调度别的进程执行。从终端设备读还有一个特点,用户输入一般字符不会是getchar函数返回,仍然阻塞着,只有当用户输入回车或者到达文件末尾时getchar才返回。
从终端设备输入时有两种方法表示文件结束,一种方法是在一行的开头输入Ctrl-D(如果不在一行的开头则需要连续输入两次Ctrl-D),另一种方法是利用Shell的Heredoc语法:
$ ./a.out <<END > hello > hey > END hello hey
<<END表示从下一行开始是标准输入,直到某一行开头出现END时结束。<<后面的结束符可以任意指定,不一定得是END。
5、操作读写位置的函数
fseek函数可以移动读写位置,ftell可以返回当前的读写位置。
#include <stdio.h> int feek(FILE *stream, long offset, int whence); 返回值:成功返回0,出错返回-1并设置errno long ftell(FILE *stream); 返回值:成功返回当前读写位置,出错返回-1并设置errno void rewind(FILE *stream);
fseek的whence参数的含义:
SEEK_SET:从文件开头移动offset个字节
SEEK_CUR:从当前位置移动offset个字节
SEEK_END:从文件末尾移动offset个字节
offset可正可负。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸,从原来的文件末尾到fseek移动之后的读写位置之间的字节都是0。
6、以字符串为单位的IO函数
fgets从指定的文件中读一行字符到调用者提供的缓冲区中,gets从标准输入读一行字符到调用者提供的缓冲区中。
#include <stdio.h> char *fgets(char *s, int size, FILE *stream); char *gets(char *s); 返回值:成功时s指向哪返回的指针就指向哪,出错或者读到文件末尾时返回NULL
fgets函数,参数s是缓冲区的首地址,size是缓冲区的长度,该函数从stream所指的文件中读取以' '结尾的一行(包括' '在内)存到缓冲区s中,并且在该行末尾添加一个' '组成完整的字符串。如果文件中的一行太长,fgets从文件中读了size-1个字符还没有读到' ',就把已经读到的size-1个字符和一个' '字符存入缓冲区,文件中剩下的半行可以在下次调用fgets时继续读。
如果一次fgets调用在读入若干个字符后到达文件末尾,则将已读到的字符串加上' '存入缓冲区并返回,如果再次调用fgets则返回NULL,可以据此判断是否读到文件末尾。如果文件中存在' '字符(或者说0x00字节),调用fgets之后就无法判断缓冲区中的' '究竟是从文件读上来的字符还是由fgets自动添加的结束符,所以fgets只适合读文本文件而不适合读二进制文件,并且文本文件中的所有字符都应该是可见字符,不能有' '。
fputs向指定的文件写入一个字符串,puts向标准输出写入一个字符串。
#include <stdio.h> int fputs(const char *s, FILE *stream); int puts(const char *s); 返回值:成功返回一个非负整数,出错返回EOF
缓冲区s中保存的是以' '结尾的字符串,fputs将该字符串写入文件stream,但并不写入结尾的' '。与fgets不同的是,fputs并不关心的字符串中的' '字符,字符串中可以有' '也可以没有' '。puts将字符串s写到标准输出(不包括结尾的' '),然后自动写一个' '到标准输出。
7、以记录为单位的IO函数
#include <stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); 返回值:读或写的记录数,成功时返回的记录数等于nmemb,出错或读到文件末尾时返回 的记录数小于nmemb,也可能返回0
fread和fwrite用于读写记录,这里的记录是指一串固定长度的字节,比如一个int、一个结构体或者一个定长数组。参数size指出一条记录的长度,而nmemb指出要读或写多少条记录,这些记录在ptr所指的内存空间中连续存放,共占size * nmemb个字节,fread从文件stream中读出size * nmemb个字节保存到ptr中,而fwrite把ptr中的size * nmemb个字节写到文件stream中。
nmemb是请求读或写的记录数,fread和fwrite返回的记录数有可能小于nmemb指定的记录数。例如当前读写位置距文件末尾只有一条记录的长度,调用fread时指定nmemb为2,则返回值为1。如果当前读写位置已经在文件末尾了,或者读文件时出错了,则fread返回0。如果写文件时出错了,则fwrite的返回值小于nmemb指定的值。
8、格式化IO函数
#include <stdio.h> int printf(const char *format, ...); int fprintf(FILE *stream, const char *format, ...); int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...); #include <stdarg.h> int vprintf(const char *format, va_list ap); int vfprintf(FILE *stream, const char *format, va_list ap); int vsprintf(char *str, const char *format, va_list ap); int vsnprintf(char *str, size_t size, const char *format, va_list ap); 返回值:成功返回格式化输出的字节数(不包括字符串的结尾' '),出错返回一个负值
printf格式化打印到标准输出,而fprintf打印到指定的文件stream中。sprintf打印到用户提供的缓冲区str中并在末尾加' ',由于格式化后的字符串长度很难预计,所以很可能造成缓冲区溢出,用snprintf更好一些,参数size指定了缓冲区长度,如果格式化后的字符串长度超过缓冲区长度,snprintf就把字符串截断到size-1字节,再加上一个' '写入缓冲区,也就是说snprintf保证字符串以' '结尾。snprintf的返回值是格式化后的字符串长度(不包括结尾的' ')
后四个函数在前四个函数名的前面多了个v,表示可变参数不是以…的形式传进来,而是以va_list类型传进来。
#include <stdio.h> int scanf(const char *format, ...); int fscanf(FILE *stream, const char *format, ...); int sscanf(const char *str, const char *format, ...); #include <stdarg.h> int vscanf(const char *format, va_list ap); int vsscanf(const char *str, const char *format, va_list ap); int vfscanf(FILE *stream, const char *format, va_list ap); 返回值:返回成功匹配和赋值的参数个数,成功匹配的参数可能少于所提供的赋值参数, 返回0表示一个都不匹配,出错或者读到文件或字符串末尾时返回EOF并设置errno
scanf从标准输入读字符,按格式化字符串format中的转换说明解释这些字符,转换后赋给后面的参数,后面的参数都是传出参数,因此必须传地址而不能传值。fscanf从指定的文件stream中读文件,而sscanf从指定的字符串str中读字符。
9、C标准库的IO缓冲区
用户程序调用C标准IO库函数读写文件或设备,而这些库函数最终要通过系统调用把读写请求传给内核,最终内核驱动磁盘或设备完成IO操作。C标准库为每个打开的文件分配一个IO缓冲区以加速读写操作,通过文件的FILE结构体可以找到这个缓冲区,用户调用读写函数大多时候都在IO缓冲区中读写,只有少数时候需要把读写请求传给内核。以fgetc/fputc为例,当用户程序第一次调用fgetc读一个文件时,fgetc函数可能通过系统调用进入内核读1K字节进入到IO缓冲区,然后返回一个IO缓冲区中的第一个字节给用户,把读写位置指向缓冲区的第二个字符下一次就从缓冲区中取,当把缓冲区中1K字节读完之后再进入内核读1K字节进缓冲区。
用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统
调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作。