Linux - 基础IO(重定向 - 重定向模拟实现 - shell 当中的 重定向)- 下篇

前言

上一篇博客当中,我们对 文件 在操作系统当中是 如何就管理的,这个问题做了 详细描述,本篇博客将基于上篇 博客当中的内容进行 阐述,如有疑问,请参考上篇博客:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇-CSDN博客

重定向

文件描述符的 分配规则

我们先来看一个例子:

此时我们先 关闭 0 号文件,也就是 stdin 这个文件,然后在使用 open()系统调用接口来 创建一个新的文件,打印这个新文件的 fd 。输出:
 

发现新文件的 fd 文件描述符是 0,也就是原本的 stdin 这个文件描述符。


 同样,我们把 1 号文件 和 2 号文件都试一试,先是 1 号文件:

输出:

发现并没有输出,因为 printf()函数其中使用的系统调用接口就是 1 号文件的 stdout 这个流。

但其实,输出就是1 。

此时,其实是几输入到文件当中了的,只是当前的进程的 stdout 已经被关闭了,所以进程当中是无法使用 printf()函数打印出信息的:

 


 关闭 2 号文件:

输出:
 

 发现,此时新打开的文件的文件描述符 fd 就是 开始关闭的 2 号文件。

所以,按照以上的 输出,其实我们已经可以得出结论了:

在 Linux 当中的文件描述符的分配规则是:


如果有新的文件要打开,那么就要为这个文件分配一个 新的 没有被其他文件使用过的 文件描述符 fd。

分配是按照 : 从 文件描述符表当中的 0 号下标位置处 开始,寻找最小的 还没有使用过的 数组位置,这个位置的下标就是新文件的 fd 文件描述符

 重定向理解 - 重定向的原理

像上述的,把 1 号文件 也就是 stdout 这个显示器文件 给关闭了, 所以我们使用printf()函数就是不能在显示器当中输出数据了。

但是,上述也说过,虽然不会在 显示器当中输入, 但是会在 log.txt 这个新打开的文件当中,把原本应该输出在显示器当中的数据,写入到 log.txt 这个文件当中。

这是为什么呢?
其实看上述的代码你应该也知道了,因为 write()函数是固定像 1 号文件当中写入 数据,但是 1 号文件在 log.txt 这个新文件打开之前就被 关闭了。

也就是说,在 log.txt 这个新文件打开之前, 1 号 fd 文件描述符已经空闲下来了,所以, 新文件就会直接使用 1 号这个 fd 文件描述符。

所以,在后序循环当中 使用 write()函数在 1  号文件当中写入数据,实际是在 log.txt 当中写入数据。

其实,像上述的过程,就是一种重定向

如上图所示,原本 1 号 文件描述符指向的是 显示器文件,但是现在指向的是 log.txt 这个文件了。

而这个变化的过程, 编译器在执行之时 ,也就是 while()循环当中的代码在执行之时,其实是不知道的,他只知道,现在要向 1 号文件当中写入 数据,但是它不知道 1 号文件,此时是 显示器文件 还是 log.txt 文件,还是其他什么文件,他是不知道的。

 而,上述的过程,就是重定向的 原理

 所以,在上图当中的  fd_array [] 数组当中的 每一个元素存储的是 文件对象的起始地址,修改 几号fd ,其实就是修改  fd_array [] 数组当中的 fd 下标位置的 文件对象的地址地址指针。

而,在代码层面,他不管他现在的任务是要在哪一个文件当中写入数据,这个文件在哪,这个文件的地址是多少,这些他都不关系;它只关心,这是几号文件,要在哪一个 fd 的文件的文件当中写入数据。

 重定向的系统调用接口

有三个调用接口。我们主要来谈谈 dup2()这个函数:

 使用 dup2()函数,就可以不向上述一样显示的使用 close()函数来关闭文件,来实现重定向的 功能,直接使用这个 dup2()函数即可

使用 dup2()函数,就不用在关闭 1 号文件了当我们在 程序当中已经打开了 某一个文件,创建了这个文件的 文件描述符,那么在这个 文件描述符指向的 文件描述符表的 下标位置,就存储了这个文件的 文件对象的地址。

使用 dup2()函数,直接 把 某一个文件 在 文件描述符表当中存储的 文件对象的地址,直接拷贝到 需要重定向的 在 文件描述符表当中存储的 文件对象的地址 当中。如下图所示:

  


而 dup2()函数的两个参数:
 

在上述说过的拷贝的过程当中,谁是 "oldfd" , 谁是 "newfd" 呢?

在上述例子当中, 1 号文件是要被 fd 也就是 log,txt 文件对象地址所拷贝的,1 号 是 被拷贝;fd 是拷贝。

所以,在上述例子当中 1 号是  "newfd"; fd 是 "oldfd" 

dup2(fd , 1);

dup2() 接口例子测试

输出重定向 

 还是上述的例子,只不过,此时不使用 close()关闭文件来实现了,而是使用 dup2()函数来实现:
 

输出:
 

在原本是空的 log.txt 文件当中,在运行 text 可执行程序之后,log.txt 当中已经被写入了数据。

 现在的输出结果和上述 使用 close()关闭文件实现的效果是一样的 。

像上述我使用的是 O_TRUNC ,是先清空 文件当中的内容,然后在从头开始写入数据,对应的就是 ">" 这个输出重定向。

如果我们把 O_TRUNC  换成 O_APPEND,就是追加的方式来写入数据,对应的就是 ">>"这个输出重定向。


( 输入重定向) - 使用 read()系统调用接口 和 dup2()

其中的 count 是我们期望 read()函数读取多少个 字节的内容,返回值 ssize_t(这个是有符号整数) 是实际read()函数读取到多少字节的内容。写入到 buf 当中。

比如,count 期望大小我们填入 1024个字节,fd 文件我们选择 0 号 键盘文件,那么,他就会一直阻塞等待我们在键盘当中输入数据到缓冲区当中读取。

 在 open()函数当中,我们可以使用 O_RDONLT 这个参数,代表的意思就是 只读

 输出:
 


 此时,我们使用read()函数在 log.txt 文件当中读取内容,在读取之前,使用 dup2()函数,把 0 号文件(也就是键盘文件 的 fd 值) ,直接替换为 log.txt 文件对象的地址:

此时输出:
 

此时,运行程序,直接把文件当中的内容给输出出来了。 

 此时我们使用 O_RDONLT 这个参数,就实现了 "<" 输入重定向的操作。

这不就是cat命令吗?

直接使用 cat 命令,就可以等待 我们在键盘当中的输入,然后把输入内容打印出来:

或者是 使用 "<" 来向文件当中,读取文件数据并打印:
 

 使用 printf()向文件当中写入数据

在这个例子当中,我们把 1 号文件,利用 dup2()函数 把 1 号文件的文件指针修改为 log.txt 文件,此时,printf()函数 fprintf()就在  log.txt 文件当中写入数据了
 

在上述输出当中, 在显示器当中没有输出,但是在 log.txt 文件当中已经 有数据了。 

 所以,在上述,就算我们 close(fd),这个程序同样是会在 log.txt 文件当中的输入数据的,因为,此时 log.txt 文件对象的地址不只是 fd 保存的,还有 1 号文件描述符也是保存了 log.txt 的文件对象的地址。

而且,我们是在 1 号文件当中写入的 ,所以,是不会影响在log.txt 文件当中写入数据的。

 shell 当中的 输入/输出重定向 的实现 概述

 上述我们已经介绍了 输入/输出重定向 的简单实现,所以,在shell 当中,其实这些 输入/输出重定向 命令是属于 内建命令,直接在内建命令当中 判断 类似 ">>" ">" "<" 这样的字符子串,就可以判断,当前是不是 输入/输出重定向 的操作,就可以执行上述所实现的逻辑。

ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt

 向上述的操作,我们可以发现,  ">>" ">" "<" 这样的字符子串 都是被空格 分隔开的,我们可以利用 之前在shell 模拟实现当中 分割字符串的操作,来提取出  ">>" ">" "<" 这样的字符子串。从而判断当前是不是  输入/输出重定向 操作。

(对于 shell 的模拟实现,参考这篇博客:Linux - 实现一个简单的 shell-CSDN博客)

其实做的工作非常简单:

在知道了上述的 重定向实现原理之后,其实我们只需要做判断即可:
 

比如上述判断 ">" 和 ">>" ,如果 当前的字符是 ">" 的话,如果下一个字符还是   ">" 的话,说明是 ">>" ,反之亦然;找到  ">" 或者 "<" 先把这两个字符修改为 '\0' 作为分割,因为 在 ">" 或者 "<" 之前的是 要输入或者要输出的数据,而在 ">" 或者 "<" 后面的是 文件名。

我们使用 isspace()函数来判断空格:

 在空格之后就是我们想要的文件名。

找到之后,就要保存 两段的数据 命令 和 文件名


在 shell 父进程 创建子进程的之后 马上  ,就要判断当前是否是 需要执行 重定向操作的(如果是,还有判断 是 哪一个重定向操作):

如上所示,在判断 是哪一个重定向,然后就 按照对应的要求,修改 1 或者0 的其中一个 文件描述符值。

同时,文件打开方式也是区分 不同重定向操作的 步骤。0666 是 八进制 的 666 ,代表 用 open()函数创建的文件,是什么访问权限。(注意要减去 umack)

完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <fcntl.h>

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

#define NONE -1
#define IN_RDIR     0
#define OUT_RDIR    1
#define APPEND_RDIR 2

int lastcode = 0;
int quit = 0;
extern char** environ;
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char* rdirfilename = NULL;
int rdir = NONE;

// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表


const char* getusername()
{
    return getenv("USER");
}

const char* gethostname1()
{
    return getenv("HOSTNAME");
}

void getpwd()
{
    getcwd(pwd, sizeof(pwd));
}

void check_redir(char* cmd)
{

    // ls -al -n
    // ls -al -n >/</>> filename.txt
    char* pos = cmd;
    while (*pos)
    {
        if (*pos == '>') // 判断当前是 ">" ">>" 还是 "<"
        {
            if (*(pos + 1) == '>') {   // 判断当前是 ">" 还是 ">>" 
                *pos++ = '\0';
                *pos++ = '\0';
                while (isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir = APPEND_RDIR;
                break;
            }
            else {      // 是 ">"          
                *pos = '\0';
                pos++;
                while (isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir = OUT_RDIR;
                break;
            }
        }
        else if (*pos == '<')  // 是 "<"
        {
            *pos = '\0'; // ls -a -l -n < filename.txt
            pos++;
            while (isspace(*pos)) pos++;
            rdirfilename = pos;
            rdir = IN_RDIR;
            break;
        }
        else {
            //do nothing
        }
        pos++;
    }
}

void interact(char* cline, int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd);
    char* s = fgets(cline, size, stdin);
    assert(s);
    (void)s;
    // "abcd\n\0"
    cline[strlen(cline) - 1] = '\0';

    //ls -a -l > myfile.txt
    check_redir(cline);  // 在上述打印完 命令行,保存命令之后,用这个函数判断
                         // 命令当中是否有 重定向操作
}

int splitstring(char cline[], char* _argv[])
{
    int i = 0;
    argv[i++] = strtok(cline, DELIM);
    while (_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
    return i - 1;
}

// 这个函数主要是实现 有 shell 父进程创建 子进程的过程
void NormalExcute(char* _argv[])
{
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return;
    }
    else if (id == 0) {
        int fd = 0;

        // 判断当前子进程是否需要执行 重定向的工作
        if (rdir == IN_RDIR) // 执行输入重定向
        {
            fd = open(rdirfilename, O_RDONLY);
            dup2(fd, 0);
        }
        else if (rdir == OUT_RDIR) // 执行 ">" 
        {
            fd = open(rdirfilename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if (rdir == APPEND_RDIR)// 执行 ">>" 
        {
            fd = open(rdirfilename, O_CREAT | O_WRONLY | O_APPEND, 0666);
            dup2(fd, 1);
        }
        //让子进程执行命令
        //execvpe(_argv[0], _argv, environ);
        execvp(_argv[0], _argv);
        exit(EXIT_CODE);
    }
    else {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid == id)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}


// 这个函数当中是判断 和 实现一些内建命令的
int buildCommand(char* _argv[], int _argc)
{
    if (_argc == 2 && strcmp(_argv[0], "cd") == 0) {
        chdir(argv[1]);
        getpwd();
        sprintf(getenv("PWD"), "%s", pwd);
        return 1;
    }
    else if (_argc == 2 && strcmp(_argv[0], "export") == 0) {
        strcpy(myenv, _argv[1]);
        putenv(myenv);
        return 1;
    }
    else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) {
        if (strcmp(_argv[1], "$?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode = 0;
        }
        else if (*_argv[1] == '$') {
            char* val = getenv(_argv[1] + 1);
            if (val) printf("%s\n", val);
        }
        else {
            printf("%s\n", _argv[1]);
        }

        return 1;
    }

    // 特殊处理一下ls
    if (strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

int main()
{
    while (!quit) {
        // 1.rdirfilename 用于保存文件名, rdir 保存要输入/输出的数据方式(命令)
        rdirfilename = NULL;
        rdir = NONE;
        // 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
        interact(commandline, sizeof(commandline));

        // commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
        // 3. 子串分割的问题,解析命令行
        int argc = splitstring(commandline, argv);
        if (argc == 0) continue;

        // 4. 指令的判断 
        // debug
        //for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
        //内键命令,本质就是一个shell内部的一个函数
        int n = buildCommand(argv, argc);

        // 5. 普通命令的执行
        if (!n) NormalExcute(argv);
    }
    return 0;
}

shell 当中的 输入/输出重定向  和 进程替换之间的关系

 不知道你有没有发现,我们在 check_redir()函数当中判断,是否需要重定向操作,然后修改对应存储 重定向操作符前后的 命令 和 文件名。

用这两个 存储 重定向操作符前后的 命令 和 文件名 的两个全局变量的值,在 shell 父进程创建子进程 之后 马上,判断当前是否需要执行 重定向的操作。

 但是,如果需要执行重定向,那么 在 修改 文件描述符值 之后,说明此时是在 子进程 当中修改的 文件描述符值。

然后我们要进行程序替换,让子进程执行我们在命令行当中输入的命令。

 那么,在执行 程序替换之时不会替换掉 这些文件描述符表 当中存储的文件对象的地址么吗?

 答案是不会的

首先你要搞清楚的是,一个进程的 各个文件描述符值 是存储在那里的?

  是存储在 struct file_struct 这个结构体 当中的,在这个结构体当中有一个数组,这个数组 就被称作 -- 文件描述符表。在这个数组当中就存储了 这个进程当前所 打开的 各个文件的  文件对象的地址

所以,这是一个机构体,是被 操作系统做管理的结构体对象,所以这个 struct file_struct 这个结构体  和 进程 的 PCB对象一样,都属于 操作系统当中的 内核结构体

而 , 我们进行程序替换,替换的是 代码 数据,把 原本子进程 从 父进程那里拷贝或者共用的代码,进行直接拷贝 或者 写时拷贝的方式,替换为 我们在命令行当中输入 的命令的 代码,而数据也是跟着一起刷新的。

那么 这些 代码 和 数据是存储在哪里的?是存储在 内存的物理空间当中的,要注意区分。

 所以,进程历史打开的所有文件,都在 struct file_struct 这个结构体 当中的 特定数组当中存储了 文件描述符,程序替换不会修改到这个 结构体,所以,程序替换 和 fd 文件描述符 无关

 所以,我们上述在实现之时,才是先把重定向的工作做了,再去程序替换;这样,重定向当中要修改的 文件描述符,和 进程替换无关,那么就不会被修改到。

而且,我们判断 是否需要重定向 的 两个全局变量是在父进程当中存储的,是在创建子进程之前就 已经 完成判断了的,这些数据,在进行程序替换之时都会被替换掉。

 stdout 和 stderr 是区别

 其实两个都是 显示器文件,都是输出重定向来使用的。

两者我们发现,使用 fprintf()函数都可以在 屏幕上打印 数据。

但是,之所以要 使用 两个显示器文件是因为,一个是 normal 正常的 数据输出;另一个 是 error 错误码输出;他想做到一种分流的 作用,把 正常的数据 和 错误的数据分成种方式输出。

为什么呢?

别忘了,输出不仅仅是在 显示器上输出,还可以在 文件当中输出。

所以,可以用两个文件,一个存储 正常的输出数据,一个存储错误输出数据;而我们在使用两种显示器文件输出数据之时,就可以在两个文件当中进程输出不同的数据了。


比如,此时有些 正常数据 和 一些错误的数据要输出:

那么,我们可以使用不同的 显示器文件来 输出,达到分流的目的:
 

 上述这个命令,就是把 mytext 执行文件的输出结果,把 1 号文件的内容输出重定向到 normal.log 当中;把 2 号文件当中的内容 输出重定向到 arr.log 当中。(其中 1 可以不写(这些简写)

这样在两个文件当中就是不同 输出内容了。

 像这样是 先把 1 号文件文件描述符 对应的 文件 当中的内容输入到 all.log 当中,然后把 1 号文件描述符当中的内容写到 2 号文件描述符值当中(2>&1)

 注意,是直接把 1 号文件描述符当中的内容,拷贝到 2 号文件描述符当中,1 号文件描述符当中的内容就是 1 号文件描述符 对应 的文件的 地址。所以,是直接把 1 号文件地址 直接 拷贝到 了 2 号文件描述符当中,相当于是 dup2()函数一样。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

应用层——HTTP协议

文章目录 HTTP协议1.HTTP简介2.认识URL3.urlencode和urldecode4.HTTP协议格式&#xff08;1&#xff09;HTTP请求协议格式&#xff08;2&#xff09;HTTP响应协议格式 5.HTTP的方法6.HTTP的状态码7.HTTP常见的Header8.Cookie和Session HTTP协议 1.HTTP简介 HTTP&#xff08;Hy…

mac安装brew

命令 /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"如图 选择下载源&#xff0c;进行安装 安装完成 验证

计算机毕业设计 基于SpringBoot的实训管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

【零基础小白也能轻松学会】3DMAX编织建模教程

有没有想过这些木质材料是如何在椅子上相互交织的&#xff1f;复杂吗&#xff1f;也许是也许不是……本教程将指导您一步一步地以任何形式提出自己的复杂编织图案。本教程将重点关注建模部分&#xff0c;并让您从那里开始发挥想象力。 1.首先创建一个新平面&#xff08;长度55&…

C++: 内存管理 (new / delete)

文章目录 一. C/C 内存分布二. C 语言中动态内存管理方式: malloc/calloc/realloc/free三. C内存管理方式1. new / delete 操作内置类型2. new / delete 操作自定义类型 四. operator new 与 operator delete 函数五. new 和 delete 的实现原理1. 内置类型2. 自定义类型 六. 定…

【 第八章】软件设计师 之 计算机软件法律法规

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 备考资料导航 软考好处&#xff1a;软考的…

程序员的护城河:职业发展的关键元素

目录 1. 技术深度与广度 2. 项目经验与实际操作 3. 沟通与团队协作 4. 持续学习与自我更新 5. 社区参与与开源贡献 6. 创新思维与解决问题的能力 7. 职业规划与自我管理 结语 在科技日新月异的今天&#xff0c;程序员的竞争已经不再仅仅依赖于技术水平&#xff0c;而是…

路径总和[简单]

优质博文&#xff1a;IT-BLOG-CN 一、题目 给你二叉树的根节点root和一个表示目标和的整数targetSum。判断该树中是否存在 根节点到叶子节点的路径&#xff0c;这条路径上所有节点值相加等于目标和targetSum。如果存在&#xff0c;返回true&#xff1b;否则&#xff0c;返回fa…

基于SpringMVC模式的电器网上订购系统的设计

大家好我是玥沐春风&#xff0c;今天分享一个基于SpringMVC模式的电器网上订购系统的设计&#xff0c;项目源码以及部署相关请联系我&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 本系统利用现在比较广泛的JSP结合后台SpringMybatisAjax编写程序的方式实现的。 在…

【C++入门】构造函数析构函数

目录 前言 1. 类的默认成员函数 2. 构造函数 2.1 什么是构造函数 2.2 构造函数的特性 3. 析构函数 3.1 什么是析构函数 3.2 析构函数的特性 前言 前边我们已经了解了类和对像的基本概念&#xff0c;今天我们将继续深入了解类。类有6个默认成员函数&#xff0c;即使类中什么都…

Golang 字符串处理汇总

1. 统计字符串长度&#xff1a;len(str) len(str) 函数用于统计字符串的长度&#xff0c;按字节进行统计&#xff0c;且该函数属于内置函数也不用导包&#xff0c;直接用就行&#xff0c;示例如下&#xff1a; //统计字符串的长度,按字节进行统计: str : "golang你好&qu…

【数据库开发】DataX开发环境的安装部署(Python、Java)

文章目录 1、简介1.1 DataX简介1.2 DataX功能1.3 支持的数据通道 2、DataX安装配置2.1 DataX2.2 Java2.3 Python 3、DataX Web安装配置3.1 mysql3.2 DataX Web3.2.1 简介3.2.2 架构图3.2.3 依赖环境3.2.4 安装 4、入门使用4.1 DataX自带打印示例测试4.2 DataX生成任务模板文件4…

Leetcode—234.回文链表【简单】

2023每日刷题&#xff08;二十七&#xff09; Leetcode—234.回文链表 直接法实现代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ bool isPalindrome(struct ListNode* head) {if(head NULL) {return t…

ablation study

文章目录 ablation study1、消融实验思想是什么&#xff1f;2、消融实验意义3、消融实验应用场景举例 ablation study 1、消融实验思想是什么&#xff1f; “消融实验”&#xff08;ablation study&#xff09;通常指的是通过逐步移除系统的一部分来评估该系统的贡献。这种方法…

相机突然断电,保存的DAT视频文件如何打开

3-6 本文主要解决因相机突然断电导致拍摄的视频文件打不开的问题。 在平常使用相机拍摄视频&#xff0c;比如使用佳能相机拍摄视频的时候&#xff0c;如果电池突然断电&#xff0c;就非常有可能会导致视频没来得及保存而损坏的情况&#xff0c;比如会产生下图中的这种DAT文件…

【Bug】当用opencv库的imread()函数读取图像,用matplotlib库的plt.imshow()函数显示图像时,图像色彩出现偏差问题的解决方法

一&#xff0c;问题描述 我们在利用opencv的imread读取本地图像&#xff0c;进行一系列处理&#xff0c;但是发现用matplotlib库的imshow&#xff08;&#xff09;函数显示的时候出现色彩改变&#xff0c;比如图像偏黄&#xff0c;偏红&#xff0c;偏蓝等等&#xff0c;但是对…

lesson05-C++模板

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 泛型编程 函数模板 类模板 泛型编程 我们先看一个代码&#xff1a; 看着是不是有点麻烦&#xff0c;我们有没有一种通用的办法&#xff0c;让编译器能够根据不同的类型自动生成不同的函数呢&#xff1f;有&#xff…

【JUC】二、线程间的通信(虚假唤醒)

文章目录 0、多线程编程的步骤1、wait和notify2、synchronized下实现线程的通信&#xff08;唤醒&#xff09;3、虚假唤醒4、Lock下实现线程的通信&#xff08;唤醒&#xff09;5、线程间的定制化通信 0、多线程编程的步骤 步骤一&#xff1a;创建&#xff08;将来被共享的&am…

c primer plus_chapter_four——字符串和格式化输入/输出

1、strlen&#xff08;&#xff09;&#xff1b;const&#xff1b;字符串&#xff1b;用c预处理指令#define和ANSIC的const修饰符创建符号常量&#xff1b; 2、c语言没有专门储存字符串的变量类型&#xff0c;字符串被储存在char类型的数组中&#xff1b;\0标记字符串的结束&a…

低价寄快递寄件微信小程序 实际商用版 寄快递 低价寄快递小程序(源代码+截图)前后台源码

盈利模式 快递代下CPS就是用户通过线上的渠道&#xff08;快递小程序&#xff09;&#xff0c;线上下单寄快递来赚取差价&#xff0c;例如你的成本价是5元&#xff0c;你在后台比例设置里面设置 首重利润是1元&#xff0c;续重0.5元&#xff0c;用户下1kg的单页面显示的就是6元…