Linux基于流的IO操作

举报
码农爱学习 发表于 2021/11/29 23:29:41 2021/11/29
【摘要】 基于流的I/O操作与基于文件描述符的I/O操作过程十分相似,同样是使用相关的函数调用打开文件或设备,然后对文件进行读写,最后关闭文件。流I/O是由C语言的标准函数库提供的,这些I/O可以替代系统中提供的read和write函数。

基于流的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:

#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;
}

编译后执行:

$ ./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函数打开一个流时,就开辟了所需的缓冲区,系统通常会赋予其一个默认的属性值。可以通过如下函数设置缓冲区的属性值:

#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:

#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));
​
    // 将标准输入的缓冲区类型设置为全缓冲,缓存大小为1024
    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;
}

编译后执行:

$ ./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操作写入缓存中的内容清空,清空可以是将流的内容完全丢掉,也可以是将其保存到文件中:

#include <stdio.h>
int fflush(FILE *fp);
#include <stdio_ext.h>
void fpurge(FILE *fp);
  • fflush函数用于将缓冲区中尚未写入文件的数据强制性地保存到文件,调用成功返回0,失败返回EOF。

  • fpurge函数用于将缓冲区中的数据完全清除,该函数使用较少。

流的打开与关闭

当用户使用基于流的缓冲时会由C语言的库函数提供对缓冲的操作,用户则不用再耗费时间和精力控制缓冲区了。

流的打开

#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参数是一个非法字符串

  • 文件的操作权限不够

流的关闭

#include <stdio.h>
int fclose(FILE *fp);

运行成功返回0,出错返回EOF。

EOF是一个定义在<stdio.h>中的宏定义,其值为-1。

打开与关闭一个流测试,stream.c:

#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空文件):

$ ./stream

程序没有输出(本来就没有),看一下hello.txt:

$ cat hello.txt
Hello! I like Linux C program!
I am doing Linux C programs!

再次运行程序后,查看hello.txt:

$ ./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个函数用于一次读取一个字符:

#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);

运行成功返回读入字符的值,出错返回EOF。

字符的输出

以下3个函数用于字符输出:

#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:

#include <stdio.h>
#include <stdlib.h>
​
#define Src_File "./hello.txt"
#define Des_File "./test.txt"
​
int main(void)
{
    FILE *fp1, *fp2; // 源文件和目标文件的文件指针
    int c; // 要进行输入和输出的字符
    // 以只读方式打开源文件hello.txt
    if((fp1=fopen(Src_File, "rb")) == NULL)
    {
        printf("fail to open source file\n");
        exit(1);
    }
    // 以只写方式打开目标文件test.txt
    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;
}

编译后运行:

$ ./char_copy
Hello! I like Linux C program!
I am doing Linux C programs!
I am doing Linux C programs!

查看test.txt:

$ 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函数实现输入一行字符串,函数原型如下:

#include <stdio.h>
char *fgets(char *buf, int n, FILE *fp);
char *gets(char *buf);

运行成功返回缓冲区的首地址,出错或已到文件尾返回EOF。

gets函数与fgets函数最大的不同是gets的缓冲区虽然由用户提供,但无法指定一次最多读入多少字节的内容。因此gets函数使用起来有风险,不推荐使用

行的输出

fputs和puts函数实现输出一行字符串,函数原型如下:

#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回换行符,函数原型如下:

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

函数返回读或写的对象数。

  • fread用于执行直接输出操作

  • fwrite用于执行直接输入操作,此函数有两个常见的用法:

    • 读写一个二进制组,如将一个浮点型数组的第2~5个元素写到一个文件上:

      float data[10];
      if(fwrite(&data[2], sizeof(float), 4, fp)!=4)
          printf("fwrite error!\n");  
    • 读或写一个结构:

      struct
      {
          short count;
          long total;
          char name[NAME_SIZE];
      }item;
      if(fwrite(&item, sizeof(item), 1, fp)!=1)
          printf("fwrite error!\n");

基于直接I/O方式的文件复制,direct_copy.c:

#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);
        }
    }
    // 如果因为读入字节小于0而跳出循环,则说明出错了
    if(n==-1)
    {
        printf("fail to read\n");
        exit(1);
    }

    fclose(fp1);
    fclose(fp2);

    return 0;
}

该程序是将hello.txt中的内容复制追加到test.txt,编译后执行,并查看test.txt:

$ ./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函数:

#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函数:

#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:

#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);// 向该文件流输出字符串数据buf
    fprintf(fp, "\n");
    fclose(fp);

    // 再以只读方式打开文件
    fp = fopen(File_path, "r");
    fscanf(fp, "%s", &buf2);// 将该文件流中的数据读入到buf2
    fclose(fp);
    printf("%s\n", buf2);// 打印buf2

    return 0;
}

编译后运行:

$ ./format_io
Hello!

查看hello.txt中的内容:

$ cat hello.txt
Hello! Linux C

可以看到buf中的字符串已写入到hello.txt(该文件原来有内容,现在被清除了)。另外,打印的buf2的结果只是一部分,原因是scanf读取文件流时,遇到空格符、制表符和回车符时会自动终止读取字符。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。