基于流的I/O操作与基于文件描述符的I/O操作过程十分相似,同样是使用相关的函数调用打开文件或设备,然后对文件进行读写,最后关闭文件。流I/O是由C语言的标准函数库提供的,这些I/O可以替代系统中提供的read和write函数。
流与缓存 流和FILE对象 基于文件的I/O函数都是针对文件描述符 的,当打开一个文件时,返回一个文件的描述符,然后通过该文件描述符进行后续的I/O操作。
而对于标准的I/O库,它的操作是围绕流(stream)进行的,当用标准I/O库打开或创建一个文件时,已使一个流与一个文件相结合。当打开一个流时,标准I/O函数fopen返回一个指向 FILE对象 的指针,该对象通常是一个结构体,它包含了I/O库为管理该流所需要的所有信息。
缓存 基于流的操作最终会调用read或write函数进行I/O操作,为了提高程序的运行效率,尽可能减少使用read和write调用的数量,流对象通常会提供缓冲区,以减少调用系统I/O库函数的次数。标准I/O提供了3种类型的缓存:
全缓存:直到缓冲区被填满,才调用系统I/O函数。
行缓存:直到遇到换行符“\n”,才调用系统I/O函数库。
无缓存:没有缓冲区,数据会立即读入或输出到外存文件和设备上。
缓冲区类型
定义的宏
全缓存
_IO_FULL_BUF
行缓存
_IO_LINE_BUF
无缓冲
_IO_UNBUFFERED
检测缓冲区类型和大小,buf_test.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <stdlib.h> int main (void ) { printf ("stdin is " ); if (stdin ->_flags&_IO_UNBUFFERED) printf ("unbuffered\n" ); else if (stdin ->_flags&_IO_LINE_BUF) printf ("line-buffered\n" ); else printf ("fully-buffered\n" ); printf ("buffer size is %ld\n" , stdin ->_IO_buf_end-stdin ->_IO_buf_base); printf ("file discriptor is %d\n\n" , fileno(stdin )); printf ("stdout is " ); if (stdout ->_flags&_IO_UNBUFFERED) printf ("unbuffered\n" ); else if (stdout ->_flags&_IO_LINE_BUF) printf ("line-buffered\n" ); else printf ("fully-buffered\n" ); printf ("buffer size is %ld\n" , stdout ->_IO_buf_end-stdout ->_IO_buf_base); printf ("file discriptor is %d\n\n" , fileno(stdout )); printf ("stderr is " ); if (stderr ->_flags&_IO_UNBUFFERED) printf ("unbuffered\n" ); else if (stderr ->_flags&_IO_LINE_BUF) printf ("line-buffered\n" ); else printf ("fully-buffered\n" ); printf ("buffer size is %ld\n" , stderr ->_IO_buf_end-stderr ->_IO_buf_base); printf ("file discriptor is %d\n\n" , fileno(stderr )); return 0 ; }
编译后执行:
1 2 3 4 5 6 7 8 9 10 11 12 $ ./buf_test stdin is fully-buffered buffer size is 0 file discriptor is 0 stdout is line-buffered buffer size is 1024 file discriptor is 1 stderr is unbuffered buffer size is 0 file discriptor is 2
对缓存的操作 在进行基于流的I/O操作时,缓存的使用将是不可或缺的。
设置缓存的属性 缓存的属性包括缓冲区的类型和大小,当调用fopen函数打开一个流时,就开辟了所需的缓冲区,系统通常会赋予其一个默认的属性值。可以通过如下函数设置缓冲区的属性值:
1 2 3 4 5 #include <stdio.h> void setbuf (FILE *fp, char *buf) ;void setbuffer (FILE *fp, char *buf, size_t size) ;void setlinebuf (FILE *fp) ;int setvbuf (FILE *fp, char *buf, int mode, size_t size) ;
setbuf用于将缓冲区设置为全缓存或无缓冲。参数buf为指向缓冲区的指针,当buf指向一个真实的缓冲区地址时,为全缓存;当buf为NULL时,为无缓存。
setbuffer的功能与setbuf类似,区别是由程序员自行指定缓冲区的大小size。
setlinebuf专用于将缓冲区设定为行缓存。
setvbuf函数比较灵活,可以方便地设置缓存的属性。
设置缓冲区属性,buf_set.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <stdio.h> #include <stdlib.h> #define SIZE 1024 int main (void ) { char buf[SIZE]; if (setvbuf(stdin , buf, _IONBF, SIZE)!=0 ) { printf ("error!\n" ); exit (1 ); } printf ("OK, set successful!\n" ); printf ("stdin is" ); if (stdin ->_flags&_IO_UNBUFFERED) printf ("unbuffered\n" ); else if (stdin ->_flags&_IO_LINE_BUF) printf ("line-buffered\n" ); else printf ("fully-buffered\n" ); printf ("buffer size is %ld\n" , stdin ->_IO_buf_end-stdin ->_IO_buf_base); printf ("file discriptor is %d\n\n" , fileno(stdin )); if (setvbuf(stdin , buf, _IOFBF, SIZE)!=0 ) { printf ("error!\n" ); exit (1 ); } printf ("OK, change successful!\n" ); printf ("stdin is" ); if (stdin ->_flags&_IO_UNBUFFERED) printf ("unbuffered\n" ); else if (stdin ->_flags&_IO_LINE_BUF) printf ("line-buffered\n" ); else printf ("fully-buffered\n" ); printf ("buffer size is %ld\n" , stdin ->_IO_buf_end-stdin ->_IO_buf_base); printf ("file discriptor is %d\n\n" , fileno(stdin )); return 0 ; }
编译后执行:
1 2 3 4 5 6 7 8 9 10 $ ./buf_set OK, set successful! stdin isunbuffered buffer size is 1 file discriptor is 0 OK, change successful! stdin isfully-buffered buffer size is 1024 file discriptor is 0
可以看出,stdin的缓冲区类型发生了变化。
缓存的冲洗 缓存冲洗是指将I/O操作写入缓存中的内容清空,清空可以是将流的内容完全丢掉,也可以是将其保存到文件中:
1 2 3 4 #include <stdio.h> int fflush (FILE *fp) ;#include <stdio_ext.h> void fpurge (FILE *fp) ;
fflush函数用于将缓冲区中尚未写入文件的数据强制性地保存到文件,调用成功返回0,失败返回EOF。
fpurge函数用于将缓冲区中的数据完全清除,该函数使用较少。
流的打开与关闭 当用户使用基于流的缓冲时会由C语言的库函数提供对缓冲的操作,用户则不用再耗费时间和精力控制缓冲区了。
流的打开 1 2 3 4 #include <stdio.h> FILE *fopen (const char *pathname, const char *type) ;FILE *freopen (const char *pathname, const char *type, FILE *fp) ;FILE *fdopen (int fd, const char *type) ;
运行成功返回文件指针,出错返回NULL(空指针)。
fopen打开一个路径名为pathname的文件。
freopen在一个特定的流上(fp)打开一个指定的文件(pathname),若该流已经打开,则先关闭该流。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准出错。
fdopen取一个现存的文件描述符(可能从open、dup、dup2、fcntl、或pipe函数得到),并使一个标准的I/O流与该描述符相结合。次函数常用于由创建管道和网络通信函数获得的描述符,因为这些特殊类型的文件不能使用标准I/O的fopen函数打开,首先必须先调用设备专用函数以获得一个文件描述符,然后调用fdopen使一个标准I/O流与该描述符相结合。
type值
操作文件类型
是否新建文件
是否清空文件
可读
可写
读写开始位置
r
文本文件
×
×
√
×
文件开头
r+
文本文件
√
×
√
√
文件开头
w
文本文件
√
√
×
√
文件开头
w+
文本文件
√
√
√
√
文件开头
a
文本文件
×
√
×
√
文件结尾
a+
文本文件
×
√
√
√
文件结尾
rb
二进制文件
×
×
√
×
文件开头
r+b或rb+
二进制文件
√
×
√
√
文件开头
wb
二进制文件
√
√
×
√
文件开头
w+b或wb+
二进制文件
√
√
√
√
文件开头
ab
二进制文件
×
√
×
√
文件结尾
a+b或ab+
二进制文件
×
√
√
√
文件结尾
fopen出错的一般原因:
指定的文件路径有误
type参数是一个非法字符串
文件的操作权限不够
流的关闭 1 2 #include <stdio.h> int fclose (FILE *fp) ;
运行成功返回0,出错返回EOF。
EOF是一个定义在<stdio.h>中的宏定义,其值为-1。
打开与关闭一个流测试,stream.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> #include <fcntl.h> #include <stdlib.h> int main (void ) { FILE *fp; int fd; if ((fp = fopen("hello.txt" , "r+" )) == NULL ) { printf ("fail to open!\n" ); exit (1 ); } fprintf (fp, "Hello! I like Linux C program!\n" ); fclose(fp); if ((fd = open("hello.txt" , O_RDWR)) == -1 ) { printf ("fail to open!\n" ); exit (1 ); } if ((fp = fdopen(fd, "a+" )) == NULL ) { printf ("fail to open stream!\n" ); exit (1 ); } fprintf (fp, "I am doing Linux C programs!\n" ); fclose(fp); return 0 ; }
编译后运行(运行前先在当前目录新建一个hello.txt空文件):
程序没有输出(本来就没有),看一下hello.txt:
1 2 3 4 $ cat hello.txt Hello! I like Linux C program! I am doing Linux C programs!
再次运行程序后,查看hello.txt:
1 2 3 4 5 6 $ ./stream $ cat hello.txt Hello! I like Linux C program! I am doing Linux C programs! I am doing Linux C programs!
可以看到输出追加了一条,原因在于文件头的被覆盖,文件尾的被追加。
流的读写 一旦打开了流,则可在4种不同类型的I/O中进行选择,来对其进行读写操作:
基于字符的I/O:每次读写一个字符数据
基于行的I/O:当输入内容遇到换行符\n
时,将之前的内容送到缓冲区
直接I/O:输入输出操作以记录为单位进行读写
格式化I/O:格式化输入输出为最常见的方式,如printf或scanf
基于字符的I/O 字符的输入 以下3个函数用于一次读取一个字符:
1 2 3 4 5 #include <stdio.h> int getc (FILE *fp) ;int fgetc (FILE *fp) ;int getchar (void ) ;
运行成功返回读入字符的值,出错返回EOF。
字符的输出 以下3个函数用于字符输出:
1 2 3 4 5 #include <stdio.h> int putc (int c, FILE *fp) ;int fputc (int c, FILE *fp) ;int putchar (int c) ;
运行成功返回读入字符的值,出错饭返回EOF。
基于字符I/O方式的文字复制,char_copy.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <stdio.h> #include <stdlib.h> #define Src_File "./hello.txt" #define Des_File "./test.txt" int main (void ) { FILE *fp1, *fp2; int c; if ((fp1=fopen(Src_File, "rb" )) == NULL ) { printf ("fail to open source file\n" ); exit (1 ); } if ((fp2=fopen(Des_File, "wb" )) == NULL ) { printf ("fail to open source file\n" ); exit (1 ); } while ((c=fgetc(fp1))!=EOF) { if (fputc(c, fp2) == EOF) { printf ("fail to write\n" ); exit (1 ); } if (fputc(c, stdout ) == EOF) { printf ("fail to write\n" ); exit (1 ); } } fclose(fp1); fclose(fp2); return 0 ; }
编译后运行:
1 2 3 4 5 $ ./char_copy Hello! I like Linux C program! I am doing Linux C programs! I am doing Linux C programs!
查看test.txt:
1 2 3 4 5 $ cat test.txt Hello! I like Linux C program! I am doing Linux C programs! I am doing Linux C programs!
可以看到,从hello.txt复制的内容已输出到屏幕和test.txt。
基于行的I/O 行的输入 fgets和gets函数实现输入一行字符串,函数原型如下:
1 2 3 4 #include <stdio.h> char *fgets (char *buf, int n, FILE *fp) ;char *gets (char *buf) ;
运行成功返回缓冲区的首地址,出错或已到文件尾返回EOF。
gets函数与fgets函数最大的不同是gets的缓冲区虽然由用户提供,但无法指定一次最多读入多少字节的内容。因此gets函数使用起来有风险,不推荐使用 。
行的输出 fputs和puts函数实现输出一行字符串,函数原型如下:
1 2 3 4 #include <stdio.h> char fputs (const char *buf, FILE *restrict fp) ;char puts (const char *str) ;
fputs函数的第1个参数为输出缓冲区,第2个参数为要输出的文件。成功返回输出字节数,失败返回-1。
puts函数用于向标准输出输出一行字符串。
直接I/O 直接I/O,也称执行二进制I/O操作,可以直接读取NULL回换行符,函数原型如下:
1 2 3 4 #include <stdio.h> size_t fread (void *ptr, size_t size, size_t nmemb, FILE *fp) ;size_t fwrite (const void *ptr, size_t size, size_t nmember, FILE *fp) ;
函数返回读或写的对象数。
基于直接I/O方式的文件复制,direct_copy.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <stdio.h> #include <stdlib.h> #define Src_File "./hello.txt" #define Des_File "./test.txt" int main (void ) { FILE *fp1, *fp2; char buf[1024 ]; int n; if ((fp1=fopen(Src_File, "rb" ))==NULL ) { printf ("fail to open source file\n" ); exit (1 ); } if ((fp2=fopen(Des_File, "ab" ))==NULL ) { printf ("fail to open des file\n" ); exit (1 ); } while ((n=fread(buf, sizeof (char ), 1024 , fp1))>0 ) { if (fwrite(buf, sizeof (char ), n, fp2)==-1 ) { printf ("fail to write\n" ); exit (1 ); } } if (n==-1 ) { printf ("fail to read\n" ); exit (1 ); } fclose(fp1); fclose(fp2); return 0 ; }
该程序是将hello.txt中的内容复制追加到test.txt,编译后执行,并查看test.txt:
1 2 3 4 5 6 7 8 9 $ ./direct_copy $ cat test.txt Hello! I like Linux C program! I am doing Linux C programs! I am doing Linux C programs! Hello! I like Linux C program! I am doing Linux C programs! I am doing Linux C programs!
可以看到内容被复制到了test.txt。
格式化I/O 格式化输出 4个printf函数:
1 2 3 4 5 6 #include <stdio.h> int printf (const char *format, ...) ;int fprintf (FILE *fp, const char *format, ...) ;int sprintf (char *str, const char *format, ...) ;int snprintf (char *str, size_t size, const char *format, ...) ;
printf用于向标准输出流中输出数据
fprintf用于向指定的流中输出数据,参数fp指向要进行输出的流
sprintf用于向指定的流中输出一个字符串
snprintf的作用与sprintf相似,不同的是snprintf可以处理缓冲区
格式化输入 3个scanf函数:
1 2 3 4 5 #include <stdio.h> int scanf (const char *format, ...) ;int fscanf (FILE *fp, const char *format, ...) ;int sscanf (char *str, const char *format, ...) ;
scanf用于从标准输入流中输入数据
fscanf用于从指定的流中输入数据,参数fp指向该的流
sscanf用于从指定的字符串中输入数据,参数str指向该字符串
使用fprintf和fscanf函数实现输入和输出,format_io.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #define File_path "./hello.txt" int main (void ) { FILE *fp; char buf[] = "Hello! Linux C" ; char buf2[80 ]; fp = fopen(File_path, "w" ); fprintf (fp, "%s" , buf); fprintf (fp, "\n" ); fclose(fp); fp = fopen(File_path, "r" ); fscanf (fp, "%s" , &buf2); fclose(fp); printf ("%s\n" , buf2); return 0 ; }
编译后运行:
查看hello.txt中的内容:
1 2 3 $ cat hello.txt Hello! Linux C
可以看到buf中的字符串已写入到hello.txt(该文件原来有内容,现在被清除了)。另外,打印的buf2的结果只是一部分,原因是scanf读取文件流时,遇到空格符、制表符和回车符时会自动终止读取字符。
参考:《精通Linux C编程》- 程国钢