当前位置: 首页 > news >正文

【Linux实践系列】:用c语言实现一个shell外壳程序

🔥本文专栏:Linux Linux实践项目
🌸博主主页:努力努力再努力wz

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

那么今天我们就要进入Linux的实践环节,那么我们之前学习了进程控制相关的几个知识点,比如进程的终止以及进程的等待和进程的替换,那么我们接下来就要结合前面所讲的进程控制相关的接口比如fork以及waitpid和execl等来自己实现一个命令行解释器,那么废话不多说,让我们进入正文

★★★ 本文前置知识
进程的替换
进程的终止与等待
进程的概念


shell实现的框架

那么在用c语言真正上手实操我们的shell外壳程序时,那么我们脑海里得有一个大体的实现框架,也就是所谓的一个整体思路,在有了整体思路后我们再去谈具体每个模块的细节,那么我们首先就先从shell本身的工作原理作为切入口入手

那么我们的shell也就是我们的命令行解释器,那么它的工作就是获取用户输入的指令,然后来执行用户输入的指令,那么我们知道用户输入的指令的本质上就是一个字符串,所以shell首先就得读取到用户输入的字符串,然后保存在一个字符数组中,读取到用户输入的字符串之后,那么紧接着下一步便是解析用户输入的字符串,那么我们用户输入的指令无非可以分成两大部分,分别是指令部分以及参数部分,那么这里我们就需要定义一个字符指针数组,那么数组的每一个元素就是一个指针,那么指针指向的就是一个字符串,那么我们用户输入的字符串的指令部分就保存在字符指针数组的第一个元素也就是下标为0的位置,那么参数部分则依次保存在之后的位置,比如我们用户输入的指令是ls -l -a,那么此时我们就要解析为三部分,分别是是指令部分的“ls”字符串以及两个参数部分的字符串"-l”和“-a”,将这三个字符串则是依次保存到我们的字符指针数组下标为0和1和2的位置当中

而具体的解析这三部分字符串则需要用到我们c语言的strtok函数,那么具体细节我们下文再说,那么这里我们讨论的是大的框架与思路,所以我们可以专门定义一个函数来完成这个字符串解析的模块,它的工作就是解析用户输入的字符串将其指令部分以及参数部分的各个字符串分别保存到字符指针数组不同位置中,并且返回命令行的个数,比如用户输入的是ls -l,那么将其保存在字符指针数组char* argv[]并返回的个数就是2,,而如果是pwd,将其保存在字符指针数组char* argv[]并返回的个数就是1

那么接下来解析完用户输入的字符串之后,那么我们就可以来执行用户输入的指令了,那么这里我们知道我们用户输入的各种指令本质上就是在特定路径下保存的一个可执行文件,那么指令的执行本质上就是创建一个进程,那么我们shell执行这些指令就得利用fork函数来创建一个子进程,然后我们利用fork函数的返回值,将父子进程分成不同的执行流,那么在子进程的执行流代码片段中,我们就可以利用进程的替换,那么将我们的子进程的内容替换为我们要执行指令所对应的进程的上下文,那么我们父进程的执行流代码片段则是等待我们子进程的退出结果,那么我们就需要用waitpid函数来获取子进程的退出码

最后获取完子进程的退出码,如果子进程没有正常终止,那么就得将情况返回给用户,也就是将错误信息打印到终端,如果子进程正常终止然后下一步就是重复我们之前上文的环节,那么重复也就意味着我们实现的时候最后这些逻辑的代码都要封装到一个死循环当中。

那么这就是我们实现shell外壳程序的一个大框将,那么我们可以简单将其分为几个模块,分别是获取用户输入->解析用户输入->创建子进程->子进程的替换->父进程等待获取子进程的退出情况->重复上述步骤

那么看到这些模块,想必你一定还有一些疑问,那么接下来我就会在下文补充每个模块的代码实现以及注意的一些细节,和其他的模块的补充,那么有了大框架之后,那么接下来就让我们具体实现每一个模块了
在这里插入图片描述

shell各个模块的实现

1.获取用户输入

那么我们的shell首先得获取用户输入的字符串,那么我们知道在c语言中,我们获取用户输入的字符串常见就是使用我们的scanf函数来获取用户的输入,但是scanf函数有一个缺陷就是一旦读取到空格的时候,那么scanf便停止读取输入,而我们用户在输入字符串的时候,会手动用空格隔开指令部分与参数部分,所以我们就不能采取scanf函数来获取输入,所以这里我们需要用fgets函数,那么fgets函数则是将从标准输入流中读取用户的输入,遇到换行符停止,那么我们可以指定其在输入流中读取的字符串的长度也就是fgets的第二个参数,那么将其保存到一个临时字符数组中,如果读取失败,那么fgets则会返回NULL,读取成功fgets则会返回保存数组的地址

  • fgets

    头文件:<string.h>

  • 功能:获取用户输入的字符串,末尾自动添加\0

    :函数原型

char *fgets(char *str, int n, FILE *stream);

而我们知道用户在输入之前,我们终端都会显示一个命令行提示符,会显示我们当前登录的用户名以及所处的工作目录和运行的主机名称,所以我们在获取用户输入之前,我们得先打印一个字符串也就是命令行提示符,而切记,我们的shell命令行解释器本质也是一个进程,所以这命令行提示符的每一个信息就保存在我们当前进程的环境变量中,我们需要通过我们的系统调用接口getenv来获取其中特定字段的环境变量,这里就需要获取到我们的USER以及HOSTNMAE以及PWD这三个字段,那么我们只需要向getenv函数传递这三个字符串的指针,那么他会依次匹配各个字段的名称所对应的字符串并返回对应的值,也就是字符串的起始地址

代码实现:

  printf("[%s@%s %s]$",getenv("USER"),getenv("HOSTNAME"),getenv("PWD"));if(fgets(temp,sizeof(temp),stdin)==NULL){perror("fgets");continue;}

2.解析命令行

那么现在我们获取了我们的用户输入的字符串之后,那么我们是将用户输入的字符串保存在一个临时字符数组里,那么接下来我们就要将这个字符串给分割,将其指令部分以及参数部分的各个字符串给分割保存到我们的字符指针数组当中,那么我们这里就专门可以定义一个函数来完成字符串解析模块,并返回命令行参数的个数,那么我们知道我们用户输入的字符串会手动以空格分割,那么这里我们就需要调用我们的字符串函数也就是strtok函数来分割我们的字符串按照空格作为分隔符。但是在分割之前,我们又得注意一个细节,也就是我们用户输入完一个字符串,那么它会敲一个回车键来表示输入的结束,而回车则是对应的一个换行符\n,他会被我们的fegts给读取到,那么意味着在我们的字符串的末尾可能会有一个回车换行符

而回车换行符并不是我们一个有效的字符信息,所以我们在解析之前得去掉这个换行符,所以我们就利用我们的strlen函数首先获取到我们这个字符串包括空格以及换行符的总长度,那么如果我判断用户输入的字符串的len-1位置处的字符处是换行符,那么我们就将len-1位置用\0来覆盖,而\0是标记字符串结尾的标志,那么这样我们就可以消去末尾的回车换行符,这里是其中一个关键的实现细节

那么第二个细节就是我们的strtok函数的使用,那么我们strtok函数第一次调用的时候要传递我们要分割的字符串的首元素的地址,那么strtok内部会访问到一个静态的全局变量,这个静态变量是用来保存下一次分割的位置,那么我们每次调用strtok函数的时候,会从分割的起始位置处往后扫描直到遇到分隔符,然后将分隔符的位置修改为\0,然后返回该分割起始位置的指针,而我们知道\0是标记字符串的结尾,所以返回分割起始位置的指针就达到了一个分割子串的一个效果
而下一次调用strtok函数的时候,那么我们就不用传要分割的字符串的首元素的地址,因为上文说过strtok内部能访问到一个记录下一次分割位置的全局变量,那么之后的调用我们只需要传递一个NULL即可,它内部会继续从这个全局变量记录的位置开始扫描到下一个分隔符,将其修改为\0,最后如果我们开始的分割的位置是\0,也就是字符串末尾,没有更多的子串来分割时候,那么strtok就返回一个NULL


  • strtok
    头文件:<string.h>
    -功能:分割字符串

    :函数原型

 char *strtok(char *str, const char *delim);

在这个函数中我们就定义一个int类型的argc变量来跟踪命令行的个数,初始化为0,而我们将分割的字符串保存在对应的字符数组的下标就是argc的值,保存之后接着递增argc,最后返回的该argc就是我们的命令行的个数

代码实现:

int getString(char temp[],char* argv[])
{int len=strlen(temp);if(len>0&&temp[len-1]=='\n'){temp[len-1]='\0';len--;}int argc=0;char* toke=strtok(temp," ");while(toke!=NULL&&argc<length-1){argv[argc++]=toke;toke=strtok(NULL," ");}argv[argc]=NULL;return argc;
}

3.指令判断

那么这里在我们上文介绍实现我们的shell外壳程序的框架的时候模块的时候,其实我们故意漏了一个模块,那就是指令的判断,那么想必你一定会有所疑问,那么就是我们获取解析完用户输入的指令之后,我们为什么还要进行指令的判断呢?直接通通交给子进程去执行不就完了吗,我们父进程也就是shell外壳程序的本职工作不就是获取用户的输入吗

那么这里我们就要注意的就是,我们用户其中输入的指令,比如cd指令,也就是更改我们进程所处的工作目录,那么它针对的对象其实是我们的父进程也就是我们的shell外壳程序,那么如果我们把这个指令交给了子进程去完成,将子进程替换为cd指令所对应的上下文,那么子进程的执行是不会影响父进程的,那么子进程执行结束退出之后,我们shell进程所处的工作目录没有进行更改,那么所以我们对于有些指令,也就是针对当前父进程shell的运行环境的指令,比如cd,比如PWD指令,那么它就不能交给子进程来执行,而是得交给父进程来自己完成,那么这些指令也就是我们的内置指令

那么内置指令那么就不再是一个编写好的可执行文件,那么它是通常是一个实现好的库函数或者直接嵌套在我们的shell进程所对应的代码中,所以我们自己用c语言实现的时候,那么我们就首先准备定义一个全局属性的字符指针数组,然后该数组里面记录了我们所有的内置命令所对应的字符串,那么当解析完用户的指令之后,解析完保存的字符指针数组的第一个位置就是对应用户输入的指令部分的字符串,所以下一步我们依次匹配保存的所有内置命令对应的字符串,如果匹配成功,那么意味着是内置指令,就直接交给我们父进程执行,匹配失败则说明该指令不是内置命令,就交给子进程来执行,那么我们匹配的过程以及内置命令的执行的过程都可以定义两个函数来分别实现这两个模块,那么其中字符串的匹配就需要用到strcmp函数来实现

而所谓的内置命令,他的底层实现的时候本质其实就是依赖用c编写的库函数或者系统调用,比如cd内置命令,那么它就是用chdir库函数来实现的,那么这个库函数的作用就是能够访问到当前进程的环境变量中的工作目录字段,然后修改当前所处的工作目录,而pwd内置命令的本质其实也就是依赖getcwd库函数,那么该库函数会访问到该进程中环境变量记录当前所处也就是工作目录的字段PWD,将其值记录保存到一个数组当中,并且返回指向该数组的指针

  • chdir
    头文件:<unistd.h>

    :函数原型

  int chdir(const char *path);
  • getcwd
    头文件:<unistd.h>

    :函数原型

    char *getcwd(char *buf, size_t size);

那么这里我在实现的时候,就只判断了cd以及pwd这两种内置命令,那么我们可以下来直接去添加更多的内置命令,然后查询对应实现所依赖的库函数或者系统调用接口

代码实现:

bool check(char* argv[])//指令的判断
{for(int i=0;order[i]!=NULL;i++){if(strcmp(argv[0],order[i])==0)//如果该指令是内置命令就返回true{return true;}}return false;
}
void ordercomplete(int argc,char* argv[])//内置命令的执行
{if(strcmp(argv[0],"cd")==0){if(argc==2)//cd指令最多只能两个参数,其中第二个参数就是跳转的工作目录{if (chdir(argv[1]) != 0) {perror("chdir");}}else{printf("error: expected argument for 'cd'\n");}}if(strcmp(argv[0],"pwd")==0){char cwd[length];  // 定义一个字符数组错误的来保存我们的当前所处的工作目录if (getcwd(cwd, sizeof(cwd)) != NULL) {printf("Current working directory: %s\n", cwd);} else {perror("getcwd failed");  // 输出错误信息}}
}

5.子进程执行指令

那么剩下几个模块的细节和实现就很简单了,接下来这个模块就调用fork函数来创建一个子进程,然后利用fork函数的返回值,让父子进程有着不同的执行流,然后我们在子进程对应的执行流代码片段中,调用进程的替换的系统接口,而这里我们调用的exec族函数,一定是不能带有l的比如execl以及execlp等,因为我们不知道用户输入的命令行个数,所以不能用可变参数列表的进程替换接口,这里要注意
而我们用户输入的字符串都解析在了一个字符指针数组中,所以我们传的参数肯定就是一个数组,所以这里我们选择进程替换的函数就是execvp,那么它可以默认在环境变量的PATH中去匹配我们用户输入的指令所对应的可执行文件
那么我们用execvp函数来将子进程替换为指令所对应的进程的上下文,但是我们知道我们进程替换会出现调用失败的情况,那么调用失败的结果则是会执行进程替换接口之后的代码,那么我们就在execvp函数后面打印一个错误信息并且返回一个特殊的退出码

6.父进程的等待

那么我们父进程对应的执行流代码片段则是等待我们子进程的退出情况,所以我们需要调用waitpid函数来获取子进程的退出码,那么waitpid我们的等待方式则是设置为阻塞式等待,那么它的返回值就分别对应两种情况,要么等待成功并且获取到子进程的退出码,对应的返回值就是子进程的pid,而等待失败则是返回-1,我们对于等待失败则是要打印错误信息以及子进程的退出码

完整实现

那么将我们上面的6个模块所对应代码融合就是我们的shell的外壳程序,那么其实我们在实现shell外壳程序的时候,其实shell的整体实现难度不大,主要考察你对shell的工作原理的理解程度和几个系统调用接口的熟悉程度,shell实现的真正的难点其实在它各个模块实现的细节上,很容易出错,其中就考察我们对于一些c语言的库函数的掌握情况,那么接下来我就给出完成的shell的c语言代码的实现

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<stdbool.h>
#define length 1000
#define EXIT_FAIL 40
const char* order[]={"cd","pwd",NULL};int getString(char temp[],char* argv[])
{int len=strlen(temp);if(len>0&&temp[len-1]=='\n'){temp[len-1]='\0';len--;}int argc=0;char* toke=strtok(temp," ");while(toke!=NULL&&argc<length-1){argv[argc++]=toke;toke=strtok(NULL," ");}argv[argc]=NULL;return argc;
}
bool check(char* argv[])
{for(int i=0;order[i]!=NULL;i++){if(strcmp(argv[0],order[i])==0){return true;}}return false;
}
void ordercomplete(int argc,char* argv[])
{if(strcmp(argv[0],"cd")==0){if(argc==2){if (chdir(argv[1]) != 0) {perror("chdir");}}else{printf("error: expected argument for 'cd'\n");}}if(strcmp(argv[0],"pwd")==0){char cwd[length];  // 定义一个足够大的缓冲区来存储路径if (getcwd(cwd, sizeof(cwd)) != NULL) {printf("Current working directory: %s\n", cwd);} else {perror("getcwd failed");  // 输出错误信息}}
}
int main()
{int argc;char* argv[length];char temp[length];while(1){printf("[%s@%s %s]$",getenv("USER"),getenv("HOSTNAME"),getenv("PWD"));if(fgets(temp,sizeof(temp),stdin)==NULL){perror("fgets");continue;}argc=getString(temp,argv);if(argc==0){continue;}if(check(argv)){ordercomplete(argc,argv);continue;}int id=fork();if(id==0){execvp(argv[0],argv);perror("execvp");exit(EXIT_FAIL);}else{int status;int m=waitpid(id,&status,0);if(m<0){perror("waitpid");}else{if(WIFEXITED(status)){if(WEXITSTATUS(status)==40){printf("error\n");}}}}}return 0;
}

在Linux上的运行截图:
在这里插入图片描述

结语

那么这就是用c语言实现shell外壳程序的所有内容啦,那么它也是我第一个学习Linux所完成的一个小项目,那么它这个小项目的教学价值以及学习意义其实非常高,因为它不仅可以帮组你了解shell外壳程序的工作原理,更重要的是帮组你更能熟练掌握运用那几个关于进程控制十分重要的系统调用接口其中比如fork以及waitpid等,那么我的下一篇Linux文章就正式进入文件系统啦,我会持续更新,希望大家多多关注,那么如果本篇文章对你有所帮组的话,那么还请多多三连加关注哦,你的支持就是我最大的动力!

在这里插入图片描述

相关文章:

【Linux实践系列】:用c语言实现一个shell外壳程序

&#x1f525;本文专栏&#xff1a;Linux Linux实践项目 &#x1f338;博主主页&#xff1a;努力努力再努力wz 那么今天我们就要进入Linux的实践环节&#xff0c;那么我们之前学习了进程控制相关的几个知识点&#xff0c;比如进程的终止以及进程的等待和进程的替换&#xff0c;…...

STL map 的 lower_bound(x)、upper_bound(x) 等常用函数

【STL map 简介】 ● STL map 是一种关联容器&#xff0c;存储键值对&#xff0c;每个键&#xff08;key value&#xff09;是唯一的&#xff0c;而值&#xff08;mapped value&#xff09;可以重复。构建 STL map 时&#xff0c;无论元素插入顺序如何&#xff0c;STL map 中的…...

【A2DP】SBC 编解码器互操作性要求详解

目录 一、SBC编解码器互操作性概述 二、编解码器特定信息元素(Codec Specific Information Elements) 2.1 采样频率(Sampling Frequency) 2.2 声道模式(Channel Mode) 2.3 块长度(Block Length) 2.4 子带数量(Subbands) 2.5 分配方法(Allocation Method) 2…...

Computational Linguistics期刊全解析:领域顶刊的投稿指南与学术价值

在人工智能与语言学交叉融合的浪潮中&#xff0c;《Computational Linguistics》&#xff08;CL&#xff09;作为该领域的标杆期刊&#xff0c;始终是研究者发表前沿成果的首选平台。本文将从期刊影响力、投稿策略、收稿方向等角度&#xff0c;为学者提供一份全面的指南。 一、…...

【量化科普】Sharpe Ratio,夏普比率

【量化科普】Sharpe Ratio&#xff0c;夏普比率 &#x1f680;量化软件开通 &#x1f680;量化实战教程 在量化投资领域&#xff0c;夏普比率&#xff08;Sharpe Ratio&#xff09;是一个非常重要的风险调整后收益指标。它由诺贝尔经济学奖得主威廉F夏普&#xff08;William…...

运行OpenManus项目(使用Conda)

部署本项目需要具备一定的基础&#xff1a;Linux基础、需要安装好Anaconda/Miniforge&#xff08;Python可以不装好&#xff0c;直接新建虚拟环境的时候装好即可&#xff09;&#xff0c;如果不装Anaconda或者Miniforge&#xff0c;只装过Python&#xff0c;需要确保Python是3.…...

TikTok Shop欧洲市场爆发,欧洲TikTok 运营网络专线成运营关键

TikTok在欧洲的影响力还在持续攀升&#xff0c;日前&#xff0c;TikTok发布了最新的欧盟执行和使用数据报告&#xff0c;报告中提到&#xff1a; 2024年7~12月期间&#xff0c;TikTok在欧盟地区的月活用户达1.591亿&#xff0c;较上一报告期&#xff08;2024年10月发布&#xf…...

基于YOLO11深度学习的电瓶车进电梯检测与语音提示系统【python源码+Pyqt5界面+数据集+训练代码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...

计算机毕业设计SpringBoot+Vue.js制造装备物联及生产管理ERP系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…...

微服务保护:Sentinel

home | Sentinelhttps://sentinelguard.io/zh-cn/ 微服务保护的方案有很多&#xff0c;比如&#xff1a; 请求限流 线程隔离 服务熔断 服务故障最重要原因&#xff0c;就是并发太高&#xff01;解决了这个问题&#xff0c;就能避免大部分故障。当然&#xff0c;接口的并发…...

labelimg标注的xml标签转换为yolo格式标签

本文不生产技术&#xff0c;只做技术的搬运工&#xff01;&#xff01;&#xff01; 前言 在yolo训练时&#xff0c;我们需要对图像进行标注&#xff0c;而使用labelimg标注时如果直接选择输出yolo格式的数据集&#xff0c;则原始数据的很多信息无法被保存&#xff0c;因此一版…...

VUE3开发-9、axios前后端跨域问题解决方案

VUE前端解决跨域问题 前端页面需要改写 如果无效&#xff0c;记得重启服务器 后端c#解决跨域问题 前端js取值&#xff0c;后端c#跨域_c# js跨域-CSDN博客...

机试准备第12天

首先学习队列&#xff0c;队列有先进先出的特性。广度优先遍历需要基于队列实现&#xff0c;C中的stl引入了队列的实现方式。队列支持push()&#xff0c;进入队尾&#xff0c;pop()出队&#xff0c;队头出队&#xff0c;front()获取队首元素&#xff0c;back()获取队尾元素&…...

计算机二级MS之PPT

声明&#xff1a;跟着大猫和小黑学习随便记下一些笔记供大家参考&#xff0c;二级考试之前将持续更新&#xff0c;希望大家二级都能轻轻松松过啦&#xff0c;过了二级的大神也可以在评论区留言给点建议&#xff0c;感谢大家&#xff01;&#xff01; 文章目录 考题难点1cm25px…...

伊藤积分(Ito Integral):随机世界中的积分魔法

伊藤积分&#xff08;Ito Integral&#xff09;&#xff1a;随机世界中的积分魔法 在研究随机微分方程&#xff08;SDE&#xff09;和布朗运动时&#xff0c;伊藤积分&#xff08;Ito Integral&#xff09;是一个绕不开的关键概念。它是处理布朗运动随机项 ( d W ( t ) dW(t)…...

【Deepseek应用】Zotero+Deepseek 阅读和分析文献(下)

【Deepseek应用】Deepseek R1 本地部署&#xff08;OllamaDockerOpenWebUI&#xff09; 【Deepseek应用】ZoteroDeepseek 阅读和分析文献&#xff08;上&#xff09; 【Deepseek应用】ZoteroDeepseek 阅读和分析文献&#xff08;下&#xff09; 使用邀请码 cXfb9wOT 注册 硅基流…...

人工智能与深度学习的应用案例:从技术原理到实践创新

第一章 引言 人工智能(AI)作为21世纪最具变革性的技术之一,正通过深度学习(Deep Learning)等核心技术推动各行业的智能化进程。从计算机视觉到自然语言处理,从医疗诊断到工业制造,深度学习通过模拟人脑神经网络的层次化学习机制,实现了对复杂数据的高效分析与决策。本…...

Docker和DockerCompose基础教程及安装教程

Docker的应用场景 Web 应用的自动化打包和发布。自动化测试和持续集成、发布。在服务型环境中部署和调整数据库或其他的后台应用。从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。 CentOS Docker 安装 使用官方安装脚本自动安装 安装命令…...

ArcGIS操作:13 生成最小外接矩阵

应用情景&#xff1a;筛选出屋面是否能放下12*60m的长方形&#xff0c;作为起降场候选点&#xff08;一个不规则的形状内&#xff0c;判断是否能放下指定长宽的长方形&#xff09; 1、面积初步筛选 Area ≥ 720 ㎡ 面积计算见 2、打开 ArcToolbox → Data Management Tools …...

Qt:事件

目录 处理事件 鼠标事件 键盘事件 定时器事件 窗口事件 虽然 Qt 是跨平台的 C 开发框架&#xff0c;Qt 的很多能力其实是操作系统提供的 只不过 Qt 封装了系统的 API 事件 前面学习过信号槽&#xff1a; 用户进行的各种操作&#xff0c;就可能会产生出信号&#xff0c;可以…...

第25节 Node.js 断言测试

Node.js的assert模块主要用于编写程序的单元测试时使用&#xff0c;通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试&#xff0c;通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成

厌倦手动写WordPress文章&#xff1f;AI自动生成&#xff0c;效率提升10倍&#xff01; 支持多语言、自动配图、定时发布&#xff0c;让内容创作更轻松&#xff01; AI内容生成 → 不想每天写文章&#xff1f;AI一键生成高质量内容&#xff01;多语言支持 → 跨境电商必备&am…...

SpringTask-03.入门案例

一.入门案例 启动类&#xff1a; package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...

3-11单元格区域边界定位(End属性)学习笔记

返回一个Range 对象&#xff0c;只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意&#xff1a;它移动的位置必须是相连的有内容的单元格…...

使用 SymPy 进行向量和矩阵的高级操作

在科学计算和工程领域&#xff0c;向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能&#xff0c;能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作&#xff0c;并通过具体…...

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

MFC 抛体运动模拟:常见问题解决与界面美化

在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

虚拟电厂发展三大趋势:市场化、技术主导、车网互联

市场化&#xff1a;从政策驱动到多元盈利 政策全面赋能 2025年4月&#xff0c;国家发改委、能源局发布《关于加快推进虚拟电厂发展的指导意见》&#xff0c;首次明确虚拟电厂为“独立市场主体”&#xff0c;提出硬性目标&#xff1a;2027年全国调节能力≥2000万千瓦&#xff0…...

高考志愿填报管理系统---开发介绍

高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发&#xff0c;采用现代化的Web技术&#xff0c;为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## &#x1f4cb; 系统概述 ### &#x1f3af; 系统定…...

数据库正常,但后端收不到数据原因及解决

从代码和日志来看&#xff0c;后端SQL查询确实返回了数据&#xff0c;但最终user对象却为null。这表明查询结果没有正确映射到User对象上。 在前后端分离&#xff0c;并且ai辅助开发的时候&#xff0c;很容易出现前后端变量名不一致情况&#xff0c;还不报错&#xff0c;只是单…...