Linux【模拟实现C语言文件流】

news2024/10/7 19:19:06

✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、FILE 结构设计
    • 2、函数使用及分析
    • 3、文件打开 fopen
    • 4、文件关闭 fclose
    • 5、缓冲区刷新 fflush
    • 6、数据写入 fwrite
    • 7、数据读取 fread
    • 8、实际效果
    • 9、小结
    • 10、源码
  • 🌆总结


🌇前言

C语言 的文件流中,存在一个 FILE 结构体类型,其中包含了文件的诸多读写信息以及重要的文件描述符 fd,在此类型之上,诞生了 C语言 文件相关操作,如 fopenfclosefwrite 等,这些函数本质上都是对系统调用的封装,因此我们可以根据系统调用和缓冲区相关知识,模拟实现出一个简单的 C语言 文件流

图解

本文重点模拟实现 FILEC语言 文件操作相关函数

注意: 本文实现的只是一个简单的 demo,重点在于理解系统调用及缓冲区


🏙️正文

1、FILE 结构设计

在设计 FILE 结构体前,首先要清楚 FILE 中有自己的缓冲区及冲刷方式

图解
图片来源:《Linux基础IO》 - 2021dragon

缓冲区的大小和刷新方式因平台而异,这里我们将 大小设置为 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;

当前模拟实现的 FILE 只具备最基本的功能,重点在于呈现原理

在模拟实现 C语言 文件操作相关函数前,需要先来简单回顾下


2、函数使用及分析

主要实现的函数有以下几个:

  • fopen 打开文件
  • fclose 关闭文件
  • fflush 进行缓冲区刷新
  • fwrite 对文件中写入数据
  • fread 读取文件数据
#include <stdio.h>
#include <assert.h>
#include <string.h>

int main()
{
    //打开文件,写入数据
    FILE* fp = fopen("file.txt", "w");
    assert(fp);

    const char* str = "露易斯湖三面环山,层峦叠嶂,翠绿静谧的湖泊在宏伟山峰及壮观的维多利亚冰川的映照下更加秀丽迷人";
    char buff[1024] = { 0 };
    snprintf(buff, sizeof(buff), str);
    fwrite(buff, 1, sizeof(buff), fp);

    fclose(fp);
    return 0;
}

结果

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

int main()
{
    //打开文件,并从文件中读取信息
    FILE* fp = fopen("file.txt", "r+");
    assert(fp);

    char buff[1024] = { 0 };
    int n = fread(buff, 1, sizeof(buff) - 1, fp);
    buff[n] = '\0';
    printf("%s", buff);

    fclose(fp);
    return 0;
}

结果

fopen

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

fclose

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

fwrite

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

fread

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

不同的缓冲区有不同的刷新策略,如果未触发相应的刷新策略,会导致数据滞留在缓冲区中,比如如果内存中的数据还没有刷新就断电的话,会导致数据丢失;除了通过特定方式进行缓冲区冲刷外,还可以手动刷新缓冲区,在 C语言 中,手动刷新缓冲区的函数为 fflush

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

int main()
{
    int cnt = 20;
    while(cnt)
    {
        printf("he");   //故意不触发缓冲
        cnt--;
        if(cnt % 10 == 5) 
        {
            fflush(stdout); //刷新缓冲区
            printf("\n当前已冲刷,cnt: %d\n", cnt);
        }
        sleep(1);
    }
    return 0;
}

结果

总的来说,这些文件操作相关函数,都是在对缓冲区进行写入及冲刷,将数据拷贝给内核缓冲区,再由内核缓冲区刷给文件


3、文件打开 fopen

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

打开文件分为以下几步:

  • 根据传入的 mode 确认打开方式
  • 通过系统接口 open 打开文件
  • 创建 MY_FILE 结构体,初始化内容
  • 返回创建好的 MY_FILE 类型

因为打开文件存在多种失败情况:权限不对 / open 失败 / malloc 失败等,所以当打开文件失败后,需要返回 NULL

注意: 假设是因 malloc 失败的,那么在返回之前需要先关闭 fd,否则会造成资源浪费

// 打开文件
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、文件关闭 fclose

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

文件在关闭前,需要先将缓冲区中的内容进行冲刷,否则会造成数据丢失

注意: my_fclose 返回值与 close 一致,因此可以复用

// 关闭文件
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、缓冲区刷新 fflush

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

缓冲区冲刷是一个十分重要的动作,它决定着 IO 是否正确,这里的 my_fflush 是将用户级缓冲区中的数据冲刷至内核级缓冲区

冲刷的本质:拷贝,用户先将数据拷贝给用户层面的缓冲区,再系统调用将用户级缓冲区拷贝给内核级缓冲区,最后才将数据由内核级缓冲区拷贝给文件

因此 IO 是非常影响效率的。数据传输过程必须遵循冯诺依曼体系结构

函数 fsync

  • 将内核中的数据手动拷贝给目标文件(内核级缓冲区的刷新策略极为复杂,为了确保数据能正常传输,可以选择手动刷新)

注意: 在冲刷完用户级缓冲区后(write),需要将缓冲区清空,否则缓冲区就一直满载了

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

    // 将数据写给文件
    int ret = write(stream->_fd, stream->_buffer, stream->_current);
    stream->_current = 0; // 每次刷新后,都需要清空缓冲区
    fsync(stream->_fd);   // 将内核中的数据强制刷给磁盘(文件)

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

6、数据写入 fwrite

size_t my_fwrite(const void *ptr, size_t size, size_t nmemb, MY_FILE *stream);    //数据写入

数据写入用户级缓冲区的步骤:

  1. 判断当前用户级缓冲区是否满载,如果满了,需要先刷新,再进行后续操作
  2. 获取当前待写入的数据大小 user_size 及用户级缓冲区剩余大小 my_size,方便进行后续操作
  3. 如果 my_size >= user_size,说明缓冲区容量足够,直接进行拷贝;否则说明缓冲区容量不足,需要重复冲刷->拷贝->再冲刷 的过程,直到将数据全部拷贝
  4. 拷贝完成后,需要判断是否触发相应的刷新策略,比如 行刷新->最后一个字符是否为 \n,如果满足条件就刷新缓冲区
  5. 数据写入完成,返回实际写入的字节数(简化版,即 user_size

如果是一次写不完的情况,需要通过循环写入数据,并且在缓冲区满后进行刷新,因为循环写入时,目标数据的读取位置是在不断变化的(一次读取一部分,不断后移),所以需要对读取位置和读取大小进行特殊处理

// 数据写入
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、数据读取 fread

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

注意:

  • 读取前,如果用户级缓冲区中有数据的话,需要先将数据刷新给文件,方便后续进行操作
  • 读取与写入不同,读取结束后,需要考虑 \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)
    {
        // 此时缓冲区中足够存储用户需要的所有数据,只需要读取一次
        read(stream->_fd, stream->_buffer, my_size);
        memcpy(ptr, stream->_buffer, my_size);
        *((char *)ptr + my_size - 1) = '\0';
    }
    else
    {
        int ret = 1;
        size_t tmp = user_size;
        while (ret)
        {
            // 一次读不完,需要多读取几次
            ret = read(stream->_fd, stream->_buffer, my_size);
            stream->_buffer[ret] = '\0';
            memcpy(ptr + (tmp - user_size), stream->_buffer, my_size);
            stream->_current = 0;
            user_size -= my_size;
        }
    }

    size_t readn = strlen(ptr);

    return readn;
}

8、实际效果

现在通过自己写的 myStdio 测试C语言文件流操作

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

结果:

结果
下面是库函数的结果:

结果
可以看出结果是一样的


9、小结

用户在进行文件流操作时,实际要进行至少三次的拷贝:用户->用户级缓冲区->内核级缓冲区->文件,C语言 中众多文件流操作都是在完成 用户->用户级缓冲区 的这一次拷贝动作,其他语言也是如此,最终都是通过系统调用将数据冲刷到磁盘(文件)中

图解
此时上一篇文章中的最后一个例子为什么会打印两次 hello fprintf 就很好理解了:因为没有触发刷新条件(文件一般为全缓冲),所以数据滞留在用户层缓冲区中,fork 创建子进程后,子进程结束,刷新用户层缓冲区[子进程],此时会触发写时拷贝机制,父子进程的用户层缓冲区不再是同一个;父进程结束后,刷新用户层缓冲区[父进程],因此会看见打印两次的奇怪现象

结果

最后再简单提一下 printfscanf 的工作原理

无论是什么类型,最终都要转为字符型进行存储,程序中的各种类型只是为了更好的解决问题
printf

  • 根据格式读取数据,如整型、浮点型,并将其转为字符串
  • 定义缓冲区,然后将字符串写入缓冲区(stdout
  • 最后结合一定的刷新策略,将数据进行冲刷

scanf

  • 读取数据至缓冲区(stdin
  • 根据格式将字符串扫描分割,存入字符指针数组
  • 最后将字符串转为对应的类型,赋值给相应的变量

这也就解释了为什么要确保 输出/输入 格式与数据匹配,如果不匹配的话,会导致 读取/赋值 错误


10、源码

关于 myStdio 的源码可以点击下方链接进行获取

模拟实现C语言文件流

图


🌆总结

以上就是本次关于 Linux【模拟实现C语言文件流】的全部内容了,通过 系统调用+缓冲区,我们模拟实现了一个简单版的 myStdio 库,在模拟实现过程中势必会遇到很多问题,而这些问题都能帮助你更好的理解缓冲区的本质:提高 IO 效率


星辰大海

相关文章推荐

Linux基础IO【重定向及缓冲区理解】

Linux基础IO【文件理解与操作】

===============

Linux【模拟实现简易版bash】

Linux进程控制【进程程序替换】

Linux进程控制【创建、终止、等待】

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

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

相关文章

4.3 实施部署Nginx 高可用负载均衡集群

部署大致可分为&#xff1a;准备工作、配置、验证与交付几个步骤&#xff0c;接下来按顺序逐一介绍。 4.3.1 准备工作 Nginx高可以负载均衡集群准备工作分两个层面&#xff1a;前端负载均衡器的准备工作与后端真实服务器的准备工作。根据长期实践出来的经验&#xff0c;先准备后…

更轻更好用的蓝牙耳机,日常佩戴更舒适,QCY Crossky Link体验

平时为了方便接打电话&#xff0c;我经常会戴上一副蓝牙耳机&#xff0c;不过戴久了入耳式的耳机&#xff0c;总感觉不舒服&#xff0c;上个月我看到一款设计很新颖的开放式耳机&#xff0c;来自我之前用过的一个国产品牌&#xff0c;最近到手后试了试&#xff0c;感觉质量不错…

Pandoc 从入门到精通,你也可以学会这一个文本转换利器

Pandoc 简介 如果你需要在不同的文件格式之间相互转换&#xff0c;多半听说或使用过文档转换的瑞士军刀——Pandoc。事实上&#xff0c;不仅人类知道 Pandoc&#xff0c;最近很火的人工智能 ChatGPT 也知道「将 Markdown 转换为 docx」&#xff0c;首选方案是使用 Pandoc。 ​…

Codeforces Round 868 (Div. 2)

Problem - D - Codeforces 思路&#xff1a; 首先&#xff0c;一个位置至多贡献1&#xff0c;不然就是0.如[l1,r]与[l2,r]都是回文串&#xff08;l1<l2) 若(l1r)/2<l2,即[l2,r]本身就是[l1,r]回文串右边的一部分&#xff0c;那么他的贡献在[l1,r]左边已经计算过。如果(…

Python程序的执行过程

哈喽&#xff0c;大家好&#xff0c;五一快乐呀&#xff0c;都去哪里旅游了呢&#xff1f;再游玩之余也花点时间来学习学习&#xff0c;让自己更强哟。这期就给大家分享的是Python程序执行的过程学习。 之前已经给大家介绍了Python语言的简介、Python环境的安装、IDE的选择与安…

4 斐波那契数列

4 斐波那契数列 作者: Turbo时间限制: 1S章节: 递归 问题描述 : 斐波那契数列的排列是&#xff1a;0&#xff0c;1&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;5&#xff0c;8&#xff0c;13&#xff0c;21&#xff0c;34&#xff0c;55&#xff0c;89&#xff0c;…

CTF-PHP反序列化漏洞2-利用魔法函数

作者&#xff1a;Eason_LYC 悲观者预言失败&#xff0c;十言九中。 乐观者创造奇迹&#xff0c;一次即可。 一个人的价值&#xff0c;在于他所拥有的。可以不学无术&#xff0c;但不能一无所有&#xff01; 技术领域&#xff1a;WEB安全、网络攻防 关注WEB安全、网络攻防。我的…

.net 智慧手术麻醉管理系统 WCF框架

实用性&#xff1a; 充分考虑科室业务场景实用性&#xff0c;把满足临床需要为第一要素进行考虑。具有操作简单、提示清晰、逻辑性强&#xff0c;直观简洁等特点。 灵活可自定义&#xff1a; 软件设计采用模块式、组件式开发&#xff0c;根据临床需要可根据实际业务进行组合…

【MATLAB第26期】区间预测 | 基于MATLAB的LASSO分位数回归预测模型 负荷预测数据

【MATLAB第26期】区间预测 | 基于MATLAB的LASSO分位数回归预测模型 负荷预测数据 一、效果预览 MSE: 22.9684327801807 %15分钟 21.6725387054707%30分钟 24.0995456594418%45分钟 21.3992361173462%60分钟 二、数据选择 案例数据&#xff1a;两个月负荷数据3599*30&#x…

网络编程 总结三

一、并发服务器模型 【1】 循环服务器 1>一次只能处理一个客户端的请求&#xff0c;等待这个客户端退出后&#xff0c;才能处理下一个客户端 2>缺点&#xff1a;循环服务器所处理的客户端不能有耗时操作 //*****模型****** sfd socket(); bind(); listen(); while(1)…

springboot websocket通信

目录 一、websocket是什么 二、实现websocket 2.1参考学习b站资料&#xff08;一定要看&#xff0c;前后端详细&#xff09; 2.2学习配套代码 一、websocket是什么 WebSocket_ohana&#xff01;的博客-CSDN博客 二、实现websocket 2.1参考学习b站资料&#xff08;一定要看…

4 异常机制--数组、切片、算法【Go语言教程】

4 异常机制–数组、切片、算法【Go语言教程】 1 异常机制 1.1 处理错误 Go 语言追求简洁优雅&#xff0c;所以&#xff0c;Go 语言不支持传统的 try…catch…finally 这种处理。Go 中引入的处理方式为&#xff1a;defer, panic, recover这几个异常的使用场景可以这么简单描述…

【小沐学Python】Python实现Web服务器(Flask框架扩展:Flask-Admin)

文章目录 1、简介1.1 flask1.2 flask-admin 2、安装2.1 安装库2.2 打印库版本 3、初始化4、添加独立视图&#xff08;BaseView&#xff09;4.1 管理接口索引页4.2 自定义视图 5、添加模型视图&#xff08;ModelView&#xff09;6、添加特殊视图6.1 Managing Files & Folder…

VS2019配置redis客户端的c++开发环境

目录 需要的开源库&#xff1a; 具体步骤&#xff1a; 遇到的问题&#xff1a; 1. 确保每个项目使用的配置解决方案管理器一样 2.CMAKE 的安装 3. 使用 CMAKE 的路径配置 4. redis 编译报错&#xff1a;jemalloc/jemalloc.h没有这个文件&#xff0c;或者找不到.dll 5. linux …

【Linux】初识Linux --指令Ⅰ

Halo&#xff0c;这里是Ppeua。平时主要更新C语言&#xff0c;C&#xff0c;数据结构算法&#xff0c;Linux…感兴趣就关注我吧&#xff01;你定不会失望。 目录 1.ls 显示当前目录下的文件内内容2.pwd-显示用户当前所在的目录3.cd-改变工作目录。将当前工作目录改变到指定的目…

C++ STL-deque

deque&#xff1a;双端队列 目录 deque&#xff1a;双端队列 1、 基本介绍 2、使用方法 2.1、 头文件 2.2、 定义 2.3、 方法函数 3、排序 1、 基本介绍 首尾都可插入和删除的队列为双端队列。 deque容器是连续的空间&#xff0c;其他连续的行空间还有 array 和 vect…

E. Multihedgehog(多叉树找root节点)

Problem - E - Codeforces 有人给Ivan一个奇怪的生日礼物&#xff0c;这是一只刺猬 - 一个连通的无向图&#xff0c;其中一个顶点的度至少为3&#xff08;我们称其为中心&#xff09;&#xff0c;而所有其他顶点的度数均为1。Ivan认为刺猬太无聊了&#xff0c;决定自己制造k-多…

总结838

四月结束&#xff0c;五月来临。又到了月度总结与规划的时候了。 四月总结&#xff1a;高等数学原计划复习完18讲内容&#xff0c;刷完对应的习题。如今还剩三讲&#xff0c;本可以看完&#xff0c;但因为后面分专题了&#xff0c;还没确定是数1还是数2.所以耽搁了一下。英语原…

SG函数(博弈论)

一&#xff0c;定义 对于满足以下条件的公平二人游戏&#xff0c;均可以用sg函数&#xff08;暴搜&#xff09;获得答案 人数2人 两人交替进行合法操作&#xff0c;无法进行者LOSE对 于游戏的任意一种可能的局面&#xff0c;合法的操作集合只取决于这个局面的本身&#xff0c;…

掌握 Python 接口自动化测试理论,深度解读测试框架源码

目录&#xff1a;导读 引言 一、什么是接口测试、为什么要做接口测试 1、什么是接口测试 2、为什么要做接口测试 二、接口测试的流程 1、接口测试的流程 2、为什么要写测试用例 3、接口测试设计要点 三、python接口自动化-requests的应用 1、requests简介 2、reques…