ANSI C标准几乎被所有的操作系统支持,ANSI C标准提供了完善的I/O函数,使用这些I/O操作我们可以控制程序的输入输出、读写系统磁盘文件。本文记录了用户进程I/O缓冲介绍、文件的读写、文件定位操作等内容。
库函数与系统调用
文件是位于磁盘上的,如何在运行的程序(进程)中控制文件的读写,通过下面的这张图,我们可以看到应用程序如何控制系统资源(包括磁盘中的文件)的大概的原理。
操作系统帮助我们管理硬件资源,封装底层实现,以接口的形式(系统调用函数)供上层应用程序调用。直接使用操作系统提供的系统调用函数我们就可以控制文件的读写,但是一般我们在应用程序中不提倡这样做,因为会带来性能上的问题。应用程序调用系统调用函数的时候,操作系统会从用户态转变成内核态,完成调用后,再从内核态转成用户态(Linux中是通过软中断实现的),而频繁的系统状态切换是需要开销的,会给用户应用程序带来性能上的问题。另外就是可移植性的问题,每个操作系统向外提供的接口是不尽相同的,如果直接在应用程序中使用系统调用函数,那么程序的可移植性将会很差。在ANSI
C标准中为我们提供了标准函数(库函数)去操作系统调用函数,库函数是一些能够完成特定功能的函数,一般由某个标准组织发布,并形成一种公认的标准,所以使用库函数可以屏蔽操作系统间对外接口的差异。
下面是网友总结的库函数和系统调用函数之间的差异
缓冲和非缓冲
对于文件的访问操作,可以按照是否使用缓冲区,分为缓冲文件操作和非缓冲文件操作
缓冲文件操作:高级文件操作,如第一幅图用户应用程序的左分支,在用户进程空间为打开的文件分配缓冲区。ANSI C标准就是使用的这种文件操作。本文讨论的正是这种。
非缓冲文件操作:低级文件操作,如第一幅图用户应用程序的右分支,在用户进程空间不设文件缓冲区。POSIX C标准的I/O就是使用的非缓冲文件操作。
说明:这里所说的缓冲全部指的是用户进程空间的文件缓冲,即使是非缓冲的文件操作,在内核部分也是使用了多级缓冲,而不是直接访问磁盘(可以参考操作系统的存储器结构)。
文件与文件流
流是一种抽象的数据结构,是ANSI C用来高效的管理打开了的文件信息的,在实际编程中的体现就是struct FILE结构体,在stdio.h头文件中定义,流对象最重要的机制就是缓冲区和格式转换。在Linux系统中系统默认为每个进程打开3个文件,对应3个流就是标准输入流、标准输出流、标准错误流。除此之外,需要用到的其他文件需要自己打开和关闭。
文件的I/O操作
按照读/写的对象、可以分为按字符、行、块、格式化等几种读写操作。
(1)按字符读/写文件流
下面的这段程序是使用fgetc和fputc来读取指定文件中的内容到标准输出(显示器),运行时需要指定一个文件
#include<stdio.h> int main(int argc,char *argv[]) { FILE *fp=NULL; char ch; if(argc<=1) { printf("check usage of %s \n",argv[0]); return -1; } if((fp=fopen(argv[1],"r"))==NULL) { printf("can not open %s\n",argv[1]); return -1; } while ((ch=fgetc(fp))!=EOF) fputc(ch,stdout); fclose(fp); return 0; }
运行程序:
(2)按行读/写文件流
下面是一个使用fgets和fputs实现上述功能的程序
#include<stdio.h> int main(int argc,char *argv[]) { FILE *fp=NULL; char str[20]; if((fp=fopen(argv[1],"r"))==NULL) //按只读的形式打开文件 { printf("can not open!\n"); return -1; } fgets(str,sizeof(str),fp); //从打开文件中读取sizeof(str)个字节到str中 fputs(str,stdout); //将str输出到标准输出 fclose(fp); //关闭文件 return 0; }
(3)按照块读写
下面是使用fread和fwrite来实现的按块读写的程序
#include<stdio.h> int main(int argc,char *argv[]) { struct student { char name[10]; int number; }; FILE *fp=NULL; int i; struct student boya[2],boyb[2],*pp,*qq; if((fp=fopen("aa.txt","w+"))==NULL) //以可读写的方式打开文件;若该文件存在则清空,若不存在就创建 { //打开文件失败 printf("can not open!\n"); return -1; } pp=boya; qq=boyb; printf(“please input two students‘ name and number:\n"); for (i=0;i<2;i++,pp++) scanf("%s\%d",pp->name,&pp->number); pp=boya; fwrite(pp,sizeof(struct student),2,fp); //将从键盘输入的信息写入到文件流fp中 rewind(fp); //将读写位置定位到文件头 fread(qq,sizeof(struct student),2,fp); //从文件流fp中读两个结构体到qq printf("name\t\t number\n"); for(i=0;i<2;i++,qq++) //输出qq中的内容 printf("%s\t\t %d\n",qq->name,qq->number); fclose(fp); return 0; }
(4)按照格式化读/写
下面这段程序是使用sprintf和sscanf进行文件读/写操作
#include<stdio.h> int main() { FILE *fp = NULL; int i = 20; char ch = 'D'; if((fp=fopen("f2","r+"))==NULL) { printf("open file f2 failed."); return -1; } fprintf(fp,"%d:%c\n",i,ch); //按照指定格式写文件 int new_i = 0; char new_ch; rewind(fp); //使读写指针归位 fscanf(fp,"%d:%c\n",&new_i,&new_ch); //按照指定格式读文件 printf("new_i=%d, new_ch=%c\n",new_i,new_ch); fclose(fp); return 0; }
程序运行结果:
再说缓冲区
可以发现上述4中形式的读写操作,都没有指明缓冲区,但是它们都使用到了位于用户进程空间的文件缓冲区,这是因为,对于任意的流,如果没有指明其缓冲区的类型,系统将指定默认类型的缓冲区。如果用户希望自己指定缓冲区,可以使用setbuf( )或者setvbuf( )函数指定,这两个函数的声明如下:
extern void setbuf ( 流对象, 缓冲区);
如果将缓冲区设置为NULL,则关闭缓冲区。
extern int setvbuf (流对象,缓冲区, 模式,缓冲区大小)
setvbuf比setbuf更加灵活,其中模式可取值有0、1、2,分别表示全缓冲、行缓冲、无缓冲,如果模式是2,那么将会忽视第二和第四个参数。
下面是一个修改缓冲区的程序:
/* Example show usage of setbuf() &setvbuf() */ #include<stdio.h> #include<error.h> #include<string.h> int main( int argc , char ** argv ) { int i; FILE * fp; char msg1[]="hello,wolrd\n"; char msg2[] = "hello\nworld"; char buf[128]; //open a file and set nobuf(used setbuf).and write string to it,check it before close of flush the stream if(( fp = fopen("no_buf1.txt","w")) == NULL) { perror("file open failure!"); return(-1); } setbuf(fp,NULL); memset(buf,'\0',128); fwrite( msg1 , 7 , 1 , fp ); printf("test setbuf(no buf)!check no_buf1.txt\n"); printf("now buf data is :buf=%s\n",buf); printf("press enter to continue!\n"); getchar(); fclose(fp); //open a file and set nobuf(used setvbuf).and write string to it,check it before close of flush the stream if(( fp = fopen("no_buf2.txt","w")) == NULL) { perror("file open failure!"); return(-1); } setvbuf( fp , NULL, _IONBF , 0 ); memset(buf,'\0',128); fwrite( msg1 , 7 , 1 , fp ); printf("test setvbuf(no buf)!check no_buf2.txt\n"); printf("now buf data is :buf=%s\n",buf); printf("press enter to continue!\n"); getchar(); fclose(fp); //open a file and set line buf(used setvbuf).and write string(include '\n') to it, // //check it before close of flush the stream if(( fp = fopen("l_buf.txt","w")) == NULL) { perror("file open failure!"); return(-1); } setvbuf( fp , buf , _IOLBF , sizeof(buf) ); memset(buf,'\0',128); fwrite( msg2 , sizeof(msg2) , 1 , fp ); printf("test setvbuf(line buf)!check l_buf.txt, because line buf ,only data before enter send to file\n"); printf("now buf data is :buf=%s\n",buf); printf("press enter to continue!\n"); getchar(); fclose(fp); //open a file and set full buf(used setvbuf).and write string to it for 20th time (it is large than the buf) //check it before close of flush the stream if(( fp = fopen("f_buf.txt","w")) == NULL){ perror("file open failure!"); return(-1); } setvbuf( fp , buf , _IOFBF , sizeof(buf) ); memset(buf,'\0',128); fwrite( msg2 , sizeof(msg2) , 1 , fp ); printf("test setbuf(full buf)!check f_buf.txt\n"); printf("now buf data is :buf=%s\n",buf); printf("press enter to continue!\n"); getchar(); fclose(fp); }
其他文件操作
下面介绍一些在文件操作中常用的函数
打开、关闭文件操作fopen和fclose
fp = fopen(文件名,文件操作方式);
文件的操作方式有以下这些
关闭文件使用fclose(文件指针); 或者 fcloseall()函数。
文件流检测
extern int feof(流对象)
用于判断流对象是否读到文件尾部,如果是返回1,否则,返回0;
extern int ferror(流对象)
用于判断流对象是否出现了错误,若没有错误,则返回0,否则,返回非0。
extern long int ftell(流对象)
返回当前读写位置距离文件开头位置的字节数,若执行失败,返回-1
extern int fseek(流对象,偏移距离,基准位置)
基准位置可取值有0、1、2分别表示文件开头、当前位置、文件结尾
将读写位置移到,距离基准位置偏移距离处。若成功,返回0;否则,返回,-1
extern void rewind(流对象)
将读写位置重置到文件开始处。