《Linux应用文件编程(二) — 标准IO》
1. 标准IO
与文件IO函数相类似,标准IO库中提供的是fopen、fclose、fread、fwrite等面向流对象的IO函数,这些函数在实现时本身就要调用linux的文件IO这些系统调用。
2. 标准IO的缓冲机制
由于IO设备的访问速度与CPU的速度相差好几个数量级,为了协调IO设备与CPU的速度的不匹配,对于块设备,内核使用了页高速缓存,即数据会先被拷贝到操作系统内核的页缓存区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间。
当应用程序尝试读取某块数据的时候,如果这块数据已经存放在页缓存中,那么这块数据就可以立即返回给应用程序,而不需要经过实际的物理读盘操作。当然,如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。对于写操作来说,应用程序也会将数据先写到页缓存中去,数据是否被立即写到磁盘上去取决于应用程序所采用的写操作机制:如果用户采用的是同步写机制,那么数据会立即被写回到磁 盘上,应用程序会一直等到数据被写完为止;如果用户采用的是延迟写机制,那么应用程序就完全不需要等到数据全部被 写回到磁盘,数据只要被写到页缓存中去 就可以了。在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。与异步写机制不同的是,延迟写机制在数据完全写到磁盘上得时候不会通 知应用程序,而异步写机制在数据完全写到磁盘上得时候是会返回给应用程序的。
标准IO提供三种类型的缓冲:
全缓冲:
一般用于访问真正的磁盘文件。 C 库会为文件访问申请一块内存,只有当文件内容将缓存填满或执行行冲刷函数flush时,C库才会将缓存内容写入内核中。
行缓冲:
当在输入和输出中遇到换行符时,标准IO库执行IO操作。通常涉及到终端(例如标准输入和标准输出)使用的是行缓冲。
对于行缓冲有两个限制:
第一,因为标准IO库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行IO操作。
第二,任何时候只要通过标准IO库要求从(a)一个不带缓存的流,或者(b)一个行缓存的流(它要求从内核得到数据)得到输入数据,那么就会造成冲洗所有行缓冲输出流(因为后面读取的数据可能就是前面输出的数据)。其实第二种情况我们会经常遇到,当我们先调用printf输出一串不带换行符的字符时,执行完这条printf语句并不会立刻在屏幕中显示我们输出的数据,当我们接下来调用scanf从标准输入读取数据时,我们才看到前面输出的数据。
不带缓冲:
标准IO库不对字符进行存储。例如,如果用标准IO函数fputs写15个字符到不带缓冲的流中,则该函数很可能直接调用write系统调用将这些字符立即写到相关的文件中。标准出错流stderr是不带缓冲的,这样为了让出错的信息可以尽快的显示出来。
3. 标准IO函数
3.1 fopen函数
#include <stdio.h> FILE *fopen(const char *path, const char *mode);//mode为操作权限 //打开一个指定的文件 FILE *fdopen(int fd, const char *mode); //取一个现存的文件描述符,并使一个标准I/O流与该描述符相结合 FILE *freopen(const char *path, const char *mode, FILE *stream); //用于重定向输入输出流的函数,将stream中的标准输入、输出、错误或者文件流重定向为filename文件中的内容。linux下需要重定向输出很容易使用 ./程序名 >test (>>test 追加),windows下的输入输出重定向可以使用freopen。
返回值: 成功返回一个FILE文件流指针,失败返回NULL,并且设置errno全局变量。 参数:
mode: r:以只读方式打开文件,该文件必须存在,返回的文件流指针位于文件开始 r+ :以读/写方式打开文件,该文件必须存在,返回的文件流指针位于文件开始 w:打开只写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件 w+:打开可读/写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件 a:以附加的方式打开只写文件。若文件不存在,则会创建该文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留。 a+:以附加方式打开可读/写的文件。若文件不存在,则会创建该文件,如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留。
stream: 文件指针,通常使用标准流文件(stdin/stdout/stderr)
fdopen常用于由创建管道及网络通信通道函数返回的描述符(我们可能从 open,dup,dup2,fcntl,pipe,socket,socketpair或accept函数得到此文件描述符)。因为这些特殊类型的文件不能用fopen打开,因此必须先调用设备专用函数以获得一个文件描述符,然后再用fdopen使一个标准I/O流与该描述符相关联,形态必须和原先文件描述词读写模式相同。
对于fdopen函数,mode的参数稍有区别:1.因为该描述符已被打开,所以fdopen为写而打开并不截短该文件。2.不能用于创建文件(因为一个描述符引用一个文件,则该文件一定存在)。
例程:
fopen和fdopen函数
FILE *fp; int fd; if ((fp = fopen ("hello.txt", "w+")) == NULL) printf("fopen file error\n"); return 0;} fprintf(fp , "hello word\n"); fclose(fp); if ((fd = open("hello.txt", O_RDWR )) == 1) { printf("open file fail\n"); return 0;} if ((fp = fdopen( fd , "a+")) == NULL) printf("fdopen open\n"); return 0; } fprintf(fp , "linux c program"); fclose(fp);
freopen函数
freopen("test.in", "r", stdin); 程序的输入就会从标准输入流stdin转换到从文件"test.in"中输入。 freopen("test.out", "w", stdout); 程序的输出就会从原来的标准输出变成写入文件"test.out"中。
3.2 fread函数
#include <stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 返回值: 成功时fread返回的值与nmemb相等;若小于nmemb但是大于0,则可能是到了文件末尾,不够次数;若返回0,则是文件读取错误,不满一个size的大小。
所以循环读取文件的时候,while(可以判断是否大于等于0)作为条件,里面再基于feof函数判断是否到了文件末尾而退出循环。 参数: Ptr:读取的数据存储的位置 Size:读取的每个单元的大小 Nmemb:读取的单元数量 Stream:从哪个文件流读取数据
fread不能区分到达文件尾和错误,因此必须使用feof和ferror函数确定是到达文件尾,还是发生错误。
例程:
int s[10]; if (fread(s, sizeof(int), 10, fp) < 0) { perror(“fread”); return -1; }
3.3 fwrite函数
#include <stdio.h> size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); 返回值: 成功时fwrite返回的值与nmemb相等;若小于nmemb表示出错,可以用perror函数查看错误原因。 参数: prt:要写入的数据缓冲区 size:每次写入的单元大小 nmemb:写入的单元数 stream:写入到哪个文件流
例程1:
struct student { int no; char name[8]; float score; } s[] = {{ 1, “zhang”, 97}, {2, “wang”, 95}}; fwrite(s, sizeof(struct student), 2, fp);
例程2:
write.c
#include <stdio.h> #define PATH "./1.txt" typedef struct PEOPLE{ char name[10]; int age; char addr[10]; }PEOPLE; //name和addr不能用指针形式,不然用fwrite的时候,用sizeof计算会有问题,导致写入后乱码 int main(void) { FILE *fp; PEOPLE Myself; int len; Myself.age = 24; strcpy(Myself.name, "zhuangsan"); strcpy(Myself.addr, "shenzhen"); fp = fopen(PATH, "w"); if(fp == NULL) { perror("fopen:");
return 0; } //sizeof计算的是已用内存长度,如果是字符串指针,用strlen len = fwrite(&Myself, sizeof(struct PEOPLE), 1, fp); if(len < 1) { perror("fwrite:");
return 0; } return 1; }
read.c
#include <stdio.h> typedef struct PEOPLE{ char name[10]; int age; char addr[10]; }PEOPLE; #define PATH "./1.txt" int main(void) { PEOPLE Myself; FILE *fp; int len; fp = fopen(PATH, "r"); if(fp == NULL) { perror("fopen:");
return 0; } len = fread(&Myself, sizeof(struct PEOPLE), 1, fp); if(len < 1) { perror("fread:");
return 0; } printf("name = %s \r\n", Myself.name); printf("age = %d \r\n", Myself.age); printf("addr = %s \r\n", Myself.addr); return 1; }
3.4 fclose函数
#include <stdio.h> int fclose(FILE *fp); 返回值: 成功返回0,刷新缓存,失败返回EOF,并且设置errno。
3.5 fseek函数
#include <stdio.h> int fseek(FILE *stream, long offset, int whence); 返回值: 重定位成功返回0,否则返回非零值 参数: stream:文件指针 offset:指针的偏移量(该指针和文件指针不一样,该指针指的是文件内部数据) whence:指针偏移起始位置 SEEK_SET 即0 文件开头 SEEK_CUR 即1 文件当前位置 SEEK_END 即2 文件结尾 fseek(fp,100,SEEK_SET);把fp指针移动到离文件开头100字节处 fseek(fp,100,SEEK_CUR);把fp指针移动到离文件当前位置100字节处 fseek(fp,100,SEEK_END);把fp指针退回到离文件结尾100字节处
fseek的作用:重定位文件内部的指针。
需要注意的是该函数不是重定位文件指针,而是重定位文件内部的指针,让指向文件内部数据的指针移到文件中我们感兴趣的数据上,重定位主要是这个目的。
说明:执行成功,则stream指向以whence为基准,偏移offset个字节的位置。执行失败(比方说offset偏移的位置超出了文件大小),则保留原来stream的位置不变。
3.6 fflush函数
#include<stdio.h> int fflush(FILE *stream); 参数: stream就是由fopen打开的文件流
fflush函数把文件流里的所有未写出的数据立即写出。调用fclose函数隐含执行了一次fflush操作,所以不必在调用fclose之前调用fflush。
3.7 fgetc、getc和getchar函数
#include<stdio.h> int fgetc(FILE *stream); int getc(FILE *stream); int getchar();
fgetc函数从文件流里取出下一个字节并把它作为一个字符返回。当它到达文件尾或出现错误时返回EOF。
getc函数作用同fgetc一样。
getchar函数的作用相当于getc(stdin),从标准输入里读取下一个字符。
fgetc和getc的区别:
fgetc是一个函数,而getc是一个宏。一般来说宏产生较大的代码,但是避免了函数调用的堆栈操作,所以速度会比较快。getc的参数不应当是具有副作用的表达式。宏都要避免用副作用表示式
1. 很多RISC处理器(如SUN SPARC)使用寄存器窗口重叠技术,(http://server.chinabyte.com/404/157904_1.shtml)
2.在寄存器充足的情况下,根本不需要堆栈操作,fgetc函数比getc宏更快
3. 在多线程的情况下,为了线程同步,getc和fgetc可能是相同的。
什么是副作用的表示式?
在这个程序中,z2我想算8*8,但是传入的参数是5+3,6+2,宏替换之后就变成了5+3*6+2,最后结果就成了25。
3.8 fputc、putc和putchar函数
#include<stdio.h> int fputc(int c, FILE *stream); int putc(int c, FILE *stream); int putchar(int c);
fputc函数把一个字符写到输出文件流中。返回值是写入的值,失败返回EOF。
putc函数作用同fputc函数.
putchar函数相当于putc(c,stdout),将字符c写到标准输出文件流中。
注意:即使fgetc和fputc这些的返回值和参数都是int的,但是读取和写入都是以字符来传递的。
3.9 fgets和gets函数
#include <stdio.h> char *fgets(char *s, int n, FILE *stream); char *gets(char *s);
参数:
-
str-- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
-
n-- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
-
stream-- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
fgets函数把读到的字符写到s指向的字符串里,直到遇到换行符或者已经读取n-1个字符或者遇到文件尾。它会把遇到的换行符也传递到接收字符串里,再加上一个表示结尾的空字符尾0。
fgets成功完成时返回一个指向字符串s的指针,如果文件流已经到达文件尾,fgets会设置这个文件流的EOF标识并返回一个空指针。如果出现读错误,fgets返回一个空指针并设置errno。
与gets相比使用这个好处是:读取指定大小的数据,避免gets函数从stdin接收字符串而不检查它所复制的缓存的容积导致的缓存溢出问题。
gets函数从标准输入读取数据并丢弃遇到的换行符,并在接收字符串尾部加上一个null字节。警告:gets对传输字符的个数并没有限制,所以它可能会溢出自己的传输缓冲区,所以应该避免使用gets函数。
实例:
#include<string.h> #include<stdio.h> int main ( void ) { FILE*stream; char string[]="Thisisatest"; char msg[20]; stream=fopen("DUMMY.FIL","w+"); fwrite(string,strlen(string),1,stream); fseek(stream,0,SEEK_SET); fgets(msg,strlen(string)+1,stream); printf("%s",msg); fclose(stream); return 0; }
3.10 fputs和puts函数
#include <stdio.h> int fputs(const char *str, FILE *stream); int puts(const char *str); 参数: str:这是一个数组,包含了要写入的以空字符终止的字符序列 stream:指向FILE对象的指针,该FILE对象标识了要被写入字符串的流 返回值: 函数返回值为非负整数,否则返回EOF(符号常量,其值为-1); 成功写入一个字符串后,文件的位置指针会自动后移
fputs是一个函数,具有的功能是向指定的文件写入一个字符串(不自动写入字符串结束标记符‘\0’)。
puts()函数用来向标准输出设备(屏幕)输出字符串并换行,具体为:把字符串输出到标准输出设备,将'\0'转换为回车换行。
fputs和puts的区别:
1.puts() 只能向标准输出流输出,而 fputs() 可以向任何流输出。
2.使用 puts() 时,系统会在自动在其后添加换行符;而使用 fputs() 时,系统不会自动添加换行符。
那么这是不是意味着使用 fputs() 时就要在后面添加一句“printf("\n");”换行呢?看情况!如果输入时使用的是 gets(),那么就要添加 printf 换行;但如果输入时用的是 fgets(),则不需要。
因为使用 gets() 时,gets() 会将回车读取出来并丢弃,所以换行符不会像 scanf 那样被保留在缓冲区,也不会被 gets() 存储;而使用 fgets() 时,换行符会被 fgets() 读出来并存储在字符数组的最后,这样当这个字符数组被输出时换行符就会被输出并自动换行。
但是也有例外,比如使用 fgets() 时指定了读取的长度,如只读取 5 个字符,事实上它只能存储 4 个字符,因为最后还要留一个空间给 '\0',而你却从键盘输入了多于 4 个字符,那么此时“敲”回车后换行符就不会被 fgets() 存储。数据都没有地方存放,哪有地方存放换行符呢!此时因为 fgets() 没有存储换行符,所以就不会换行了。
实例:
#include <stdio.h> #include <conio.h> int main(void) { int i; char string[20]; for(i=0;i<10;i++) string[i]='a'; string[10]='\0'; puts(string); getch(); return 0; }
puts输出字符串时要遇到'\0’也就是字符结束符才停止,如上面的程序加上一句 string[10]='\0';
3.11 printf、fprintf和sprintf函数
#include <stdio.h> int printf(const char *format, ...); int sprintf(const *s, const char *format, ...); int fprintf(FILE *stream, const char *format, ...);
printf函数将输出送到标准输出流。
sprintf函数把输出加上一个结尾空字符送到字符串s中。
fprintf函数把输出输送到指定的文件流中。
format为输出格式类型,通常有以下几种常用的转换控制符:
3.12 scanf、fscanf和sscanf函数
#include<stdio.h> int scanf(const char *format, ...); int fscanf(FILE *stream, const char *format, ...); int sscanf(const char *s,const char *format,. ...);
scanf系列函数使用一个格式字符串来控制输入数据的转化。
scanf函数从标准输入流中读入数据。fscanf函数从指定的文件流中读入数据。sscanf函数则从指定的字符串中读入数据。
format为输入格式类型,通常有以下几种常用的转换控制符:
用法:https://blog.csdn.net/respectableking/article/details/76478897
3.13 ftell函数、rewind函数
#include <stdio.h> long ftell(FILE *stream); void rewind(FILE *stream); rewind()函数无返回值,fseek成功返回0,ftell成功返回当前偏移量,失败返回-1,并且设置errno。
4. 例程
例1:
#include <string.h> #include <stdio.h> int main(void) { FILE *stream; char msg[] = "this is a test"; char buf[20]; if ((stream = fopen("DUMMY.FIL", "w+")) == NULL) { fprintf(stderr,"Cannot open output file.\n"); return 1; } /* write some data to the file */ fwrite(msg, strlen(msg)+1, 1, stream); /* seek to the beginning of the file */ fseek(stream, SEEK_SET, 0); /* read the data and display it */ fread(buf, 1, strlen(msg)+1, stream); printf("%s\n", buf); fclose(stream); return 0; }
例2:
#include <stdio.h> #include <stdlib.h> int main(int argc,char *argv[]) { FILE *fp1, *fp2; //流指针 char buf[1024]; //缓冲区 int n; //存放fread和fwrite函数的返回值 if(argc <=2) //如果参数错误 { printf("请输入正确的参数\n!"); //参数错误 } if ((fp1 = fopen(*(argv+1), "rb")) == NULL) //以只读方式打开源文件,读开始位置为文件开头 { printf("读源文件%s发生错误\n",*(argv+1)); return 1; //出错退出 } if ((fp2 = fopen(*(argv+2), "wb")) == NULL) //以只写方式打开目标文件,写开始位置为文件结尾 { printf("打开目标文件%s失败\n",*(argv+2)); return 2; //出错退出 } //开始复制文件,文件可能很大,缓冲一次装不下,所以使用一个循环进行读写*/ while ((n = fread(buf, sizeof(char), 1024, fp1)) > 0) { //读源文件,直到将文件内容全部读完*/ if (fwrite(buf, sizeof(char), n, fp2) == -1) { //将读出的内容全部写到目标文件中去 printf("写如文件发生错误\n"); return 3; /*出错退出*/ } } printf("从源文件%s读数据写入目标文件%s中完成\n",*(argv+1),*(argv+2)); //输出对应的提示 if(n == -1) { //如果因为读入字节小于0而跳出循环,则说明出错了*/ printf("读文件发生错误\n"); return 4; /*出错退出*/ } fclose(fp1); /*操作完毕,关闭源文件和目标文件*/ fclose(fp2); return 0; }
关于换行符的一些:https://blog.csdn.net/weixin_44879687/article/details/89739899