首页 > 编程语言 >【unix高级编程系列】标准I/O

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

时间:2024-07-10 17:55:23浏览次数:22  
标签:char 系列 文件 int 缓冲 编程 restrict unix FILE

在这里插入图片描述

背景

在上一篇文章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,人生尽是坦途

在这里插入图片描述

在这里插入图片描述

标签:char,系列,文件,int,缓冲,编程,restrict,unix,FILE
From: https://blog.csdn.net/xieyihua1994/article/details/140330539

相关文章

  • 不同深度的埋点事件如何微妙地改变广告系列的成本
    /  作者简介  /本篇文章来自现金贷领域市场投放大佬 亮哥 的投稿,主要分享了在广告投放过程中,不同深度的埋点事件如何微妙地改变广告系列的成本的相关经验,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。/  前言 /在广告投放的征途中,相信每位同行必定经历......
  • 1 搭建编程环境
    对于一个刚从图形化编程进阶来的小白来说,Python是一门不错的语言。它关键字偏少,采用面向对象(OOP)的方式来进行代码的运行。不要担心你学不会,因为Python是一门精简的语言,易上手,我敢说,你只要熟练,就永远忘不掉他了。好了,话不多说,让我们开始Python之旅吧!!!1.1下载Python访问该网站:......
  • 软件设计13丨结构化编程:为什么做设计时仅有结构化编程是不够的?
    上一讲,我们讲到了编程范式,现在开发的一个重要趋势就是多种编程范式的融合,显然,这就要求我们对各种编程范式都有一定的理解。从这一讲开始,我们就展开讨论一下几个主要的编程范式。首先,我们来讨论程序员们最熟悉的编程范式:结构化编程。很多人学习编程都是从C语言起步的,C语言......
  • 软件设计12丨编程范式:明明写的是Java,为什么被人说成了C代码?
    在上一个小模块,我给你讲了程序设计语言,帮助你重新审视一下自己最熟悉的日常工具。但是,使用程序设计语言是每个程序员都能做到的,可写出的程序却是千差万别的。这一讲,我们就来看看这些差异到底是怎样造成的。在开始之前,我先给你讲一个小故事。在一次代码评审中,小李兴致勃勃地......
  • VBA编程:从入门到高手之路
    引言VisualBasicforApplications(VBA)是MicrosoftOffice套件中内置的编程语言,广泛用于自动化办公任务,特别是在Excel中。本文将带您从VBA的基础知识开始,逐步深入到高级技巧,助您成为VBA编程高手。1.VBA基础1.1什么是VBA?VBA是一种事件驱动的编程语言,允许用户......
  • 对Stream函数式编程的理解
    什么是StreamStream被翻译为流,它的工作过程像将一瓶水导入有很多过滤阀的管道一样,水每经过一个过滤阀,便被操作一次,比如过滤,转换等,最后管道的另外一头有一个容器负责接收剩下的水。示意图如下:首先通过source产生流,然后依次通过一些中间操作,比如过滤,转换,限制等,最后结束对流的操......
  • 【转】-Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
    Java并发编程:CountDownLatch、CyclicBarrier和Semaphore该博客转载自​Matrix海子​的​Java并发编程:CountDownLatch、CyclicBarrier和Semaphore在java1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下......
  • 【转】-Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
    Java并发编程:CountDownLatch、CyclicBarrier和Semaphore该博客转载自​Matrix海子​的​Java并发编程:CountDownLatch、CyclicBarrier和Semaphore在java1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下......
  • 深入理解Java中的并发编程
    深入理解Java中的并发编程大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!并发编程是Java开发中的一个重要领域,通过并发编程,可以提高程序的执行效率和资源利用率。本文将深入探讨Java中的并发编程,包括线程的创建、同步机制、并发集合、线程池和并发工具类......
  • 数据血缘系列(3)—— 数据血缘可视化之美
    大家好,我是独孤风。在当今数据驱动的商业环境中,数据治理成为企业成功的关键因素之一,而数据血缘正是数据治理成功的一个关键。本文我们详细探讨下数据血缘可视化是什么,该如何实现。并顺便对比一下ApacheAtlas、Datahub、Openmetadata、Marquez、SQLLineage、Amundsen的数据血缘......