【Linux/C++文件篇(一) 】标准I/O与文件I/O基础 :一文掌握文件操作的核心

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页:YYYing.

⭐️Linux/C++进阶 系列专栏:【从零开始的linux/c++进阶编程】

⭐️ 其他专栏:【linux基础】【数据结构与算法】【从零开始的计算机网络学习】

系列上期内容:【Linux/C++进阶篇(二) 】自动化构建:Makefile/CMake

系列下期内容:暂无


目录

前言:

标准I/O

一、标准I/O的实现原理

二、FILE结构体

[🎯 原型](#🎯 原型)

[🔍 特殊的FILE指针](#🔍 特殊的FILE指针)

三、常用的标准I/O函数

[📖 打开与关闭文件:fopen/fclose](#📖 打开与关闭文件:fopen/fclose)

[📖 关于错误码的问题](#📖 关于错误码的问题)

[📖 单字符读写:fputc/fgetc](#📖 单字符读写:fputc/fgetc)

[📖 字符串读写:fputs/fgets](#📖 字符串读写:fputs/fgets)

[📖 关于标准IO的缓冲区问题](#📖 关于标准IO的缓冲区问题)

缓存区刷新函数:fflush

行缓存

全缓存

不缓存

三种缓存区的大小验证

[📖 格式化读写:fprintf/fscanf](#📖 格式化读写:fprintf/fscanf)

[📖 格式串转字符串存入字符数组中:sprintf/snprintf](#📖 格式串转字符串存入字符数组中:sprintf/snprintf)

[📖 模块化读写:fread/fwrite](#📖 模块化读写:fread/fwrite)

[📖 关于文件内光标:fseek/ftell/rewind](#📖 关于文件内光标:fseek/ftell/rewind)

文件I/O

一、文件描述符

二、常用的文件I/O函数

[📖 打开与关闭文件:open/close](#📖 打开与关闭文件:open/close)

[📖 读写文件:read/write](#📖 读写文件:read/write)

[📖 关于光标的操作:lseek](#📖 关于光标的操作:lseek)

标准I/O与文件I/O的对比

何时使用标准I/O?

何时使用文件I/O?

结语

---⭐️封面自取⭐️---



前言:

为什么需要两种I/O?

在C/C++语言的世界里,输入输出(I/O)操作是我们与外部世界沟通的桥梁。但是你可能不知道,C/C++语言提供了两套完全不同的I/O系统

  • 标准I/O:高级、可移植、带缓冲的接口,使用系统提供的库函数实现

  • 文件I/O:低级、直接、无缓冲的系统调用,每进行一次系统调用,进程会从用户空间向内核空间进行一次切换, 当用户空间与内核空间进行切换时,进程就会进入一次挂起状态,从而导致进程执行效率低

  • 标准I/O与文件I/O的区别:标准IO相比于文件IO而言,提供了缓冲区,用户可以将数据先放入缓冲区 中,等到缓冲区时机到了后,统一进行一次系统调用,将数据刷入内核空间

那为什么要有两套呢?它们各有什么优缺点?什么时候该用哪一种?今天,我们将彻底揭开它们的面纱。

(本文凡涉及到各函数API均参考linux内核man手册,翻译如有不适请指出,感激不尽)

标准I/O

一、标准I/O的实现原理

标准I/O的核心思想是 。流是一个抽象的概念,表示数据的流动,可以是文件、终端、网络等。**那我们现在设想我们的程序怎么去对一个文件进行操作呢?**是不是会用到一些接口函数,但程序也不知道我们文件到底在哪,或者说是哪个文件呢?那我们看下图:


二、FILE结构体

FILE结构体是系统提供的用于描述一个文件全部信息的结构体

🎯 原型

cpp 复制代码
struct FILE{
    char * _IO_buf_base;        //缓冲区的起始地址
    char * _IO_buf_end;         //缓冲区终止地址
    int _fileno;                //文件描述符,用于进行系统调用
};

🔍 特殊的FILE指针

|------------|--------|
| stderr | 标准出错指针 |
| stdin | 标准输入指针 |
| stdout | 标准输出指针 |

这三个指针,全部都是针对于终端文件而言的,当程序启动后,系统默认打开的三个特殊文件指针


三、常用的标准I/O函数

📖 打开与关闭文件:fopen/fclose

|----------|-------------------------------------------------------------|----------------------------|
| 函数原型 | FILE *fopen(const char *path, const char *mode); | int fclose(FILE *fp); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 打开指定的文件 | 关闭指定的文件 |
| 参数说明 | 参数1:表示文件路径,是一个字符串 参数2:打开文件的方式,也是一个字符串,字符串必须以以下的字符开头 | 要关闭的文件指针(由fopen返回的结果) |
| 返回值 | 成功返回打开的文件指针,失败返回NULL并置位错误码 | 成功执行返回0,失败返回EOF并置位错误码 |

cpp 复制代码
#include"stdio.h"        //标准的输入输出头文件

int main(int argc, const char *argv[]){
    //1、定义一个文件指针
    FILE *fp = NULL;
    //以只读的形式打开一个不存在的文件,并将返回结果存入到fp指针中
    //fp = fopen("./file.txt", "r");    
    //此时会报错,原因是以只读的形式打开一个不存在的文件,是不允许的
    //以只写的形式打开一个不存在的文件,如果文件不存在就创建一个空的文件,如果文件存在就清空
    fp = fopen("./file.txt", "w");           
    if(fp == NULL){
        printf("fopen error\n");
        return -1;
    }
    printf("fopen success\n");
    //2、关闭文件
    fclose(fp);
    return 0;
}

文件模式总结:

|--------|-----------------------------------------------------------------|
| 模式 | 功能 |
| r | 以只读 的形式打开文件,文件光标定位在开头部分 |
| r+ | 以读写 的形式打开文件,文件光标定位在开头部分 |
| w | 以只写 的形式打开文件,如果文件不存在,就创建文件,如果文件存在,就清空,光标定位在开头 |
| w+ | 以读写 的形式打开文件,如果文件不存在,就创建文件,如果文件存在,就清空, 光标定位在开头 |
| a | 以追加(结尾写) 的形式打开文件,如果文件不存在就创建文件,光标定位在文件末尾 |
| a+ | 以读写 的形式打开文件,如果文件不存在就创建文件,如果文件存在,读取操作光 标定位在开头,写操作光标定位在结尾 |


📖 关于错误码的问题

概念:

内核提供的函数出错后,内核空间会向用户空间反馈一个错误信息,由于错误信息比较多 也比较复杂,系统就给每种不同的错误信息起了一个编号,用一个整数表示,这个整数就是错误码

关于错误码的处理函数:

|----------|----------------------------------|--------------------------------------------|
| 函数原型 | char *strerror(int errnum); | void perror(const char *s); |
| 头文件 | string.h,错误码所在头文件是errno.h | stdio.h |
| 功能 | 将错误码转换为错误信息描述 | 输出当前错误码对应的错误信息 |
| 参数说明 | 错误码 | 提示符号,会原样打印出来,并且会在提示数据后面加上冒号,并输出完后,自动换行 |
| 返回值 | 错误信息字符串描述 | 无返回值 |

演示代码如下:

cpp 复制代码
#include<stdio.h>        //标准的输入输出头文件
#include<errno.h>        //错误码所在的头文件
#include<string.h>       //字符串处理的头文件

int main(int argc, const char *argv[]){
    //1、定义一个文件指针
    FILE *fp = NULL;

    fp = fopen("./file.txt", "r");           
    if(fp == NULL){
        //打印错误信息,并输出错误码 2
        printf("fopen error: %d, errmsg:%s\n", errno, strerror(errno));
  
        //打印当前错误码对应的错误信息
        perror("fopen error");      
        return -1;
    }
    printf("fopen success\n");
    //2、关闭文件
    fclose(fp);
}

📖 单字符读写:fputc/fgetc

|----------|-------------------------------------|--------------------------------------|
| 函数原型 | int fgetc(FILE *stream); | int fputc(int c, FILE *stream); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 从指定文件中读取一个字符数据,并以无符号整数的形式返回 | 将指定的字符c写入到stream指向的文件中 |
| 参数说明 | 文件指针 | 文件指针 |
| 返回值 | 成功返回读取的字符对应的无符号整数,失败返回EOF并置位错误码 | 成功返回写入字符对的无符号整数,失败返回EOF并置位错误码 |

我们现在用fgetc和fputc完成两个文件的拷贝工作,实现指令cp的功能: cp srcfile destfile

cpp 复制代码
#include<stdio.h>
int main(int argc, const char *argv[]){
    //判断外部传参的个数是否为 3
    if(argc != 3){
        printf("input file error\n");
        printf("usage:./a.out srcfile destfile\n");
        return -1;
    }

    //以只读的形式打开源文件,以只写的形式打开目标文件
    FILE *srcfp = NULL;    //源文件文件指针
    FILE *destfp = NULL;   //目标文件文件指针
    if((srcfp = fopen(argv[1], "r")) ==NULL){
        perror("srcfile open error");
        return -1;
    }
    if((destfp = fopen(argv[2], "w")) ==NULL){
        perror("destfile open error");
        return -1;
    }

    //将源文件中的内容搬运到目标文件中
    char ch = 0;        //搬运工
    while(1){
        ch = fgetc(srcfp);      //从源文件中读取一个字符
        if(ch == EOF){
            break;              //文件全部读取结束
        }
        fputc(ch, destfp);        //将读取的字符写入到目标文件中
    }
    //关闭两个文件
    fclose(srcfp);
    fclose(destfp);
    printf("拷贝成功\n");
 
    return 0;
}

📖 字符串读写:fputs/fgets

此处我相信也是部分初学者一开始从终端输入字符串的方法。

|----------|------------------------------------------------------------------------------------|-----------------------------------------------|
| 函数原型 | char *fgets(char *s, int size, FILE *stream); | int fputs(const char *s, FILE *stream); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 从stream指向的文件中最多读取size-1个字符到s容器中,遇到回车或文件结束,会结束一 次读取,并且会将回车放入容器,最后自动加上一个字符串结束标识'\0' | 将指定的字符串,写入到指定的文件中 |
| 参数说明 | 参数1:字符数组容器起始地址 参数2:要读取的字符个数,最多读取size-1个字符 参数3:文件指针 | 参数1:要被写入的字符串 参数2:文件指针 |
| 返回值 | 成功返回容器s的起始地址,失败返回NULL | 成功返回本次写入字符的个数,失败返回EOF |

我们现在再用fputs与fgets完成两个文件的拷贝

cpp 复制代码
#include<iostream>
#include<stdio.h>
#include<string.h>

int main(int argc, const char *argv[]){
    //判断外部是否传入两个文件
    if(argc != 3){
        printf("inut file error\n");
        printf("usage:./a.out srcfile destfile\n");
        return -1;
    }
    //以只读的形式打开源文件,以只写的形式打开目标文件
    FILE *srcfp = NULL;
    FILE *destfp = NULL;
    if((srcfp = fopen(argv[1], "r")) == NULL){
        perror("fopen error");
        return -1;
    }
    if((destfp = fopen(argv[2], "w")) == NULL){
        perror("fopen error");
        return -1;
    }
    //定义搬运工
    char buf[128] = "";
    while(1){
        char *ptr = fgets(buf, sizeof(buf), srcfp);     //将将数据从源文件中读取下来
        if(ptr == NULL){
            break;                 //读取结束
        }
        fputs(buf, destfp);         //将从源文件中读取的数据写入到目标文件中
    }
    //关闭文件
    fclose(srcfp);
    fclose(destfp);
    printf("拷贝成功\n");
    return 0;
}

📖 关于标准IO的缓冲区问题

缓存区刷新函数:fflush

|----------|--------------------------------|
| 函数原型 | int fflush(FILE *stream); |
| 头文件 | stdio.h |
| 功能 | 刷新给定的文件指针对应的缓冲区 |
| 参数说明 | 文件指针 |
| 返回值 | 成功返回0,失败返回EOF并置位错误码 |


行缓存

和终端文件相关的缓冲区叫做行缓存,行缓冲区的大小为1024字节,对应的文件指针:stdin、stdout

行缓存的刷新时机:

cpp 复制代码
#include<iostream>
#include<stdio.h>

int main(int argc, const char *argv[]){
    //1、验证缓冲区如果没有达到刷新时机,就不会将数据进行刷新
    printf("hello world");           
    while(1);        //阻塞程序不让进程结束
    //在终端上打印输出一个hello world,没有到缓冲区的刷新时机,就不会输出数据
    
    //2、当程序结束后,会刷新行缓冲区
    printf("hello world");

    //3、当遇到换行时,会刷新行缓存
    printf("hello world\n");
    while(1);

    //4、当输入输出发生切换时,也会刷新行缓存
    int num = 0;
    printf("请输入>>>");        //向标准输出缓冲区中写入一组数据,没有换行符号
    scanf("%d", &num);            

    //5、当关闭行缓存对应的文件指针时,也会刷新行缓存
    printf("hello world");        //向标准输出缓冲区中写入数据,没有换行
    fclose(stdout);         //关闭标准输出指针
    while(1);

    //6、使用fflush函数手动刷新缓冲区时,行缓存会被刷新
    printf("hello world");
    fflush(stdout);
    while(1);

    //7、当缓冲区满了后,会刷新行缓存,行缓存大小:1024字节
    for(int i=0; i<1025; i++){
        printf("A");
    }
    while(1);      
    //防止程序结束
    
    return 0;
}

全缓存

和外界文件相关的缓冲区叫做全缓存,全缓冲区的大小为4096字节,对应的文件指针:fp

全缓存的刷新时机:

cpp 复制代码
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[]){
    //打开一个文件
    FILE *fp = NULL;
    if((fp = fopen("./aa.txt", "r+")) == NULL){        //以读写的形式打开文件
        perror("fopen error");
        return -1;
    }

    //1、当缓冲区刷新时机未到时,不会刷新全缓存
    fputs("hello world", fp);                //将字符串写入到文件中
    while(1);
    
    //2、当遇到换行时,也不会刷新全缓存
    fputs("hello world\n", fp);                //将字符串写入到文件中
    while(1);

    //3、当程序结束后,会刷新全缓存
    fputs("hello world 你好 星球!\n", fp);   

    //4、当输入输出发生切换时,会刷新全缓存
    fputs("I love China\n", fp);      //向文件中输出一个字符串
    fgetc(fp);               //从文件中读取一个字符,主要是让输入输出发生切换
    while(1);

    //5、当关闭缓冲区对应的文件指针时,也会刷新全缓存
    fputs("上海欢迎你", fp);
    fclose(fp);            //刷新文件指针
    while(1);

    //6、当手动刷新缓冲区对应的文件指针时,也会刷新全缓存
    fputs("上海欢迎你", fp);
    fflush(fp);            //刷新文件指针
    while(1);
    
    //7、当缓冲区满了后,再向缓冲区中存放数据时会刷新全缓存
    for(int i=0; i<4097; i++){
        fputc('A', fp);        //循环将单字符放入文件中
    }
    while(1);
    return 0;
}

不缓存

和标准出错相关的缓冲区叫不缓存,不缓存的大小为0字节,对应的文件指针:stderr

不缓存的刷新时机:只要放入数据,立马进行刷新

cpp 复制代码
#include<iostream>
#include<stdio.h>
using namespace std;
int main(int argc, const char *argv[]){
    //perror("a");        //向标准出错中放入数据

    fputs("A", stderr);          //向标准出错缓冲区中写入一个字符A

    while(1);          //阻塞

    return 0;
}

三种缓存区的大小验证
cpp 复制代码
#include<iostream>
#include<stdio.h>

int main(int argc, const char *argv[]){
    //如果缓冲区没有被使用时,求出大小为0,只有被至少使用了一次后,缓冲区的大小就被分配了
    printf("行缓存的大小为:%d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);   
    //行缓存的大小 1024
    printf("行缓存的大小为:%d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);     


    printf("行缓存的大小为:%d\n", stdin->_IO_buf_end - stdin->_IO_buf_base);     //0
    //大小为0  因为没有使用标准输入


    int num = 0;
    std::cin >> num;
    //使用了标准的输入缓冲区
    printf("行缓存的大小为:%d\n", stdin->_IO_buf_end - stdin->_IO_buf_base);
    //1024

    //验证全缓存的大小为4096
    FILE *fp = NULL;
    if((fp = fopen("./aa.txt", "r")) == NULL){
        perror("fopen error");
        return -1;
    }

    //未使用全缓存时,大小为0
    printf("全缓存的大小为:%d\n", fp->_IO_buf_end - fp->_IO_buf_base);         //0

    //使用一次全缓存
    fgetc(fp);      //从文件中读取一个字符
    printf("全缓存的大小为:%d\n", fp->_IO_buf_end - fp->_IO_buf_base);         //4096

    //关闭文件
    fclose(fp);


    //验证不缓存的大小为0
    //未使用不缓存时大小为0
    printf("不缓存的大小为:%d\n", stderr->_IO_buf_end - stderr->_IO_buf_base);   //0、

    //使用一次不缓存
    perror("error");
    printf("不缓存的大小为:%d\n", stderr->_IO_buf_end - stderr->_IO_buf_base);   //0

    return 0;
}

下图为效果图:


📖 格式化读写:fprintf/fscanf

|----------|----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| 函数原型 | int fprintf(FILE *stream, const char *format, ...); | int fscanf(FILE *stream, const char *format, ...); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 向指定的文件中输出一个格式串 | 从指定的文件中以指定的格式读取数据,放入程序中 |
| 参数说明 | 参数1:文件指针 参数2:格式串,可以包含格式控制符,%d(整数)、%s(字符串)、%f(小数)、%lf(小数) 参数3:可变参数,输出项列表,参数个数由参数2中的格式控制符的个数决定 | 参数1:文件指针 参数2:格式串,可以包含格式控制符,%d(整数)、%s(字符串)、%f(小数)、%lf(小数) 参数3:可变参数,输入项地址列表,参数个数由参数2中的格式控制符的个数决定 |
| 返回值 | 成功返回输出的字符个数,失败返回一个负数 | 成功返回读入的项数,失败返回EOF并置位错误码 |

使用fprintf与fscanf共同实现登录注册功能

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

int do_register(){
    char reg_name[20] = "";
    char reg_pwd[20] = "";
    char name[20] = "";
    char pwd[20] = "";
    printf("请输入注册账号: ");
    fgets(reg_name, sizeof(reg_name), stdin);
    reg_name[strlen(reg_name) - 1] = '\0';
    printf("请输入密码: ");
    fgets(reg_pwd, sizeof(reg_pwd), stdin);
    reg_pwd[strlen(reg_pwd) - 1] = '\0';
    FILE* rfp = NULL;
    if((rfp = fopen("./usr.txt", "a+")) == NULL){
        perror("rfp error ");
        return -1;
    }
    while(1){
        int res = fscanf(rfp, "%s %s", name, pwd);
        if(res == EOF){
            break;
        }
        if(strcmp(name, reg_name) == 0 
        && strcmp(pwd, reg_pwd) == 0){
            fclose(rfp);
            return 2;
        }
    }
    fprintf(rfp, "%s %s\n", reg_name, reg_pwd);
    fclose(rfp);
    printf("注册成功");
    return 1;
}

int do_login(){
    char log_name[20] = "";
    char log_pwd[20] = "";
    char name[20] = "";
    char pwd[20] = "";
    printf("请输入账号: ");
    fgets(log_name, sizeof(log_name), stdin);
    log_name[strlen(log_name)-1] = '\0';      //将字符串最后的换行符换成结束符号
    printf("请输入登录密码: ");
    fgets(log_pwd, sizeof(log_pwd), stdin);
    log_pwd[strlen(log_pwd)-1] = '\0';

    FILE* lfp = NULL;
    if((lfp = fopen("./usr.txt", "r")) == NULL){
        perror("lfp error");
        return -1;  
    }
    while(1){
        int res = fscanf(lfp, "%s %s", name, pwd);
        if(res == EOF){
            break;
        }
        if(strcmp(name, log_name) == 0 
        && strcmp(pwd, log_pwd) == 0){
            printf("登录成功");
            fclose(lfp);
            return 1;
        }
    }
    fclose(lfp);
    printf("登录失败,请重新登录");
    return 0;
}

int main(int argc, const char *argv[]){
    int menu = 0;
    while(1){
        system("clear"); // 创建子进程调用终端指令
        
        printf("\t\t======登录界面=========\n");
        printf("\t\t======1、注册==========\n");
        printf("\t\t======2、登录==========\n");
        printf("\t\t======0、退出==========\n");
        printf("请输入功能选项:");
        scanf("%d", &menu);
        getchar();
        switch (menu) {
        case 1:{
            int res = do_register();
            if(res == 2){
                printf("此账号已注册过,请重新选择登录界面\n");
            }
        break;
        }
        case 2:{
            int res = do_login();
            if(res == 1){
                printf("登录成功后的界面\n");
                //此处省略一万行程序代码
            }
        }
        break;
        case 0: exit(EXIT_SUCCESS);// 退出进程
        default:
            printf("输入的数字有误,重新检查\n");
        }
        printf("请输入任意键按回车清屏\n");
        while(getchar() != '\n');
    }
    return 0;
}

📖 格式串转字符串存入字符数组中:sprintf/snprintf

我们目前所学的函数中的printf是向终端打印一个格式串,而fprintf是向外部文件中打印一个格式但有时候,想要将多个不同数据类型的数据,组成一个字符串放入字符数组中,此时我们就可以使用 sprintf 或 snprintf

|----------|--------------------------------------------------------|-----------------------------------------------------------------------------------|
| 函数原型 | int sprintf(char *str, const char *format, ...); | int snprintf(char *str, size_t size, const char *format, ...); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 将指定的格式串转换为字符串,放入字符数组中 | 将格式串中最多size-1个字符转换为字符串,存放到字符数组中 |
| 参数说明 | 参数1:字符数组的起始地址 参数2:格式串,可以包含多个格式控制符 参数3:可变参数 | 参数1:字符数组的起始地址 参数2:要转换的字符个数,最多为size-1 参数3:格式串,可以包含多个格式控制符 参数4:可变参数 |
| 返回值 | 成功返回转换的字符个数,失败返回EOF | 如果转换的字符小于size时,返回值就是成功转换字符的个数,如果大于size则只转换 size-1个字符,返回值就是size,失败返回EOF |

看功能我们也能明白,对于前者函数而言,用一个小的容器去存储一个大的转换后的字符时,会出现指针越界的段错误,为 了安全起见引入了snprintf。

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>

int main(int argc, const char *argv[]){
    char buf[128] = "";
    // 将多种数据类型的数据连接成字符串放入字符数组中
    // sprintf(buf, 128, "%s %d %lf", "张三", 1001, 99.5);

    //安全起见,我们使用snprintf
    snprintf(buf, sizeof(buf), "%s %d %lf", "张三", 1001, 99.5);

    // 输出转换后的字符串
    printf("buf = %s\n", buf);

    return 0;
}

📖 模块化读写:fread/fwrite

|----------|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| 函数原型 | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); |
| 头文件 | stdio.h | stdio.h |
| 功能 | 从stream指向的文件中,读取nmemb项数据,每一项的大小为size,将整个结果放入ptr指向的容器中 | 向stream指向的文件中写入nmemb项数据,每一项的大小为size,数据的起始地址为ptr |
| 参数说明 | 参数1:容器指针,是一个void*类型,表示可以存储任意类型的数据 参数2:要读取数据每一项的大小 参数3:要读取数据的项数 参数4:文件指针 | 参数1:要写入数据的起始地址 参数2:每一项的大小 参数3:要写入的总项数 参数4:文件指针 |
| 返回值 | 成功返回nmemb,就是成功读取的项数,失败返回小于项数的值,或者是0 | 成功返回写入的项数,失败返回小于项数的值,或者是0 |

结构体的读写:

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
//定义一个学生信息
class Stu
{
public:
char name[20];        
int age;             
double score;          
};
int main(int argc, const char *argv[]){
    //定义文件指针,以只写的形式打开文件
    FILE *fp = NULL;
    if((fp = fopen("./test.txt", "w")) == NULL){
        perror("fopen error");
        return -1;
    }
    //定义三个学生
    Stu s[3] = {{"张三",18, 98}, \
                {"李四",20, 88}, \
                {"王五",16, 95}};
    //将三个学生信息写入文件中
    fwrite(s, sizeof(Stu), 3, fp); 
    //关闭文件
    fclose(fp);
    //再次以只读的形式打开文件
    if((fp = fopen("./test.txt", "r")) == NULL){
        perror("fopen error");
        return -1;
    }
    //定义一个对象,接收读取的结果
    Stu temp;
    //从文件中读取一个学生的信息
    fread(&temp, sizeof(Stu), 1, fp);
    //将读取的数据展示出来
    printf("name:%s, age:%d, score:%.2lf\n", temp.name, temp.age, temp.score);
    //关闭文件
    fclose(fp);
    
    return 0;
}

📖 关于文件内光标:fseek/ftell/rewind

|--------------------------------------------------------|---------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| 函数原型 | 头文件 | 功能 | 参数说明 | 返回值 |
| int fseek(FILE *stream, long offset, int whence); | stdio.h | 移动文件光标位置,将光标从指定位置处进行前后偏移 | 参数1:文件指针 参数2:偏移量 >0 :表示从指定位置向后偏移n个字节 <0:表示从指定位置向前偏移n个字节 =0:在指定位置处不偏移 参数3:偏移的起始位置 SEEK_SET:文件起始位置 SEEK_CUR:文件指针当前位置 SEEK_END:文件结束位置 | 成功返回0,失败返回-1并置位错误码 |
| long ftell(FILE *stream); | stdio.h | 获取文件指针当前的偏移量 | 文件指针 | 成功返回文件指针所在的位置,失败返回-1并置位错误码 |
| void rewind(FILE *stream); | stdio.h | 将文件光标定位在开头:fseek(fp, 0, SEEK_SET); | 文件指针 | 无返回值 |

代码演示:

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>

//定义一个学生信息
class Stu
{
public:
char name[20];        
int age;             
double score;          
};
int main(int argc, const char *argv[]){
    //定义文件指针,以只写的形式打开文件
    FILE *fp = NULL;
    if((fp = fopen("./test.txt", "w+")) == NULL){
        perror("fopen error");
        return -1;
    }
    //定义三个学生
    Stu s[3] = {{"张三",18, 98}, \
    {"李四",20, 88}, \
    {"王五",16, 95}};
    //将三个学生信息写入文件中
    fwrite(s, sizeof(Stu), 3, fp); 
    //求出文件的大小
    printf("此时文件的大小为:%ld\n", ftell(fp));         
    //将光标移动到开头位置
    //fseek(fp, 0, SEEK_SET);
    //将光标直接定位到第二个学生信息前,但是此时光标在最后
    //fseek(fp, sizeof(Stu), SEEK_SET);       
    //文件大小
    //将光标从开头后移一个学生空间的内容
    fseek(fp, -sizeof(Stu)*2,SEEK_CUR);    
    //定义一个对象,接收读取的结果
    Stu temp;
    //从文件中读取一个学生的信息
    fread(&temp, sizeof(Stu), 1, fp);    
    //将光标从当前位置向前偏移两个学生空间的内容
    //将读取的数据展示出来
    printf("name:%s, age:%d, score:%.2lf\n", temp.name, temp.age, temp.score);
    //关闭文件
    fclose(fp);
    
    return 0;
}

文件I/O

就是通过系统调用(内核提供的函数)实现,只要我们使用文件IO接口,那么进程就会从用户空间向内核空间进行一次切换。标准IO的实现中,也是在内部调用了文件IO操作。该操作效率较低,因为没有缓冲区的概念。文件IO常用的操作:open、close、read、write、lseek

当然,我们文件I/O也得用一个标识去指向我们指定文件,此处我们指定文件的标识就直接是我们标准I/O的FILE结构体里的文件描述符了。

一、文件描述符

🥇 文件描述符的本质是一个大于等于0的整数,在使用open函数打开文件时,就会产生一个用于操作文件的句柄,这就是文件描述符

🥈 在一个进程中,能够打开的文件描述符是有限制的,一般是1024个,[0,1023],可以通过指令 ulimit -a进行查看,如果要更改这个限制,可以通过指令 ulimit -n 数字进行更改

🥉 文件描述符的使用原则一般是最小未分配原则

🏅 特殊的文件描述符:0、1、2,这三个文件描述符在一个进程启动时就默认被打开了,分别表示标准输入、标准输出、标准错误

cpp 复制代码
#include<iostream>
#include<stdio.h>

int main(int argc, const char *argv[]){
    //分别输出标准输入、标准输出、标准出错文件指针对应的文件描述符
    printf("stdin->_fileno = %d\n", stdin->_fileno);       // 0
    printf("stdout->_fileno = %d\n", stdout->_fileno);     // 1
    printf("stderr->_fileno = %d\n", stderr->_fileno);     // 2

    return 0;
}

二、常用的文件I/O函数

📖 打开与关闭文件:open/close

|----------|------------------------------------------------------------------------------------------------------|------------------------|
| 函数原型 | int open(const char *pathname, int flags, mode_t mode); | int close(int fd); |
| 头文件 | sys/types.h sys/stat.h fcntl.h | unistd.h |
| 功能 | 打开或创建一个文件,并返回该文件的文件描述符,返回文件描述符的规则是最小未分配原则 | 关闭文件描述符对应的文件 |
| 参数说明 | 参数1:要打开的文件文件路径 参数2:打开模式 参数3:如果参数2中的flags中有O_CREAT时,表示创建新文件,参数3就必须给定,表示新创建的 文件的权限 | 文件描述符 |
| 返回值 | 成功返回打开文件的文件描述符,失败返回-1并置位错误码 | 成功返回0,失败返回-1并置位错误码 |

值得一提的是,这块的参数2与参数3其实是很有说法的:

在参数2中以下三种方式必须选其一:

O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)

这三种打开方式同时可以有零个或多个,跟上述的方式一起用位或运算连接到一起:

🥇**O_CREAT:**用于创建文件,如果文件不存在,则创建文件,如果文件存在,则打开文件,如果 flag中包含了该模式,则函数的第三个参数必须要加上

🥈**O_APPEND:**以追加的形式打开文件,光标定位在结尾

🥉**O_TRUNC:**清空文件

🏅O_EXCL: 常跟O_CREAT一起使用,确保本次要创建一个新文件,如果文件已经存在,则open 函数报错

下图就是对应着我们之前fopen中模式的选择而搭配出来的flags


而对于参数 3 如果当前参数给定了创建的文件权限,最终的结果也不一定是参数3的值,因为系统会用你给定的参数3的值,与系统的umask取反的值进行位与运算后,才是最终创建文件的权限(mode & ~umask)

当前终端的umask的值,可以通过指令 umask 来查看,一般默认为为 0022,表示当前进程所在的组中对该文件没有写权限,其他用户对该文件也没有写权限

当前终端的umask的值是可以更改的,通过**指令:"umask 数字"**进行更改,这种方式只对 当前终端有效

普通文件的权限一般为:0644,表示当前用户没有可执行权限,当前组中其他用户和其 他组中的用户都只有读权限

目录文件的权限一般为:0755,表示当前用户具有可读、可写、可执行,当前组中其他 用户和其他组中的用户都没有可写权限

代码演示:

cpp 复制代码
#include<iostream>
#include<cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, const char *argv[]){
    //1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
    int fd = -1;
    //以只读的形式创建文件,如果文件不存在则创建文件
    //如果创建文件时没有给权限,则该文件的权限是随机权限
    //如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
    if((fd = open("./tt.txt", O_WRONLY|O_CREAT, 0644)) == -1)
    {
    perror("open error");
    return -1;
    }
    printf("open success fd = %d\n", fd);    //3,由于0、1、2已经被使用,所以该数为3

    //关闭文件
    close(fd);
    return 0;
}

📖 读写文件:read/write

|----------|---------------------------------------------------------|------------------------------------------------------------|
| 函数原型 | ssize_t read(int fd, void *buf, size_t count); | ssize_t write(int fd, const void *buf, size_t count); |
| 头文件 | unistd.h | unistd.h |
| 功能 | 从fd文件描述符引用的文件中读取count的字符放入buf对应的容器中 | 将buf容器中的count个数据,写入到fd引用的文件中 |
| 参数说明 | 参数1:已经打开文件对应的文件描述符 参数2:容器的起始地址 参数3:要读取的字符个数 | 参数1:已经打开的文件的文件描述符 参数2:要写入的数据的起始地址 参数3:要写入数据的个数 |
| 返回值 | 成功返回读取的字符个数,这个个数可能会小于count的值,失败返回-1并置位错误码 | 成功返回写入的字符个数,这个个数可能小于count,失败返回-1并置位错误码 |

cpp 复制代码
#include<iostream>
#include<cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main(int argc, const char *argv[]){
    //1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
    int fd = -1;
    //以只读的形式创建文件,如果文件不存在则创建文件
    //如果创建文件时没有给权限,则该文件的权限是随机权限
    //如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
    if((fd = open("./tt.txt", O_WRONLY|O_CREAT, 0644)) == -1){
        perror("open error");
        return -1;
    }
    printf("open success fd = %d\n", fd);    //由于0、1、2已经被使用,所以该数为3
    //对数据进行读写操作
    char wbuf[128] = "hello world";
    //将上述字符串写入文件中
    write(fd, wbuf, strlen(wbuf));
    //关闭文件
    close(fd);         
    //关闭fd引用的文件

    //再次以只读的形式打开文件,此时参数2中不需要使用O_CREAT,那么第三个参数也不需要了
    if((fd = open("./tt.txt", O_RDONLY)) == -1){
        perror("open error");
        return -1;
    }
    printf("open success fd = %d\n", fd);       // 3
    //定义接收数据容器
    char rbuf[5] = "";
    int res = read(fd, rbuf, sizeof(rbuf));      //从文件中读取数据放入rbuf中
    write(1, rbuf, res);      //向1号文件描述符中写入数据,之前读取多少,现在写入多少


    //关闭文件
    close(fd);
    return 0;
}

📖 关于光标的操作:lseek

|----------|------------------------------------------------------------------------------------------------------------|
| 函数原型 | off_t lseek(int fd, off_t offset, int whence); |
| 头文件 | sys/types.h unistd.h |
| 功能 | 移动光标的位置,并返回光标现在所在的位置 |
| 参数说明 | 参数1:文件描述符 参数2:偏移量 >0:表示从指定位置向后偏移n个字节 <0:表示从指定位置向前偏移n个字节 =0:在指定位置处不偏移 参数3:偏移的起始位置 |
| 返回值 | 光标现在所在的位置 |

注意:lseek的效果 = fseek + ftell

cpp 复制代码
#include<iostream>
#include<cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, const char *argv[]){
    //1、定义文件描述符,对于文件IO而言,句柄就是文件描述符
    int fd = -1;
    //以读写的形式创建文件,如果文件不存在则创建文件,如果文件存在则清空文件
    //如果创建文件时没有给权限,则该文件的权限是随机权限
    //如果创建文件时,给定了文件的权限,则文件最终的权限是 给定的 mode&~umask
    if((fd = open("./tt.txt", O_RDWR|O_CREAT|O_TRUNC, 0644)) == -1){
        perror("open error");
        return -1;
    }
    printf("open success fd = %d\n", fd);          //3,由于0、1、2已经被使用,所以该数为3
    //对数据进行读写操作
    char wbuf[128] = "hello world";
    //将上述字符串写入文件中
    write(fd, wbuf, strlen(wbuf));
    //此时文件光标是在文件的末尾位置
    
    //需求是:读取文件中的 world
    //lseek(fd, 6, SEEK_SET);            //从文件开头位置向后偏移6个字节
    lseek(fd, -5, SEEK_END);          //从文件结束位置向前偏移5个字节
    //定义接收数据容器
    char rbuf[5] = "";
    int res = read(fd, rbuf, sizeof(rbuf));      //从文件中读取数据放入rbuf中
    write(1, rbuf, res);      //向1号文件描述符中写入数据,之前读取多少,现在写入多少
    //关闭文件
    close(fd);
    return 0;
}

标准I/O与文件I/O的对比

|------------|----------------------|------------------------|
| 特性 | 标准I/O | 文件I/O |
| 抽象级别 | 高(流) | 低(文件描述符) |
| 缓冲 | 自动缓冲 | 无缓冲(可手动设置) |
| 可移植性 | 高(ANSI C标准) | 低(POSIX标准,Unix-like系统) |
| 性能 | 小数据量好(缓冲减少系统调用) | 大数据量或特殊场景好 |
| 错误处理 | 通过返回值+errno | 通过返回值+errno |
| 文件定位 | fseek, ftell, rewind | lseek |
| 格式化I/O | 支持(printf, scanf系列) | 不支持 |
| 二进制I/O | fread/fwrite | read/write |


何时使用标准I/O?

  1. 常规文件操作:大多数情况下,标准I/O是更好的选择

  2. 需要格式化输入输出:printf/scanf系列函数非常方便

  3. 可移植性要求高:需要在不同平台运行

  4. 小量频繁的I/O:缓冲机制减少系统调用开销

  5. 简单的文本处理:行缓冲模式对文本文件友好


何时使用文件I/O?

  1. 需要底层控制:如设置文件描述符标志

  2. 特殊文件类型:设备文件、管道、socket

  3. 需要文件锁定:flock或fcntl锁定

  4. 需要获取文件元数据(状态):fstat

  5. 高性能场景:大数据量、直接I/O、内存映射

文件锁定与获取文件状态的API感兴趣的可以继续参照man手册玩玩,此处就不进行过多赘述了**。**


结语

那么关于IO基础部分的讲解就到这里了。

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

**无限进步,**我们下次再见。


---⭐️ 封面自取 ⭐️---

相关推荐
寻寻觅觅☆6 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio6 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
fpcc6 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
小白同学_C6 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖7 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
2601_949146537 小时前
Shell语音通知接口使用指南:运维自动化中的语音告警集成方案
运维·自动化
儒雅的晴天7 小时前
大模型幻觉问题
运维·服务器
ceclar1237 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS8 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
通信大师8 小时前
深度解析PCC策略计费控制:核心网产品与应用价值
运维·服务器·网络·5g