16.番外_模拟c语言文件流

news2025/2/24 9:02:09

16.番外:模拟c语言文件流

一,前言

先来看一下Struct _IO_FILE的定义。

grep -rnw '/usr/include/' -e 'struct _IO_FILE'

image

image

今天我们要做的就是模拟实现FILEC语言文件操作相关函数

二,开始!做吧!我们!

1.FILE的缓冲区及冲刷方式

大小设置为1024刷新方式选择行缓冲

为了方便对缓冲区进行控制,需要下标_current,最重要的描述符_fd

#define BUFFER_SIZE 1024   //缓冲区大小

//通过位图的方式,控制刷新方式
#define BUFFER_NONE 0x1    //无缓冲
#define BUFFER_LINE 0x2    //行缓冲
#define BUFFER_ALL 0x4     //全缓冲

typedef struct MY_FILE
{
    char _buffer[BUFFER_SIZE];  //缓冲区
    size_t _current;    //缓冲区下标
    int _flush;  //刷新方式,位图结构

    int _fd;    //文件描述符
}MY_FILE;

2.函数需求分析

1.**fopen** 打开文件

以多种方式打开,若是以读方式打开时,文件不存在会报错

2.**fclose** 关闭文件

根据 FILE* 关闭指定文件,不能重复关闭

3.**fflush** 进行缓冲区刷新

进行手动刷新

4.**fwrite** 对文件中写入数据

对文件中写入指定数据,一般是借助缓冲区进行写入

5.**fread** 读取文件数据

读取文件数据,同理一般是借助缓冲区先进行读取

3.文件打开

MY_FILE *my_fopen(const char *path, const char *mode); //打开文件
  1. 根据传入的mode确定打开方式

  2. 使用系统接口open打开文件

  3. 返回创建好的MY_FILE 类型的指针

  4. 初始化各项参数

文件打开失败的情况:权限不对/open失败/malloc失败,失败就返回NULL

 O_RDONLY    //只读
 O_WRONLY    //只写
 O_APPEND    //追加
 O_CREAT    //新建
 O_TRUNC    //清空

那么读:O_RDONLY

读+:O_RDONLY | O_WRONLY

写:O_WRONLY | O_CREAT | O_TRUNC

写+:O_WRONLY | O_CREAT | O_TRUNC | O_RDONLY

追加:O_WRONLY | O_CREAT | O_APPEND

追加+:O_WRONLY | O_CREAT | O_APPEND | O_RDONLY

会发现写和追加都有O_WRONLY | O_CREAT,我们放在一起处理。

我们想使用这个可以使用按位或来进行得到!

// 打开文件
MY_FILE *my_fopen(const char *path, const char *mode)
{
    assert(path && mode);

    // 确定打开方式
    int flags = 0; // 打开方式

    // 读:O_RDONLY   读+:O_RDONLY | O_WRONLY
    // 写:O_WRONLY | O_CREAT | O_TRUNC   写+:O_WRONLY | O_CREAT | O_TRUNC | O_RDONLY
    // 追加: O_WRONLY | O_CREAT | O_APPEND   追加+:O_WRONLY | O_CREAT | O_APPEND | O_RDONLY
    // 注意:不考虑 b 二进制读写的情况
    if (*mode == 'r')
    {
        flags |= O_RDONLY;
        if (strcmp("r+", mode) == 0)
            flags |= O_WRONLY;
    }
    else if (*mode == 'w' || *mode == 'a')
    {
        flags |= (O_WRONLY | O_CREAT);

        if (*mode == 'w')
            flags |= O_TRUNC;
        else
            flags |= O_APPEND;

        if (strcmp("w+", mode) == 0 || strcmp("a+", mode) == 0)//如果出现了+号的情况
            flags |= O_RDONLY;
    }
    else
    {
        // 无效打开方式
        assert(false);
    }

    // 根据打开方式,打开文件
    // 注意新建文件需要设置权限
    int fd = 0;
    if (flags & O_CREAT)
        fd = open(path, flags, 0666);
    else
        fd = open(path, flags);

    if (fd == -1)
    {
        // 打开失败的情况
        return NULL;
    }

    // 打开成功了,创建 MY_FILE 结构体,并返回
    MY_FILE *new_file = (MY_FILE *)malloc(sizeof(MY_FILE));
    if (new_file == NULL)
    {
        // 此处不能断言,需要返回空
        close(fd); // 需要先把 fd 关闭
        perror("malloc FILE fail!");
        return NULL;
    }

    // 初始化 MY_FILE
    memset(new_file->_buffer, '\0', BUFFER_SIZE); // 初始化缓冲区
    new_file->_current = 0;                       // 下标置0
    new_file->_flush = BUFFER_LINE;               // 行刷新
    new_file->_fd = fd;                           // 设置文件描述符

    return new_file;
}

4.文件关闭

int my_fclose(MY_FILE *fp);   //关闭文件

// 关闭文件
int my_fclose(MY_FILE *fp)
{
    assert(fp);

    // 刷新残余数据
    if (fp->_current > 0)
        my_fflush(fp);

    // 关闭 fd
    int ret = close(fp->_fd);

    // 释放已开辟的空间
    free(fp);
    fp = NULL;

    return ret;
}

5.缓冲区刷新

// 缓冲区刷新
int my_fflush(MY_FILE *stream)
{
    assert(stream);

    // 将数据写给文件
    int ret = write(stream->_fd, stream->_buffer, stream->_current);
    //wirte函数将buffer里的内容写入fd标识的文件中
    //stream->_current 是缓冲区下标,表示缓冲区中有效数据的字节数
    //(从缓冲区的开始到 _current 下标的内容是有效数据)
    stream->_current = 0; // 每次刷新后,都需要清空缓冲区
    fsync(stream->_fd);   // 将内核中的数据强制刷给磁盘(文件)

    if (ret != -1) return 0;
    else return -1;
}

内核缓冲区的刷新:在现代操作系统中,文件写操作通常会先写入内核缓冲区,然后再批量写入磁盘。这种机制提高了写操作的效率,但也带来了数据丢失的风险。fsync 可以强制将内核缓冲区中的数据写入磁盘,减少数据丢失的风险。

6.文件写入

size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream);    //数据写入
  1. 判断当前用户级缓冲区是否满载,如果满了,需要先刷新,再进行后续操作

    if (stream->_current == BUFFER_SIZE)
    my_fflush(stream);

2.获取用户想写入的字节数,和缓冲区剩余的可用空间

size_t user_size = size * nmemb;                 // 用户想写入的字节数
size_t my_size = BUFFER_SIZE - stream->_current; // 缓冲区中剩余可用空间
size_t writen = 0;                               // 成功写入数据的大小

3.如果缓冲区剩余大小ok,那么直接进行copy

if (my_size >= user_size)
{
    // 直接可用全部写入
    memcpy(stream->_buffer + stream->_current, ptr, user_size);
    stream->_current += user_size;
    writen = user_size;
}

4.当缓冲区的大小不足时

else
{
    // 一次写不完,需要分批写入
    size_t tmp = user_size; // 用于定位 ptr 的读取位置
    while (user_size > my_size)
    {
        // 一次写入 my_size 个数据。user_size 会减小
        1.memcpy(stream->_buffer + stream->_current, ptr + (tmp - user_size), my_size);
        2.stream->_current += my_size; // 切记实时更新下标
       
        3.my_fflush(stream);           // 写入后,刷新缓冲区
        
        4.user_size -= my_size;
        5.my_size = BUFFER_SIZE - stream->_current;
    }
    // 最后空间肯定足够,再把数据写入缓冲区中
    6.memcpy(stream->_buffer + stream->_current, ptr + (tmp - user_size), user_size);
    7.stream->_current += user_size;
    8.writen = tmp;
}

image

5.通过刷新方式来判断是否进行刷新

// 通过刷新方式,判断是否进行刷新
if (stream->_flush & BUFFER_NONE)
{
    // 无缓冲,直接冲刷
    my_fflush(stream);
}
else if (stream->_flush & BUFFER_LINE)
{
    // 行缓冲,遇见 '\n' 才刷新
    if (stream->_buffer[stream->_current - 1] == '\n')
        my_fflush(stream);
}
else
{
    // 全缓冲,满了才刷新
    if (stream->_current == BUFFER_SIZE)
        my_fflush(stream);
}

6.简化,返回用户实际写入的字节数

// 为了简化,这里返回用户实际写入的字节数,即 user_size
return writen;

8.总结:

// 数据写入
size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{
    // 写入先判断缓冲区是否已满
    if (stream->_current == BUFFER_SIZE)
        my_fflush(stream);

    size_t user_size = size * nmemb;                 // 用户想写入的字节数
    size_t my_size = BUFFER_SIZE - stream->_current; // 缓冲区中剩余可用空间
    size_t writen = 0;                               // 成功写入数据的大小

    if (my_size >= user_size)
    {
        // 直接可用全部写入
        memcpy(stream->_buffer + stream->_current, ptr, user_size);
        stream->_current += user_size;
        writen = user_size;
    }
    else
    {
        // 一次写不完,需要分批写入
        size_t tmp = user_size; // 用于定位 ptr 的读取位置
        while (user_size > my_size)
        {
            // 一次写入 my_size 个数据。user_size 会减小
            memcpy(stream->_buffer + stream->_current, ptr + (tmp - user_size), my_size);
            stream->_current += my_size; // 切记实时更新下标
            my_fflush(stream);           // 写入后,刷新缓冲区
            user_size -= my_size;
            my_size = BUFFER_SIZE - stream->_current;
        }
        // 最后空间肯定足够,再把数据写入缓冲区中
        memcpy(stream->_buffer + stream->_current, ptr + (tmp - user_size), user_size);
        stream->_current += user_size;
        writen = tmp;
    }

    // 通过刷新方式,判断是否进行刷新
    if (stream->_flush & BUFFER_NONE)
    {
        // 无缓冲,直接冲刷
        my_fflush(stream);
    }
    else if (stream->_flush & BUFFER_LINE)
    {
        // 行缓冲,遇见 '\n' 才刷新
        if (stream->_buffer[stream->_current - 1] == '\n')
            my_fflush(stream);
    }
    else
    {
        // 全缓冲,满了才刷新
        if (stream->_current == BUFFER_SIZE)
            my_fflush(stream);
    }

    // 为了简化,这里返回用户实际写入的字节数,即 user_size
    return writen;
}

7.数据读取

size_t my_fread(void *ptr, size_t size, size_t nmemb, MY_FILE *stream)

在进行数据读取时,需要经历 文件->内核级缓冲区->用户级缓冲区->目标空间 的繁琐过程,并且还要考虑 用户级缓冲区是否能够一次读取完所有数据,若不能,则需要多次读取.

目标:这个函数将读取的数据写入到了ptr上,void*ptr指向用户缓冲区的指针,用于存储读取的数据。

TIP:

  1. 读取前,如果用户级缓冲区中有数据的话,需要先将数据刷新给文件,方便后续进行操作.

  2. 读取与写入不同,读取结束后,需要考虑 **\0** 的问题(在最后一个位置加),如果不加的话,会导致识别错误;系统(内核)不需要 **\0**,但C语言中的字符串结尾必须加 **\0**,现在是 系统->用户(C语言)

实现函数:

// 数据读取
size_t my_fread(void *ptr, size_t size, size_t nmemb, MY_FILE *stream)
{
    // 数据读取前,需要先把缓冲区刷新
    if (stream->_current > 0)
        my_fflush(stream);

    size_t user_size = size * nmemb;//读取用户需要的大小
    size_t my_size = BUFFER_SIZE;//读取缓存区大小

    // 先将数据读取到FILE缓冲区中,再赋给 ptr
    if (my_size >= user_size)
    {
        // 此时缓冲区中足够存储用户需要的所有数据,只需要读取一次
        //从文件中读取my_size个字符到缓存区中
        read(stream->_fd, stream->_buffer, my_size);
        
        memcpy(ptr, stream->_buffer, my_size);
        //将内核缓冲区(buffer)的数据复制到用户提供的缓冲区(ptr)中
        *((char *)ptr + my_size) = '\0';
        //(char *)将ptr强制转换为指向字符类型的指针,这一步的操作目的是让解引用后的位置赋值为字符串终止符‘\0’
        //确保用户提供的缓冲区以字符串终止符结尾
        //为什么-1,比如说读取了2个,0+2=2 0空间存储了一个东西 1 
    }
    //需要反复读的情况,内核缓存区容量不够
    else
    {
        int ret = 1;
        size_t tmp = user_size;
        while (ret)
        {
            // 一次读不完,需要多读取几次
            1.ret = read(stream->_fd, stream->_buffer, my_size);
            2.memcpy(ptr + (tmp - user_size), stream->_buffer, my_size);
            3.stream->_current = 0;
            4.user_size -= my_size;
        }
        *((char *)ptr + my_size) = '\0';
    }

    size_t readn = strlen(ptr);

    return readn;
}

8.实现

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>

int main()
{
    //打开文件,写入一段话
    FILE* fp = fopen("log.txt", "w+");
    assert(fp);

    char inPutBuff[512] = "2110年1月1日,距离地球能源完全枯竭还有3650天。为了解决地球能源危机,\n人类制造了赛尔机器人和宇宙能源探索飞船赛尔号,去寻找神秘精灵看守的无尽能源。";

    int n = fwrite(inPutBuff, 1, strlen(inPutBuff), fp);
    printf("本次成功写入 %d 字节的数据", n);

    fclose(fp);

    printf("\n==============================\n");

    //重新打开文件
    fp = fopen("log.txt", "r");
    assert(fp);

    char outPutBuff[512] = { '\0' };
    n = fread(outPutBuff, 1, sizeof(outPutBuff), fp);
    printf("本次成功读取 %d 字节的数据,具体内容为: \n%s\n", n, outPutBuff);

    fclose(fp);
    fp = NULL;
    return 0;
}

三,升华提升

printf和scanf的原理

printf

1.根据格式读取数据,如整型、浮点型,并将其转为字符串

2.定义缓冲区,然后将字符串写入缓冲区(stdout)

3.最后结合一定的刷新策略,将数据进行冲刷

scanf

1.读取数据至缓冲区(stdin)

2.根据格式将字符串扫描分割,存入字符指针数组

3.最后将字符串转为对应的类型,赋值给相应的变量

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1824310.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

「小爱同学」接入豆包大模型;华为鸿蒙 OS 成为中国第二大操作系统丨 RTE 开发者日报 Vol.224

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE&#xff08;Real-Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

【Android面试八股文】你知道如何实现非阻塞式生产者消费者模式吗?

文章目录 这道题想考察什么 ?考察的知识点日常生活中的生产者消费者模式生产者消费者模式简介为什么需要缓冲区?阻塞与非堵塞非阻塞式生产者消费者模式的实现非阻塞式生产者消费者模式的实现阻塞式生产者消费者模式实现特点这道题想考察什么 ? 是否了解非阻塞式生产者消费者…

R可视化:ggpubr包学习

欢迎大家关注全网生信学习者系列&#xff1a; WX公zhong号&#xff1a;生信学习者 Xiao hong书&#xff1a;生信学习者 知hu&#xff1a;生信学习者 CDSN&#xff1a;生信学习者2 介绍 ggpubr是我经常会用到的R包&#xff0c;它傻瓜式的画图方式对很多初次接触R绘图的人来…

GO RACE 测试在低版本GCC上报错误 exit status 0xc0000139

windows机器环境&#xff0c;go程序使用race定位时一运行就报错&#xff0c;写了个example如&#xff1a; 能看到加了race之后就不行了&#xff0c; 搜了一下&#xff0c;git上有个issue&#xff1a; runtime: Race detector causes exit status 0xc0000139 on Windows 11 wi…

高考志愿填报选专业,兴趣爱好和就业前景哪个优先?

每个人都有自己的兴趣与爱好&#xff0c;而高考志愿填报是在为自己选择职业方向。最理想的状态就是把自己的兴趣和爱好与自己的职业统一起来&#xff0c;让兴趣和爱好促进职业的发展&#xff0c;为职业增添动力。但现实生活中&#xff0c;这种理想的状态并不是每个人都能达到的…

Java多线程面试重点-2

16.Synchronized关键字加在静态方法和实例方法的区别? 修饰静态方法&#xff0c;是对类进行加锁&#xff08;Class对象&#xff09;&#xff0c;如果该类中有methodA和methodB都是被Synch修饰的静态方法&#xff0c;此时有两个线程T1、T2分别调用methodA()和methodB()&#x…

http穿透怎么做?

众所周知http协议的默认端口是80&#xff0c;由于国家工信部要求&#xff0c;域名必须备案才给开放80端口&#xff0c;而备案需要固定公网IP&#xff0c;这就使得开放http80端口的费用成本和时间成本变的很高。那么能不能利用内网穿透技术做http穿透呢&#xff1f;下面我就给大…

Stability AI最新的SD3模型存在严重问题 为规避裸体结果导致躯体部分错乱

人工智能 Stability AI 最新的 SD3 Medium 模型存在严重问题&#xff0c;只要生成人物就会出现躯体错乱&#xff0c;这似乎是该公司刻意规避生成裸体图片的结果。目前猜测他们可能在训练过程中就剔除了 NSFW 内容&#xff0c;同时在训练时规避裸体内容进而导致模型也会刻意将人…

Haption Virtuose 6D力反馈设备遥操作机器人应用研究

在虚拟现实和远程操作技术的飞速发展下&#xff0c;力反馈设备成为了一个不可或缺的工具。其中&#xff0c;Haption Virtuose 6D力反馈设备以其卓越的性能和广泛的应用领域&#xff0c;成为了这一领域的佼佼者。本文将详细介绍Haption Virtuose 6D力反馈设备及其在遥操作机器人…

Flutter系列:关于ensureInitialized()

Flutter系列 关于ensureInitialized() - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_28…

RBAC权限实战

一、项目结构说明、搭建以及初步验证 引入SSM框架依赖: <dependencies> <dependency> <groupId>javax.servlet…

原型模式(大话设计模式)C/C++版本

原型模式 C 参考&#xff1a;https://www.cnblogs.com/Galesaur-wcy/p/15924300.html #include <iostream> #include <string> using namespace std;class WorkExprerience { private:string workDate;string company;public:WorkExprerience() {}~WorkExprerie…

《站在2024年的十字路口:计算机专业是否仍是高考生的明智之选?》

文章目录 每日一句正能量前言行业竞争现状行业饱和度和竞争激烈程度[^3^]新兴技术的影响[^3^]人才需求的变化[^3^]行业创新动态如何保持竞争力 专业与个人的匹配度判断专业所需的技术能力专业核心课程对学生的要求个人兴趣和性格特点专业对口的职业发展要求实践和经验个人价值观…

redis的四种模式部署应用

这里写目录标题 redis应用redis单机部署redis主从redis哨兵Cluster模式 redis应用 redis单机部署 关闭防火墙[rootzyq ~]#: yum -y install wget make gcc gcc-c ...... [rootzyq ~]#: wget https://download.redis.io/redis-stable.tar.gz --2024-01-01 19:41:14-- https:/…

qemu创建kvm虚拟机-x86模拟arm

1、虚拟机环境 虚拟机ubuntu22.042、下载需要的依赖 apt install openssh-server net-tools vim -yapt install qemu qemu-kvm qemu-system-arm bridge-utils uml-utilities qemu-efi-aarch64 cloud-image-utils -y#查看版本 qemu-img -V(1) 下载uefi固件 cd /optwget https…

tcp协议机制的总结(可靠性,提高性能),基于tcp的应用层协议,用udp如何实现可靠传输

目录 总结 引入 可靠性 ​编辑 分析 三次握手 提高性能 其他 常见的基于tcp应用层协议 用udp实现可靠传输 总结 引入 为什么tcp要比udp复杂的多? 因为它既要保证可靠性,又要兼顾性能 可靠性 分析 其中,序列号不止用来排序,还可以用在重传时去重 确认应答是机制中的…

中小制造业工厂要不要上MES系统

MES系统的主要功能包括制造数据管理、计划排产管理、生产调度管理、库存管理、质量管理、人力资源管理、工作中心/设备管理、工具工装管理、采购管理、成本管理、项目看板管理、生产过程控制、底层数据集成分析、上层数据集成分解等。通过这些模块&#xff0c;MES为企业打造一个…

前端菜鸡流水账日记 -- pnpm的学习

哈咯哇大家&#xff0c;我又来了&#xff0c;最近稍微悠闲一些&#xff0c;所以就趁着这个机会学习一些新的知识&#xff0c;今天就是碰巧遇到了pnm&#xff0c;这个可以看作是npm的升级版本&#xff0c;比npm要快&#xff0c;用起来也更得劲更迅速 官网地址&#xff1a;https…

Java ORM 双雄:Mybatis vs Hibernate 的技术对决

引言&#xff1a;本文将对 MyBatis 和H ibernate 进行全面比较&#xff0c;探讨它们在各个方面的异同以及在实际项目中的应用场景。我们将从基本概念、工作原理、配置方式、性能表现、灵活性、学习曲线等多个方面进行比较&#xff0c;帮助读者更好地理解并选择适合自己项目需求…

每日一题——Python实现PAT乙级1028 人口普查 Keyboard(举一反三+思想解读+逐步优化)六千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 题目链接​编辑我的写法 专业点评 时间复杂度分析 空间复杂度分析 总结 我要更强…