【Linux】详解自定义Shell管道 | 构建简易进程池

news2024/11/13 15:51:13

目录

续:通信 4 种情况

应用场景

1. 自定义 shell 管道

1. 包含头文件

2. 解析命令函数

详细步骤

3. 执行命令函数

4. 主函数

总结

2. 使用管道实现一个简易版本的进程池

代码结构

代码实现

channel.hpp

tasks.hpp

main.cc

子进程读取任务,重定向

选择进程

主函数

退出任务

测试和观察


续:通信 4 种情况

  1. 读写段正常,管道如果为空,读端就要阻塞
  2. 读写端正常,管道如果被写满,写端就要阻塞
  3. 读端正常读,写端关闭,读端就会读到 0,表面读到了文件(pipe)结尾,不会被阻塞

父进程等待子进程(Z)退出,测试 n=0;

      4.写端是正常写入,读端关闭了的情况呢?

操作系统就要杀掉正在写入的进程,如何干掉?通过信号杀掉

os 是不会做低效,浪费等类似的工作的。如果做了,就是操作系统的 bug

让子进程写入父进程读取是几号信号?代码没跑完,程序出异常了

结论:管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,让write进程退出

查看 信号可以发现

应用场景

这个管道和我们之前学到的知识,哪些是有关系的呢?

  1. 都是 bash 的子进程,匿名管道
cat test.txt | head -10 | tail -5
sleep 666666 | sleep 7777 | sleep 88888

观察 pid 可以发现,具有血缘关系

  1. 我们想让我们的 shell 支持 | 管道,代码该如何写?

1. 自定义 shell 管道

三步:切割 读取 重定向

例如实现:支持单个管道 ls -a -l | wc -l

思路:

0.分析输入的命令行字符串,获取有多少个 l ,命令打散多个子命令字符串

1.malloc 申请空间,pipe 先申请多个管道

2. 循环创建多个子进程,每一个子进程的重定向情况。

  • 最开始,输出重定向,1->指定的一个管道的写端,
  • 中间:输入输出重定向,0 标准输入重定向到上一个管道的读端,1 标准输出重定向到下一根管道的写端
  • 最后一个:输入重定向,将标准输入重定向到最后一个管道的读端

3.分别让不同的子进程执行不同的命令--exec* --exec* 不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向

这段代码是一个简单的管道实现,用于连接多个命令并将它们的输出串联起来。下面是分块解释每部分代码的含义:

1. 包含头文件

#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <cstring>
#include <cerrno>

2. 解析命令函数

void parseCommand(const string& command, vector<string>& cmds) {
    istringstream stream(command);
    string cmd;
    while (getline(stream, cmd, '|')) {
        cmds.push_back(cmd);
    }
}
  • 接收一个字符串 command,并将其拆分为由 | 符号分隔的命令列表。
  • ❓ 使用 istringstream 来从字符串中读取命令。
  • 将每个命令添加到 cmds 向量中。

解答:

istringstream 类是 C++ 标准库中的一个类,它提供了一个类似文件流的接口来从字符串中读取数据。在这个 parseCommand 函数中,istringstream 被用来从一个包含多个命令的字符串中读取各个命令,并将它们存储在一个 vector<string> 容器中。

下面是 parseCommand 函数的详细解释:

void parseCommand(const string& command, vector<string>& cmds) {
    istringstream stream(command); // 创建一个 istringstream 对象
    string cmd;
    while (getline(stream, cmd, '|')) { // 读取命令直到遇到 '|' 字符
        cmds.push_back(cmd); // 将命令添加到 cmds 向量中
    }
}

详细步骤

  1. 创建 istringstream 对象:
istringstream stream(command);

这一行代码创建了一个 istringstream 对象 stream,并将字符串 command 作为构造函数的参数传入。这意味着 stream 现在可以像处理文件一样处理这个字符串。

  1. 读取命令:
while (getline(stream, cmd, '|')) {
    // ...
}

这里使用 getline 函数从 stream 中读取数据getline 的第三个参数是分隔符,这里设置为 '|',这意味着 getline 将读取直到遇到 '|' 字符为止的内容作为一个命令。

    • getline(stream, cmd, '|'):
      • stream: istringstream 对象。
      • cmd: 用来存储读取到的命令的字符串。
      • '|': 用作分隔符的字符。
  1. 存储命令:
cmds.push_back(cmd);

getline 读取到一个命令后,该命令会被存储在 cmd 字符串中,然后将其添加到 cmds 向量中。

  1. 循环继续:while 循环会一直执行,直到 getline 无法再从 stream 中读取到更多数据为止。这意味着当 getline 遇到最后一个命令之后的 '|' 或者到达字符串的结尾时,循环就会结束。

3. 执行命令函数

void executeCommand(const string& cmd, int inputFd, int outputFd) {
    istringstream stream(cmd);
    vector<char*> args;//获取命令段
    string arg;
    while (stream >> arg) {
        char* cstr = new char[arg.size() + 1];
        strcpy(cstr, arg.c_str());
        args.push_back(cstr);
    }
    args.push_back(NULL);

    if (inputFd != 0) {
        dup2(inputFd, 0);
        close(inputFd);
    }
    if (outputFd != 1) {
        dup2(outputFd, 1);
        close(outputFd);
    }

    // 添加 sleep 以便观察进程状态
    sleep(1);

    execvp(args[0], args.data());
    perror("execvp failed");
    exit(EXIT_FAILURE);
}
  • 接收一个命令字符串 cmd,一个输入文件描述符 inputFd 和一个输出文件描述符 outputFd
  • 使用 istringstreamcmd 中读取命令参数。
  • strcpy(cstr, arg.c_str()); 将命令参数转换为 C 风格的字符串数组。
  • 重定向标准输入和输出文件描述符。
  • 使用 execvp 函数执行命令
  • 如果 execvp 失败,则打印错误消息并退出。

4. 主函数

int main() {
    string command = "ls -a -l | wc -l";
    vector<string> cmds;
    parseCommand(command, cmds);

    int numPipes = cmds.size() - 1;//计算需要管道数量
    int pipefds[2 * numPipes];//利用管道数组创建管道

    for (int i = 0; i < numPipes; ++i) {
        if (pipe(pipefds + i * 2) == -1) {
            perror("pipe failed");
            exit(EXIT_FAILURE);
        }
    }

    for (int i = 0; i <= numPipes; ++i) {
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork failed");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            if (i != 0) {
                dup2(pipefds[(i - 1) * 2], 0);
            }
            if (i != numPipes) {
                dup2(pipefds[i * 2 + 1], 1);
            }

            for (int j = 0; j < 2 * numPipes; ++j) {
                close(pipefds[j]);
            }

            // 添加 sleep 以便观察进程状态
            sleep(1);

            executeCommand(cmds[i], 0, 1);
        }
    }

    for (int i = 0; i < 2 * numPipes; ++i) {
        close(pipefds[i]);
    }

    // 添加 sleep 以便观察进程状态
    sleep(1);

    for (int i = 0; i <= numPipes; ++i) {
        wait(NULL);
    }

    return 0;
}

❓ 父进程是如何实现对多个子进程和管道数组进行管理的?

  1. 创建管道:父进程首先计算出需要创建的管道数量,然后使用 pipe 函数为数组 pipefds 创建管道数组。
  2. 创建子进程:父进程使用一个循环来创建多个子进程,每个子进程都负责执行命令数组 cmds 中的一个命令。
  3. 重定向子进程的输入输出:在每个子进程中,根据其在管道数组中的位置,使用 dup2 来重定向其标准输入输出到对应的管道。如果子进程不是第一个,它的标准输入会重定向到前一个管道的读端;如果子进程不是最后一个,它的标准输出会重定向到当前管道的写端。
  4. 关闭不需要的管道描述符:在每个子进程中,关闭所有不需要的管道描述符,防止子进程读取或写入错误的管道。
  5. 执行命令:子进程调用 executeCommand 函数来执行其对应的命令。
  6. 关闭父进程中的管道描述符:在所有子进程创建完毕后,父进程关闭所有管道描述符。
  7. 等待子进程结束父进程使用循环调用 wait 函数来等待所有子进程结束。

以下是管理子进程和管道数组的详细步骤:

关闭所有管道描述符: 在子进程中,通过循环关闭所有 pipefds 中的管道描述符,确保子进程只使用需要的管道描述符。

  • cpp复制
for (int j = 0; j < 2 * numPipes; ++j) {
    close(pipefds[j]);
}

在父进程中,在所有子进程创建之后,也关闭所有管道描述符。

cpp复制

for (int i = 0; i < 2 * numPipes; ++i) {
    close(pipefds[i]);
}

总结

  • 首先解析命令字符串,将它分割成一系列单独的命令。
  • 然后创建所需的管道,并为每个命令创建一个子进程。
  • 每个子进程执行一个命令,并使用管道连接命令的输出
  • 最终输出是最后一个命令的结果。

这个程序模拟了 shell 中管道的工作原理,允许你将多个命令的输出串联起来处理。

问题:

解决:

  1. 替换 nullptrNULL
args.push_back(NULL);
  1. 包含头文件以确保 perror 被正确声明:
#include<stdio.h>
#include<errno.h>

2. 使用管道实现一个简易版本的进程池

通过使用管道实现父进程和子进程之间的通信。该进程池可以执行不同的任务,人选择任务,父进程通过管道向子进程发送任务,子进程执行收到的任务。

代码结构

  1. channel 类:用于保存子进程的信息,包括命令管道、子进程 PID 和子进程名称。
  2. 任务函数:定义一些任务供子进程执行。
  3. slaver 函数:子进程执行的函数,等待从管道读取任务并执行。
  4. 初始化进程池:创建子进程和管道,并保存子进程信息到 channels 向量中。
  5. 控制子进程:父进程选择任务并将任务发送给子进程。
  6. 退出任务:关闭管道并等待子进程退出。

代码实现

channel.hpp

建立好了父进程和子进程之间的通道:先写类再用容器

  1. channel 类
    • channel 类用于存储子进程的信息,包括管道写端文件描述符 _cmdfd,子进程 PID _slaverid 和子进程名称 _processname
#pragma once

#include <string>
#include <vector>
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cassert>

class channel {
public:
    channel(int cmdfd, int slaverid, const std::string &processname)
        : _cmdfd(cmdfd), _slaverid(slaverid), _processname(processname) {}

public:
    int _cmdfd;               // 发送任务的文件描述符
    pid_t _slaverid;          // 子进程的PID
    std::string _processname; // 子进程的名字 -- 方便我们打印日志
};

tasks.hpp

命名规范:

  • 输入:const &
  • 输出:*
  • 输入输出:&
  1. 任务函数
    • 定义了一些任务函数 (task1, task2, task3, task4),这些函数表示子进程可以执行的不同任务。
    • LoadTask 函数通过vector<task_t> *tasks将这些任务加载到 tasks 动态数组中。
#pragma once

#include <iostream>
#include <vector>

typedef void (*task_t)();

void task1() {
    std::cout << "lol 刷新日志" << std::endl;
}

void task2() {
    std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}

void task3() {
    std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}

void task4() {
    std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}

void LoadTask(std::vector<task_t> *tasks) {
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

注意对函数指针的设置  typedef void (*task_t)();  ,便于后续生成任务码

main.cc

  • slaver 函数
  1. 子进程slaver 函数中运行,等待从标准输入(由管道重定向)读取任务码,并执行相应的任务
  • 初始化进程池
  1. InitProcessPool 函数创建管道和子进程,并将子进程的信息保存到 channels 向量中。每个子进程在创建后会关闭不需要的管道端,确保每个子进程只有一个写端。
  • 控制子进程
  1. ctrlSlaver 函数由父进程运行,显示菜单并根据用户输入选择任务和子进程,将任务码通过管道发送给子进程。
  • 退出任务
  1. QuitProcess 函数关闭管道并等待所有子进程退出。

前备芝士:

为什么要设置缓冲区 | slab 分派器?

slab 分派器是一种内存管理技术,用于高效地分配和释放小块内存,减少系统调用,系统调用是有成本的,(如内存分配)需要从用户空间切换到内核空间,

任务码和管道:使用管道和任务码来选择性地调用子进程,实现进程间通信和任务调度。

后缀解释:

  • .cc:C++源文件。
  • .hpp:C++头文件,包含了声明,有时也包含定义,因为这里没有使用库。

管道使用:在管道中,父进程写入数据,子进程选择读取,实现不同进程间共享数据。

子进程读取任务,重定向
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <functional>
#include <cassert>
#include <cstdlib>
#include <ctime>

#include "channel.hpp"
#include "tasks.hpp"

void slaver(const std::vector<task_t> &tasks) {
    while (true) {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int)); // 从标准输入读取命令码
        if (n == sizeof(int)) {
            std::cout << "slaver say@ get a command: " << getpid() << " : cmdcode: " << cmdcode << std::endl;
            if (cmdcode >= 0 && cmdcode < tasks.size()) {
                tasks[cmdcode]();
            }
        }
        if (n == 0) break; // 父进程关闭写端,子进程退出
    }
}

void InitProcessPool(std::vector<channel> *channels, const std::vector<task_t> &tasks, int processnum) {
    std::vector<int> oldfds;
    for (int i = 0; i < processnum; i++) {
        int pipefd[2];
        int n = pipe(pipefd);
        assert(!n); 

        pid_t id = fork();
        if (id == 0) { // child process
            std::cout << "child: " << getpid() << " close history fd: ";
            for (auto fd : oldfds) {
                std::cout << fd << " ";
                close(fd);
            }
            std::cout << std::endl;

            close(pipefd[1]);
            dup2(pipefd[0], 0); // 重定向子进程的读端
            close(pipefd[0]);
            slaver(tasks);
            exit(0);
        } else { // parent process
            close(pipefd[0]);
            std::string name = "process-" + std::to_string(i);
            channels->push_back(channel(pipefd[1], id, name));
            oldfds.push_back(pipefd[1]);
            sleep(1);
        }
    }
}

void Menu() {
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}

批注:

close(pipefd[1]);

dup2(pipefd[0], 0);

选择进程

轮询发送的实现

void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;
    // int cnt = 5;
    while(true)
    {
        int select = 0;
        Menu();
        std::cout << "Please Enter@ ";
        std::cin >> select;

        if(select <= 0 || select >= 5) break;
        // select > 0&& select < 5
        // 1. 选择任务
        // int cmdcode = rand()%tasks.size();
        int cmdcode = select - 1;

        // 2. 选择进程
        // int processpos = rand()%channels.size();

        std::cout << "father say: " << " cmdcode: " <<
            cmdcode << " already sendto " << channels[which]._slaverid << " process name: " 
                << channels[which]._processname << std::endl;
        // 3. 发送任务
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));

        which++;
        which %= channels.size();

        // cnt--;
        // sleep(1);
    }
}

子进程被唤醒,收到了任务

主函数

void QuitProcess(const std::vector<channel> &channels) {
    for (const auto &c : channels) {
        close(c._cmdfd);
        waitpid(c._slaverid, nullptr, 0);
    }
}

int main() {
    srand(time(nullptr) ^ getpid() ^ 1023);

    std::vector<task_t> tasks;
    LoadTask(&tasks);

    int processnum = 4;
    std::vector<channel> channels;
    InitProcessPool(&channels, tasks, processnum);

    ctrlSlaver(channels, tasks);

    QuitProcess(channels);

    return 0;
}

❓ 为什么要埋种子 srand(time(nullptr)^getpid()^1023);实现负载均衡

调用 srand() 函数并传递一个种子值,可以初始化随机数生成器,使得每次运行程序时生成的随机数序列

  1. 安全性:如果随机数生成器没有设置种子,攻击者可以预测随机数序列,这可能会带来安全风险。
  2. 多样性:在需要不同结果的情况下(如游戏、模拟、负载均衡等),设置种子可以确保每次运行都有不同的体验或结果。

任务是在人输入时选择的,经常需要系统随机选择来实现均衡

整个过程:任务先和子进程挂上钩,父进程通过对任务的转化读取子进程

补充:

在 C++ 的 <functional> 头文件中,包含了许多预定义的函数对象和函数适配器。以下是一些算术运算函数对象

    • std::plus<T>:执行加法。
    • std::minus<T>:执行减法。
    • std::multiplies<T>:执行乘法。
    • std::divides<T>:执行除法。
    • std::modulus<T>:执行取模运算。
    • std::negate<T>:执行取反运算。

监控会发现:父端不同文件的写,字段都是 3 读取

退出任务

重点:解决 子进程之间有互相通信的能力 这一问题

在使用管道进行通信的情况下,如果子进程之间未妥善处理管道的文件描述符,可能会导致子进程之间的意外通信和退出的混乱。解决:

关闭文件描述符:父进程首先关闭所有子进程的写端管道,这样,当子进程在尝试读取管道时会发现到达文件末端,从而退出(因为管道的写端关闭,读取操作会返回 0 表示文件结束)。

测试和观察

问题:

解决:

根据错误信息,我们需要进行以下修改:

  1. 使用正确的类型定义:在 for 循环中需要指定变量的类型。
  2. 引入缺失的头文件:如 <string> 中的 to_string 成员。
  3. 解决 nullptr 问题:使用 NULL 替代 nullptr,因为 nullptr 是 C++11 及之后的特性。
  4. 启用 C++11 标准:大多数编译器默认设置为 C++98,需要手动启用 C++11。

首先,我们在编译时启用 C++11 标准:

shCopy code
g++ -std=c++11 -o mypipe main.cc

通过这些步骤,可以测试简易进程池的功能并观察进程的运行情况。

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

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

相关文章

企业数据接口:被执行人(人员)查询

根据搜索关键字、名称等参数&#xff0c;获取被执行人相关信息 批量获取企业信息

摄影曝光:光圈大小认知

写在前面 学习整理《摄影曝光&#xff1a;拍出好照片的49个关键技法》读书笔记博文内容涉及光圈&#xff0c;景深认知 &#xff0c;以及光圈和镜像的选择适合小白认知理解不足小伙伴帮忙指正 &#x1f603;,生活加油 99%的焦虑都来自于虚度时间和没有好好做事&#xff0c;所以唯…

基于hadoop的网络流量分析系统的研究与应用

目录 摘要 1 Abstract 2 第1章 绪论 3 1.1 研究背景 3 1.2 研究目的和意义 4 1.2.1 研究目的 4 1.2.2 研究意义 6 1.3 国内外研究现状分析 7 1.3.1 国内研究现状 7 1.3.2 国外研究现状 9 1.4 研究内容 11 第2章 Hadoop技术及相关组件介绍 12 2.1 HDFS的工作原理及…

小阿轩yx-Docker Compose与私有仓库部署

小阿轩yx-Docker Compose 与私有仓库部署 Docker 的网络模式 Docker 四种网络模式 网络模式参数说明host 模式- - nethost 容器和宿主机共享 Network namespace container 模式- - net{id} 容器和另外一个容器共享 Network namespace。 kubernetes 中的pod就是多个容器共享一…

Win11让局域网内其他电脑通过IP访问自己电脑上的网站

1.步骤&#xff1a;设置-->控制面板-->Windows Defender 防火墙-->高级设置 2.修改域配置文件 入站链接”改为”允许“选项。 3.修改专用配置文件 入站链接”改为”允许“选项。 4. 大功告成了&#xff01;&#xff01;&#xff01; 设置完可以看到&#xff0c;下图…

消费企业经营管理的两大痛点!一篇文章讲透解决办法!

在当下这个快速变化的消费市场中&#xff0c;企业面临着前所未有的挑战和机遇。消费企业&#xff0c;尤其是那些直接面向消费者的零售商&#xff0c;需要不断地适应市场动态&#xff0c;以保持竞争力。然而&#xff0c;在这个过程中&#xff0c;它们往往会遇到一些普遍的痛点&a…

高通DSP、HVX、HMX

CDSP Compute Digital Signal Processor cDSP主要用途有&#xff1a; 1、摄像头、视频的图像增强相关处理 2、计算机视觉、增强、虚拟现实处理 3、深度学习硬件加速 HVX Hexagon Vector Extension HVX意味着你可以将视频和摄像机任务从CPU转移到Hexagon DSP&#xff…

Linux LD_PRELOAD优先加载so失效原因分析

网上由很多介绍LD_PRELOAD劫持的文章&#xff0c;我就不做过多介绍&#xff0c;之前有碰到失效的&#xff0c;网上找了很久没找到原因&#xff0c;后面分析出原因&#xff0c;现在写出来给后人避坑。 Linux系统使用LD_PRELOAD环境变量可以让程序优先加载指定的so文件&#xff0…

ES6 (一)——ES6 简介及环境搭建

目录 简介 环境搭建 可以在 Node.js 环境中运行 ES6 webpack 入口 (entry) loader 插件 (plugins) 利用 webpack 搭建应用 gulp 如何使用&#xff1f; 简介 ES6&#xff0c; 全称 ECMAScript 6.0 &#xff0c;是 JavaScript 的下一个版本标准&#xff0c;2015.06 发版…

探索 Kubernetes 持久化存储之 Rook Ceph 初窥门径

在 Kubernetes 生态系统中&#xff0c;持久化存储是支撑业务应用稳定运行的基石&#xff0c;对于维护整个系统的健壮性至关重要。对于选择自主搭建 Kubernetes 集群的运维架构师来说&#xff0c;挑选合适的后端持久化存储解决方案是关键的选型决策。目前&#xff0c;Ceph、Glus…

护眼台灯什么牌子好?曝光劣质产品常见的四大套路

孩子使用护眼台灯什么牌子好&#xff1f;孩子不仅是家庭的希望&#xff0c;也是国家的未来。为了让孩子们在未来具备更强的竞争力&#xff0c;父母们总是竭尽全力提供最佳的教育资源&#xff0c;如购置优质学位房、报名各类培训课程和兴趣班。然而&#xff0c;在这些努力之外&a…

AR技术:汽车行业创新发展的新动力

在当今科技飞速发展的时代&#xff0c;增强现实技术&#xff08;AR&#xff09;正逐渐在各个领域展现出其独特的优势和应用价值。在汽车行业中&#xff0c;AR也扮演着越来越重要的角色&#xff0c;为汽车的设计、制造、维修和销售等环节带来了诸多创新和变革。以下是汽车行业中…

排序算法之折半插入排序

title: 折半插入排序 date: 2024-7-19 10:17:24 0800 categories: 排序算法 tags:排序算法折半插入排序 description: 折半插入排序&#xff08;Binary Insertion Sort&#xff09;是插入排序的一种改进版本。它在插入每个元素时使用二分查找&#xff08;Binary Search&#x…

一文读懂第三代半导体

一、氮化嫁&#xff08;GaN&#xff09;定义 氮化镓材料定义&#xff1a;氮化镓(GaN)主要是由人工合成的一种半导体材料&#xff0c;禁带宽度大于2.3eV&#xff0c;也称为宽禁带半导体材料。 氮化镓材料为第三代半导体材料的典型代表&#xff0c;是研制微电子器件、光电子器件…

AI技术在招聘数据分析洞察中的作用

一、引言&#xff1a;AI赋能招聘新纪元 在数字化转型浪潮中&#xff0c;人工智能技术&#xff08;AI&#xff09;正以前所未有的速度渗透至各行各业&#xff0c;其中&#xff0c;招聘领域正经历着一场深刻的变革。传统招聘模式依赖于人工筛选简历、面试评估等低效且主观性强的…

【源码+论文】基于VUE的新闻类网站

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 获取资料请私聊我 1 概述 1.1课题背景及意义 随着现代网络技术发展&#xff0c;对于新闻类网站的设计现在正处于发展的阶段&#xff0c;所以对的要求也是比较严格的&#xff0c;要从系统的…

c++编程(20)——类与对象(6)继承

欢迎来到博主的专栏——c编程 博主ID&#xff1a;代码小豪 文章目录 继承继承与权限访问 基类和派生类基类和派生类的赋值兼容转换基类与派生类的类作用域派生类与基类的构造函数基类与派生类拷贝构造函数 继承与静态成员final关键字 面向对象编程的核心思想是封装、继承和多态…

LeetCode - 209 - 长度最小的子数组

力扣209题 题目描述&#xff1a;长度最小的子数组 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度**。**如果不存在符合条件的子数组&…

unordered_map与unordered_set的实现

目录 1.底层结构 1&#xff09;方式 2&#xff09;哈希冲突 3&#xff09;哈希函数 4&#xff09;哈希冲突的解决 1.闭散列 1&#xff09;线性探测 扩容&#xff1a; 2&#xff09;二次探测 2.开散列 1&#xff09;概念 2&#xff09;实现 插入操作&#xff1a; 删…

8个不可错过的高清视频素材网

寻找优质高清视频素材是许多创意工作者和内容创作者必不可少的一项任务。无论是在制作广告、影视作品&#xff0c;还是在进行视频编辑和设计工作&#xff0c;高质量的视频素材都能为你的作品增色不少。 推荐8个备受好评的高清视频素材网站&#xff0c;这些网站提供丰富多样、高…