【unix高级编程系列】标准I/O

在这里插入图片描述

背景

在上一篇文章unix高级编程系列之文件I/O中,我们已经介绍了文件I/O是非缓存的,那么与非缓存I/O对应的就是具有缓存的I/O。而标准I/O库就是具有缓存的I/O,今天我们就来认识一下它的独特魅力及相关注意事项。

标准I/O 与 文件I/O的区别

我个人认为标准I/O和文件I/O存在以下几点不同:

  1. 文件I/O的操作对象是文件描述符fd,而标准I/O操作的对象是FILE对象。
  2. 标准I/O提供更为丰富的接口。比如:格式化输入输出、perror错误处理接口等。
  3. 标准I/O使用起来更为方便简洁,不需要用户在意不同系统的块长度及缓冲区分配(在文件I/O章节,我们知道不同的buffer大小,对文件读写性能有影响)。内部供了完善的缓冲机制,可以减少使用read和write系统调用的次数,从而提高效率。
  4. 标准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_FILENOSTDOUT_FILENOSTDERR_FILENO;同样的标准I/O也定义了三个流:stdinstdoutstderr

全缓冲、行缓冲、不带缓冲

标准I/O核心的优点就是提供缓存尽可能减少使用readwrite的调用次数。但是标准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提供了三种类型的缓冲:

  1. 全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作,或主动调用fflush冲洗一个流。

  2. 行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库才执行I/O操作。当流涉及到终端时,通常使用行缓冲

注:我们需要考虑两个问题:

  • 当一行数据超过缓冲区时,如何处理

因为标准I/O的缓冲区是有限制的,长度不是无限大。因此只要填满了缓冲区,那么即使还没有遇到一个换行符,也需要进行I/O操作。
分析上述示例:

  1. 由于printf属于标准I/O,并且示例中的程序默认向终端输出,因此流为行缓冲。因此,并不会每秒输出信息到终端。
  2. 为什么会在74秒左右输出呢?因为每次打印hello world %d占13 or 14Byte。根据上图,共打印了14*73 - 10 + 12 = 1024Byte,而缓冲区的大小为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秒后,终端会将日志输出。

  1. 不带缓冲。标准错误流stderr通常是不带缓冲的。如下示例,你会发现终端间隔1秒输出信息。
int main()
{
    int i = 0;
    while(1)
    {
        fputs("hello world",stderr);
        sleep(1);
    }
    return 0;
}

那么如何判断一个流是全缓冲、行缓冲、还是无缓冲呢?这似乎是一个难题。ISO C 有如下要求,我们可以作为参考。

  1. 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的;大部分系统默认指向终端设备的流,则是行缓冲,否则是全缓冲
  2. 标准错误绝不会是全缓冲;大部分系统标准错误是不带缓冲的

当然我们可以通过以下接口设置流的缓冲类型。

#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设置为不带缓冲的流时,则忽略bufsize参数。如果指定全缓冲或行缓冲,则bufsize可选择指定一个缓冲区及其长度。

任何时候,我们都可强制冲洗流

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);

注意:当文件流被关闭前,或进程正常终止(exitmain退出)前,将会冲洗缓冲中的输出数据,缓冲区中的任何输入数据都会被丢弃。所有标准I/O流都会被关闭。

读和写流

对于一个打开的流而言,我们可以通过标准I/O对其进行读写。大体上可以分为三种不同类型的非格式化I/O:

  1. 每次一个字符的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);
  1. 每次一行的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字符写到指定流中,并追加一个换行符。

  1. 直接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);
        //两个函数的返回值:读或写的对象数

对于这两个接口,我们需要注意两点:

  1. 返回值与预期不符。对于读,如果出错或达到文件尾端,则此数字可以少于nobj,此时需要通过ferrorfeof判断是哪一种情况;对于写,返回值少于nobj,则一定属于出错。
  2. 字节序问题。不同系统中,数据的存储方式可能不一样。导致读与写的解析不一致。

格式化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,人生尽是坦途

在这里插入图片描述

在这里插入图片描述

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢艺华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值