背景
在上一篇文章unix高级编程系列之文件I/O中,我们已经介绍了文件I/O是非缓存的,那么与非缓存I/O对应的就是具有缓存的I/O。而标准I/O库就是具有缓存的I/O,今天我们就来认识一下它的独特魅力及相关注意事项。
标准I/O 与 文件I/O的区别
我个人认为标准I/O和文件I/O存在以下几点不同:
- 文件I/O的操作对象是文件描述符
fd
,而标准I/O操作的对象是FILE
对象。 - 标准I/O提供更为丰富的接口。比如:格式化输入输出、
perror
错误处理接口等。 - 标准I/O使用起来更为方便简洁,不需要用户在意不同系统的块长度及缓冲区分配(在文件I/O章节,我们知道不同的buffer大小,对文件读写性能有影响)。内部供了完善的缓冲机制,可以减少使用read和write系统调用的次数,从而提高效率。
- 标准I/O提供了一层抽象,使得代码在不同的系统中移植更为容易。
而标准I/O的实质就是基于文件I/O做了一层封装,内部做了一些细节处理。比如缓冲区分配、以优化的块长度执行I/O操作,达到提高效率的目的。区别大致如下图:
而标准I/O的缓存也就是缓存在这C库函数中。
初识流概念
相对于文件I/O,标准I/O的操作是围绕流进行的。你可以认为流就是一个FILE
对象指针。它包含了标准I/O管理该流所有的信息。包括实际I/O的文件描述符(因为标准I/O内部也是调用文件I/O接口)、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓存区中的字符数以及出错标志等。
我们知道文件I/O为标准输入、标准输出、错误输出定义了三个文件描述符:STDIN_FILENO
、STDOUT_FILENO
、STDERR_FILENO
;同样的标准I/O也定义了三个流:stdin
、stdout
、stderr
。
全缓冲、行缓冲、不带缓冲
标准I/O核心的优点就是提供缓存尽可能减少使用read
和write
的调用次数。但是标准I/O是如何管理这个缓冲的呢?如果不了解,避免不了会吃大亏。比如下面的例子:
#include<stdio.h>
int main()
{
int i = 0;
while(1)
{
printf("hello world %d",i++);
sleep(1);
}
return 0;
}
对于上述示例,你认为现象应该如何呢?不妨先自己预测一下;
现象:程序刚开始运行时,界面不会有任何输出。当运行大约73秒时,则会一次性将所有打印内容输出。现象如下:
这其实就是标准I/O的缓冲机制(printf
也属于标准I/O库中的接口)。 标准I/O提供了三种类型的缓冲:
-
全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作,或主动调用
fflush
冲洗一个流。 -
行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库才执行I/O操作。当流涉及到终端时,通常使用行缓冲。
注:我们需要考虑两个问题:
- 当一行数据超过缓冲区时,如何处理?
因为标准I/O的缓冲区是有限制的,长度不是无限大。因此只要填满了缓冲区,那么即使还没有遇到一个换行符,也需要进行I/O操作。
分析上述示例:
- 由于
printf
属于标准I/O,并且示例中的程序默认向终端输出,因此流为行缓冲。因此,并不会每秒输出信息到终端。 - 为什么会在74秒左右输出呢?因为每次打印
hello world %d
占13 or 14Byte。根据上图,共打印了14*73 - 10 + 12 = 1024
Byte,而缓冲区的大小为1024Byte。
- 当任何时候只要通过标准I/O库从一个不带缓冲的流,或者一个行缓冲的流得到输入数据,那么就会冲洗所有行缓冲输出流。
在上述示例中进行修改,如下:
#include<stdio.h>
int main()
{
int i = 0;
while(1)
{
printf("hello world %d",i++);
sleep(1);
if(i == 15)
{
int num = 0;
scanf("%d",num);
}
}
return 0;
}
与之前演示效果不同点:等待15秒后,终端会将日志输出。
- 不带缓冲。标准错误流stderr通常是不带缓冲的。如下示例,你会发现终端间隔1秒输出信息。
int main()
{
int i = 0;
while(1)
{
fputs("hello world",stderr);
sleep(1);
}
return 0;
}
那么如何判断一个流是全缓冲、行缓冲、还是无缓冲呢?这似乎是一个难题。ISO C 有如下要求,我们可以作为参考。
- 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的;大部分系统默认指向终端设备的流,则是行缓冲,否则是全缓冲。
- 标准错误绝不会是全缓冲;大部分系统标准错误是不带缓冲的。
当然我们可以通过以下接口设置流的缓冲类型。
#include<stdio.h>
void setbuf(FILE* restrict fp, char* restrict buf);
int setvbuf(FILE* restrict fp, char* restrict buf, int mode, size_t size);
setbuf
接口只能打开或关闭缓机制。若打开全缓冲,则需将其中buf
设置为一个长度为BUFSIZE的缓冲区。若不带缓冲,将buf
设置为NULL即可。
setvbuf
接口可以精确的指定缓冲类型。只是由mode
参数实现的。
- _IOFBF 全缓冲
- _IOLBF 行缓冲
- _IONBF 不带缓冲
当mode
设置为不带缓冲的流时,则忽略buf
和size
参数。如果指定全缓冲或行缓冲,则buf
和 size
可选择指定一个缓冲区及其长度。
任何时候,我们都可强制冲洗流:
int fflush(FILE* fp);
注: 此函数只是将未写的数据都被传送至内核。但并没有保证数据落盘。
打开和关闭流
可通过以下接口打开一个标准I/O流:
#include<stdio.h>
FILE* fopen(const char* restrict pathname,const char* restrict type);
FILE* freopen(const char* restrict pathname,const char* restrict type,FILE * restrict fp);
FILE* fdopen(int fd, const char* type);
区别如下:
fopen
函数打开名为pathname
的一个指定文件。freopen
函数在一个指定的流上打开一个指定的文件,若该流文件已经打开,则先关闭该流。此函数一般用于将一个指定的文件打开为一个预定义的流:
标准输入、标准输出、错误输出。如下示例:
#include<stdio.h>
int main()
{
freopen("./test.log","ab+",stdout);
printf("hello world\n");
return 0;
}
通过freopen
接口将标准输出执行了test.log
文件中。按照预期hello world
字符串应该输出到文件中,而不是终端。实际也是如此:
xieyihua@xieyihua:~/test$ gcc 2.c -o 2
xieyihua@xieyihua:~/test$ ./2
xieyihua@xieyihua:~/test$ cat test.log
hello world
xieyihua@xieyihua:~/test$
fdopen
函数将一个已有的文件描述符与标准I/O的流相结合。比如,我们通过open
获取文件描述符,再通过fdopen
获取与该文件描述符关联的标准I/O流。后续就可以通过标准I/O接口对该文件描述符控制了。
同理,可通过int fileno(FILE* fp);
接口,从一个标准I/O流,获取对应的文件描述符。
其中type
参数指定对I/O流的读写方式,取值范围如下表:
type | 说明 | open标志 |
---|---|---|
r或rb | 为读而打开 | O_RDONLY |
w或wb | 把文件长度截至0长,或为写而创建 | O_WRONLY | O_CREAT | O_TRUNC |
a或ab | 追加;为在文件尾写而打开,或为写而创建 | O_WRONLY | O_CREAT | O_APPEND |
r+或r+b或rb+ | 为读和写而打开 | O_RDWR |
w+或w+b或wb+ | 把文件截断至0长,或为读和写而打开 | O_RDWR | O_CREAT | O_TRUNC |
a+或a+b或ab+ | 为在文件尾读和写而打开或创建 | O_RDWR | O_CREAT | O_APPEND |
注:b
字符的作用是用于区分文本文件和二进制文件。但是unix内核并不对这两种文件进行区分,所以并无实际作用。
可以调用fclose
关闭一个打开的流,其声明如下:
#include<stdio.h>
int fclose(FILE* fp);
注意:当文件流被关闭前,或进程正常终止(exit
或main
退出)前,将会冲洗缓冲中的输出数据,缓冲区中的任何输入数据都会被丢弃。所有标准I/O流都会被关闭。
读和写流
对于一个打开的流而言,我们可以通过标准I/O对其进行读写。大体上可以分为三种不同类型的非格式化I/O:
- 每次一个字符的I/O,一次只读或写一个字符。
#include <stdio.h>
int getc(FILE* fp);
int fgetc(FILE* fp);
int getchar(void);
注:三个函数的返回值:若成功,返回下一个字符;若已达到文件尾端或出错,返回EOF。
getchar
等同于fgetc(stdin)
;getc
可以被实现为宏,而fgetc
不能实现为宏。即需要注意:1.getc
的参数不应当是具有副作用的表达式且不可以作为指针函数进行传参。2.fgetc
的调用时间可能会长些,因为涉及到函数上下文切换。
因为不管是出错(非预期)还是达到文件尾(预期),返回的错误都是同样的。为了区分两种不同的场景,需要调用下面的接口用于判断。
#include<stdio.h>
int ferror(FILE *fp);
int feof(FILE* fp);
可通过下面的接口输出单个字符:
#include <stdio.h>
int putc(int c, FILE* fp);
int fputc(int c, FILE* fp);
int putchar(int c);
- 每次一行的I/O,以换行符终止。
#include<stdio.h>
char* fgets(char* restrict buf, int n, FILE* restrict fp);
char* gets(char* buf);
//两个函数返回值:若成功返回buf;若已到达文件尾端或出错,返回NULL;
注:gets
不建议使用,因为没有指定buf
缓冲区的大小。容易造成缓冲区溢出。
#include<stdio.h>
int fputs(const char* restrict str, FILE* restrict fp);
int puts(const char* str);
//两个函数返回值:若成功,返回非负值;若错误,返回EOF
这两个的区别:fputs
将\0
字符写到指定流中,puts
将\0
字符写到指定流中,并追加一个换行符。
- 直接I/O,每次读或写某种数量的对象,一般用于读取二进制数据。
#include<stdio.h>
size_t fread(void* restrict ptr,size_t size,size_t nobj,FILE* restrict fp);
size_t fwrite(const void * ptr,size_t size, size_t nobj,FILE* restrict fp);
//两个函数的返回值:读或写的对象数
对于这两个接口,我们需要注意两点:
- 返回值与预期不符。对于读,如果出错或达到文件尾端,则此数字可以少于
nobj
,此时需要通过ferror
或feof
判断是哪一种情况;对于写,返回值少于nobj
,则一定属于出错。 - 字节序问题。不同系统中,数据的存储方式可能不一样。导致读与写的解析不一致。
格式化I/O
格式化I/O分为输入和输出。接口如下,可以浏览一遍,加深印象:
#include<stdio.h>
int printf(const char* restrict fotmat,...);
int fprintf(FILE* restrict fp, const char * restrict format,...);
// printf(const char* restrict fotmat,...); = fprintf(stdout,const char * restrict format,...);
int dprintf(int fd, const char* restrict format,...);
//3个函数返回值:若成功,返回输出字符数;若出错,返回负值
int sprintf(char* restrict buf,const char * restrict format,...);
//返回值:若成功,返回存入数组的字符数;若编码出错,返回负值。
int snprintf(char* restrict buf,size_t n, const char * restrict format,...);
//返回值:若缓冲区足够大,返回将要存入数组的字符数;若编码出错,返回负值。
#include<stdio.h>
int scanf(const char* restruct format, ...);
int fscanf(FILE* restrict fp, const char* restrict format, ...);
int sscanf(cosnt char * restrict buf,cosnt char* restrict format, ...);
总结
标准I/O提供了流的概念,简化了文件I/O操作,提供了更丰富的接口和缓冲机制,使得I/O操作更加高效和方便。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途