【Linux】进程控制(shell的模拟实现)

目录

一.进程终止

进程退出 

 exit(int status)和_exit(int status)

exit的方式退出 

 _exit的方式退出

退出码 

二.进程等待

进程等待方法 

wait

waitpid 

非阻塞等待 

三.进程替换 

execl 

execv 

execle 

四.shell模拟实现

一.进程终止

什么是进程终止,这应该很好理解,就是一个进程结束了,但是一个进程结束又分为三种情况

1.代码跑完了,结果正确

2.代码跑完了,结果错误

3.代码没跑完,直接崩溃了


对于第三种情况来说,我们很好判断,但是我们怎么判断第一种和第二种情况呢,一段代码跑完了,我怎么知道结果正确呢还是错误呢? 


在解释之前,我们来思考一个问题,当我们在写一个C/C++程序的时候,在main函数的末尾总要写一个return 0

这个return 0到底是什么,为什么要写return 0,对于其他函数的调用来说,写一个return,很好理解,因为return可以帮我们把想要的结果带回来, 同样的,main函数中的return也有这样的用处,main函数的return可以把程序的结果正确性反馈给我们,具体怎么做呢,看下面的代码。

 1 #include<stdio.h>2 3 int main()4 {5     int begin=1;6     int end=100;7     int count=0;8     int i=0;9     for(i=begin;i<end;i++)                                                                                                                                                                    10     {11         count+=i;12     }13     if(count==5050)14     {15         return 0;16     }17     else18     {19         return 1;20     }21 }

在这段代码中,我写了一个从1加到100的程序,但是当程序执行完后,我需要知道我写的程序是否正确,即最终结果是否是5050,所以这个时候我需要用到return来判断,如果结果正确,则返回0,错误返回1,通过这样的方式,可以判断我们所写的程序的运行结果是否正确。

那这个结果又如何判断呢查看呢?

当我们的程序运行完后,可以通过下面这命令来获取程序的运行结果 

echo $?

全过程如下图所示。

echo $?

这个命令是将最近一次运行的程序的程序的退出码打印出来 

进程退出 

在上面我们提到了程序的退出码,那么程序的退出码又是什么呢?

其实程序的退出码就是我们上面所说的程序退出结果是否正确。


进程退出的方式又有三种

1.通过main函数的return退出

2.在任意地方调用exit()退出

3.在任意地方调用_exit()退出


对于第一种方法,我们已经讲解过了,那些现在来看一下第二、三种方式,在任意地方调用exit(),_exit(), 

 exit(int status)和_exit(int status)

 

exit()和_exit(),它们的作用是立即终止当前进程,同时参数中的status就是所谓的退出码,当进程通过exit或者_exit来退出的时候,其程序的退出码就是参数status

那么exit_exit有什么区别呢?

1.exit是C语言库所提供的函数,_exit是系统所提供的接口。

2.exit会主动刷新缓冲区等操作,而_exit不会。


对于区别1,这个应该很好理解,那区别2呢?我们来看一下代码来解释。

exit的方式退出 

  1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 5 int main()6 {7     printf("hello world");8     exit(1);                                                                                                                                                                                  9 }

对于exit的方式退出,我们可以看到,这是一个正常的结果,把hello world打印出来后再退出的。 


 _exit的方式退出

  1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 5 int main()6 {7     printf("hello world");8     _exit(1);                                                                                                                                                                                 9 }

对于_exit的退出方法,可以看到,并没有把hello world打印出来。 


所以exit_exit的区别是会不会刷新缓冲区,exit会把缓冲区的东西全部刷新出来打印到屏幕了,而_exit不会,但是远不止这样的区别,exit还会执行用户自定义的清理函数和关闭流等操作。

同时exit和_exit是一个包含与被包含的关系,其实在exit中,是通过调用_exit来实现的,只不过在调用之前会先进行缓冲区的刷新执行用户自定义的清理函数关闭流操作等等。

总的来说,一般都是使用exit来终止我们的进程的。

退出码 

在上面,我们解释可以通过退出码来判断一个程序是否运行正确,同时也讲到了程序退出的三种办法,但是现在有一个问题,程序的退出码都是数字,那这些数字都代表什么呢?

退出码0代表结果正确

退出码非0代表结果错误

但是非0的数字有很多,且这些数字都代表什么错误呢?

别怕,我们来写一个程序将所有的退出码的退出信息打印出来。

在C语言库函数中,有这么一个函数:strerror(),这个函数可以将相应退出码转换为对应的退出信息。

  1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 #include<string.h>5 int main()6 {7     int i=0;8     int j=150;9     for(i=0;i<j;i++)                                                                                                                                                                          10     {11         printf("退出码:%d,退出信息:%s\n",i,strerror(i));12     }13     return 0;14 }

运行这段代码,就可以将所有的退出码所对应的退出信息打印出来,如下图所示。 

 

这里的退出码信息很多,有133条,这里就不全截图了。 

二.进程等待

什么是进程等待?为什么要有进程等待?

进程等待是指父进程等待子进程的一种行为,那么为什么父进程要等待子进程呢?

首先我们要先知道,在一个父进程中,它是可以创建一个子进程的,一般来说,当这个子进程结束的时候,父进程回收子进程相应的资源,但是如果子进程运行时间比父进程长,导致父进程提前结束而被它的父进程回收了资源,那么此时的子进程是一种孤儿进程,当子进程运行结束的时候,由于没有父进程回收资源,则子进程会陷入一种僵尸状态

一旦一个进程变成了僵尸状态,那么就没有办法可以将它杀死,用kill -9也没有办法,则这个僵尸状态的进程就会占用系统资源导致内存泄漏的问题。

所以进程等待的必要性是避免这种情况发生,同时还有另外一个原因,在上面我们提到,当一个程序结束的时候,我们需要知道这个程序的退出信息,这个程序的退出信息会传递给它的父进程,如果它的父进程比它先走,那么这个退出信息又怎么办呢?

所以这个时候就要有进程的等待。


所以要进程等待的两个原因是:

1.父进程要回收子进程的资源

2.父进程要获取子进程的退出信息

进程等待方法 

那么既然要有进程的等待,那么具体怎么做呢?

在linux系统中,提供了几个系统接口给我们用于进程的等待,如下图所示: 

wait

wait是最简单的等待方法,其中这个函数的返回值是所等待进程的pid,status参数是一个int类型的指针,这个指针所指向的内容存储着所等待进程的退出信息

具体用法,如下所示:

  1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h>                                                                                                                                                                            6 int main()7 {8     pid_t id=fork();//创建子进程9 10     if(id==0)//说明是子进程11     {12         int cnt=5;13         while(cnt--)14         {15             printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());16             sleep(1);17         }18         exit(10);//进程退出19     }20     else if(id>0)//说明是父进程21     {22         int status=0;23         pid_t ret=wait(&status);24         printf("父进程等待成功,等待进程的id是:%d\n",ret);25     }26 27     return 0;28 }

运行结果:

waitpid 

通过查手册可以看到waitpid有三个参数

pid_t waitpid(pid_t id,int* status,int option) 

其中,id为需要等的进程的id,option以什么方式等待,status保存着所等进程的退出信息,option阻塞等待非阻塞等待两种方式,在这里我们先以阻塞等待方式来进行演示。

  1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h>6 int main()7 {8     pid_t id=fork();//创建子进程9 10     if(id==0)//说明是子进程11     {12         int cnt=5;13         while(cnt--)14         {15             printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());16             sleep(1);17         }18         exit(10);//进程退出19     }20     else if(id>0)//说明是父进程21     {22         int status=0;23         pid_t ret=waitpid(id,&status,0);//0就是阻塞等待的意思24         printf("父进程等待成功,等待进程的id是:%d,其退出信息是:%d\n",ret,status);                                                                                                          25     }26 27     return 0;28 }

 运行结果:

 通过运行结果我们可以看到,与wait的方式大差不差,但是输出的退出信息却是2560,而我们所写的子进程的退出码是10,就是exit(10)那一句。为什么会这样呢?

那是因为status不是我们所想的那样只是一个单独的整形,它是一个位图结构,具体我们看图

对于这个status,我们只关心它的前16位,其中在这16位中,它的后八位记录着这个进程的退出状态(退出码)前7位记录着这个进程所收到的终止信号。具体这么做呢,我们来看代码。

  1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h>6 int main()7 {8     pid_t id=fork();//创建子进程9 10     if(id==0)//说明是子进程11     {12         int cnt=5;13         while(cnt--)14         {15             printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());16             sleep(1);17         }18         exit(10);//进程退出19     }20     else if(id>0)//说明是父进程21     {22         int status=0;23         pid_t ret=waitpid(id,&status,0);//0就是阻塞等待的意思24         printf("父进程等待成功,等待进程的id是:%d,其退出状态是:%d,收到的信号是:%d\n",ret,(status>>8)&0xff,status&0x7f);//通过位运算,将其退出状态和信号取出来                                                                  25     }26 27     return 0;28 }

 运行结果

非阻塞等待 

在上面的waitpid中,我们默认用的是阻塞等待,但是这里还有一个等待方式:非阻塞等待,那么这两个等待方式又有什么区别呢?

阻塞等待:当子进程没有退出的时候,则父进程进行一个干等,直到子进程退出了,才等待成功,在这个等待的过程中,父进程是不执行其他任何操作,只是干巴巴的等待子进程的退出。

非阻塞等待: 不管子进程有没有退出,父进程只要等待了,就等待成功了,换句话说,就是阻塞等待期间,父进程不能执行其他任何操作,而非阻塞等待期间,父进程可以执行其他操作,这种操作也称为轮询


这句话可能很难理解,我们来看一下代码。

  1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/wait.h>4 #include<unistd.h>5 #include<stdlib.h>6 int main()7 {8     pid_t id = fork();//创建子进程9     if(id == 0)//说明是子进程10     {11         int cnt1=9;12         while(cnt1--)13         {14             printf("子进程正在运行\n");15             sleep(1);16         }17     }18     else if(id > 0)//说明是父进程 19     {20         int status = 0;21         while(1)//进行轮询操作22         {23             pid_t ret = waitpid(id,&status,WNOHANG);//WHOHANG代表非阻塞等待24             if(ret == 0)                                                                                                                                                                      25             {26                 //说明等待成功,但是子进程没有退出27                 printf("等待子进程成功,子进程没有退出,还在运行\n");28             }29             else if(ret > 0)30             {31                 //说明等待成功,子进程退出了32                 printf("等待子进程成功,子进程退出\n");33                 break;34             }35             else36             {37                 //说明等待失败38                 printf("等待子进程失败\n");39                 break;40             }41             sleep(3);42         }43     }44     return 0;45 }

运行结果:

从代码和运行结果我们可以看到,与阻塞等待不同的是,非阻塞等待是一个一瞬间的过程,只要等待成功了,不管子进程是否退出,等待都结束,但是我们通过一个while循环,就可以达到一直循环等待的操作,这种操作叫做轮询,在轮询的期间,父进程可以执行其他任务,这是阻塞等待做不到的。

下面来演示一下非阻塞等待期间,父进程执行其他的任务。 


 如下图代码所示,在父进程等待子进程的期间,父进程去执行别的任务。

#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>typedef void (*pfunc)();//typedef函数指针pfunc arr[4];//定义函数指针数组void Task1()//任务1
{printf("正在执行任务1\n");return;
}void Task2()//任务2
{printf("正在执行任务2\n");return;
}void Task3()//任务3
{printf("正在执行任务3\n");return;
}void LoadTask()//加载任务
{arr[0]=Task1;arr[1]=Task2;arr[2]=Task3;arr[3]=NULL;
}int main()
{LoadTask();//加载任务pid_t id = fork();//创建子进程if(id == 0)//说明是子进程{int cnt1=9;while(cnt1--){printf("子进程正在运行\n");sleep(1);}}else if(id > 0)//说明是父进程 {int status = 0;while(1){pid_t ret = waitpid(id,&status,WNOHANG);//WHOHANG代表非阻塞等待if(ret == 0){//说明等待成功,但是子进程没有退出printf("等待子进程成功,子进程没有退出,还在运行\n");int i=0;for(i=0;i<3;i++)//遍历任务{arr[i]();}}else if(ret > 0){//说明等待成功,子进程退出了printf("等待子进程成功,子进程退出\n");break;}else {//说明等待失败printf("等待子进程失败\n");break;}sleep(3);}}return 0;
}

运行结果:

三.进程替换 

在上面我们所讲到的父进程创建子进程中,都是子进程执行父进程的一部分代码,那么能不能创建一个子进程去执行磁盘中其他的代码了,答案是可以的,这个过程叫做进程替换

在C语言库函数中,提供下面一系列的函数来给我们使用去进行进程替换。

可以看到有很多函数,但是不要怕,其实你只会其中一个,其他的也就很容易了,我们拿第一函数来举一个例子。

execl 

我们先写两份代码,其中在myexec.c中,我们通过进程的替换将子进程替换成./mybin这个程序。

mybin.c

#include<stdio.h>int main()
{printf("我是被替换的进程\n");return 0;
}

myexec.c

#include<stdio.h>
#include<unistd.h>int main()
{pid_t id = fork();//创建子进程if(id == 0)//说明是子进程{//在子进程中进行进程的替换execl("./mybin","./mybin",NULL);//将子进程替换成./mybin,并且也./mybin的方式执行    }else if(id > 0)//说明是父进程{}sleep(1);printf("进程替换结束\n");return 0;
}

在myexec.c中,我们通过execl来进行一个子进程的替换,其中,在这个函数中,要传递的参数如上,第一个参数是你要替换程序的路径,这个路径可以是绝对路径,也可以是相对路径,后面的参数是你要用什么样的方式去执行最后一定要加一个NULL

这就是进程的替换。 

当我们的代码运行起来的时候,我们创建的子进程就会去执行./mybin的代码


那么在exec一类的函数中,有那么多个,这些又有什么区别呢? 

我来挑一些演示一下。 

execv 

#include<stdio.h>
#include<unistd.h>int main()
{pid_t id = fork();//创建子进程if(id == 0)//说明是子进程{//在子进程中进行进程的替换char* const  _argv[]={(char*)"./mybin",(char*)NULL};execv("./mybin",_argv);//将子进程替换成./mybin,并且也./mybin的方式执行    }else if(id > 0)//说明是父进程{}sleep(1);printf("进程替换结束\n");return 0;
}

execle 

这个函数的进程替换的作用是,将我们的环境变量也传过去。如下面所示: 

myexec.c 

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>int main()
{printf("父进程running......\n");pid_t id = fork();if(id==0)//子进程{char* const env_[]={(char*)"MYPATH=111222333",NULL};execle("./mybin","./mybin",NULL,env_);exit(1);}return 0;
}

 mybin.c

#include<stdio.h>
#include<stdlib.h>int main()
{printf("SHELL:%s\n",getenv("SHELL"));//获取环境变量printf("HOME:%s\n",getenv("HOME"));printf("MYPATH:%s\n",getenv("MYPATH"));printf("替换进程!!!\n");printf("替换进程!!!\n");printf("替换进程!!!\n");printf("替换进程!!!\n");printf("替换进程!!!\n"); return 1;
}

运行结果:

通过代码和结果,我们可以看到当使用execle的时候,可以将我们自定义的环境变量传递过去


同时进程的替换不仅可以替换相同类型的语言程序,也能替换其他类型语言的程序,也就是说在我们的程序中,你可以去替换python的程序以及所有后端语言的程序。 

四.shell模拟实现

当进程替换学会了,我们就可以来实践一下了,其实在我们的命令行解释器shell中就有用到进程的替换,我们可以来模拟实现一下。

代码如下: 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>#define NUM 100int main()
{while(1){//输出printf("[用户名@主机名 当前路径]");//输出提示符fflush(stdout);//刷新缓冲区char linemanned[NUM];//定义数组存储我们输入的命令char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);//将命令输入到linemanned数组中linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n//ls -a -l  假设我们输入的名字是这样的,所以要进字符串分割char* arr[NUM];//定义一个指针数组存储分割后的字符串int i=0;arr[i]=strtok(linemanned," ");i++;while(arr[i-1]!=NULL){arr[i]=strtok(NULL," ");i++;}arr[i]=NULL;int id = fork();//创建子进程来运行我们的命令if(id == 0)//说明是子进程{execvp(arr[0],arr);//进行进程替换去运行我们的命令exit(1);//如果子进程创建失败,则用exit退出,且退出码为1}int ret = waitpid(id,NULL,0);//进行一个进程的等待}return 0;
}

当代码运行起来的时候,我们就可以用我们自己的命令行解释器了。如下图所示: 

 

但是我们的这个命令行解释器还不完善,比如说,当我们使用cd命令来切换路径的时候,是做不到的,为什么呢?

在讲解之前,我们先来理解一下什么是当前路径


当前路径可以理解为当前进程的工作目录,在我们没有输入命令之前,当前的进程是我们的命令行解释器shell,所以当前路径可以理解为当前shell的工作目录,当我们进行cd命令的时候,其实是在改变shell的工作目录,但是在我们上面的代码中,我们是通过创建一个子进程来运行我们的命令,所以当我们使用cd命令的时候,其实是在改变这个子进程的工作目录不是shell的工作目录,所以固然达不到我们想要的效果。


所以要想实现cd命令,则需要shell亲自去执行(这种命令也叫内建命令/内置命令),而不是派子进程去执行,同时在这里要用到chdir这个函数。

这个函数的作用是改变当前工作目录,所以我们的代码要进行修改,修改完成后如下:

int main()
{while(1){//输出printf("[用户名@主机名 当前路径]");//输出提示符fflush(stdout);//刷新缓冲区char linemanned[NUM];//定义数组存储我们输入的命令char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);//将命令输入到linemanned数组中linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n//ls -a -l  假设我们输入的名字是这样的,所以要进字符串分割char* arr[NUM];//定义一个指针数组存储分割后的字符串int i=0;arr[i]=strtok(linemanned," ");i++;while(arr[i-1]!=NULL){arr[i]=strtok(NULL," ");i++;}arr[i]=NULL;//进行判断是否cd命令if(arr[0]!=NULL && strcmp(arr[0],"cd") == 0){if(arr[1]!=NULL){chdir(arr[1]);}continue;}int id = fork();if(id == 0)//说明是子进程{execvp(arr[0],arr);exit(1);//如果子进程创建失败,则用exit退出,且退出码为1}int ret = waitpid(id,NULL,0);//进行一个进程的等待}return 0;
}

此时我shell就能实现cd的功能了,但现在的shell功能还不是很完善,我在这里再改进一下,就不过多讲解了。

增加了一个颜色的实现以及获取子进程的退出信息,并稍作修改。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>#define NUM 100char linemanned[NUM];//定义数组存储我们输入的命令
char* arr[NUM];//定义一个指针数组存储分割后的字符串int lastcode = 0;//存储退出码
int lastsign = 0;//存储信号
int status = 0;//存储子进程的退出信息int main()
{while(1){//输出提示符printf("[用户名@主机名 当前路径]");fflush(stdout);//刷新缓冲区//将我们在控制台输入的命令  输入到  linemanned数组中char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n//ls -a -l  假设我们输入的命令是这样的,所以要进行字符串分割int i=0;arr[i++]=strtok(linemanned," ");//使用strtok函数来完成分割if(strcmp(arr[0],"ls") == 0)//如果此时的命令是ls命令,则我们给它带上颜色显示{arr[i++]="--color=auto";}//继续进行字符串分割while(arr[i-1]!=NULL){arr[i++]=strtok(NULL," ");}arr[i]=NULL;//进行判断是否cd命令if(arr[0]!=NULL && strcmp(arr[0],"cd") == 0){if(arr[1]!=NULL){chdir(arr[1]);}continue;}//创建子进程执行我们的命令int id = fork();if(id == 0)//说明是子进程{execvp(arr[0],arr);exit(1);//如果子进程创建失败,则用exit退出,且退出码为1}//子进程运行完成后,要进行一个进程的等待来获取子进程的运行结果int ret = waitpid(id,&status,0);lastcode = (status>>8) & 0xff;lastsign = status & 0x7f;}return 0;
}

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

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

相关文章

2024年企业最担心的是什么?隐私?数据安全?企业聊天软件?

随着科技日新月异的发展&#xff0c;企业在面对激烈的市场竞争时&#xff0c;也必须不断调整策略&#xff0c;积极应对新的挑战。 2024年&#xff0c;企业的关注焦点涉及到多个方面&#xff0c;然而可以认为其最担心的一些核心问题大致为以下几点&#xff1a; 隐私与数据安全。…

2024C++信息素养大赛-算法创意实践挑战_复赛真题(广东省)题目+参考答案和详细解析

第一题&#xff1a; #include<bits/stdc.h> using namespace std; int a,b;int main(){scanf("%d%d",&a,&b);printf("%d",a*b);return 0; } 第二题&#xff1a; #include<bits/stdc.h> using namespace std; int a,b,c;int main(){sca…

如何选择一款适合自己的鼠标?

在今天的数字时代&#xff0c;鼠标已经成为人们日常办公和娱乐的不可或缺的工具之一。然而&#xff0c;市面上各式各样的鼠标琳琅满目&#xff0c;如何选择一款适合自己的鼠标成为了一个令人困惑的问题。 鼠标的类型 在选择鼠标时&#xff0c;首先需要了解鼠标的类型。常见的…

昇思学习打卡-15-热门LLM及其他AI应用/基于MindNLP+MusicGen生成自己的个性化音乐

文章目录 MusicGen权重选择生成音乐采样模式&#xff08;Sampling&#xff09;贪心模式&#xff08;Greedy Search&#xff09;使用 学习使用MindNLPMusicGen生成自己的个性化音乐的流程 MusicGen MusicGen模型基于Transformer结构&#xff0c;可以分解为三个不同的阶段: 用户…

文心快码——百度研发编码助手

介绍 刚从中国互联网大会中回来&#xff0c;感受颇深吧。百度的展商亮相了文心快码&#xff0c;展商人员细致的讲解让我们一行了解到该模型的一些优点。首先&#xff0c;先来简单介绍一下文心快码吧。 文心快码&#xff08;ERNIE Code&#xff09;是百度公司推出的一个预训练…

AGI 之 【Hugging Face】 的【问答系统】的 [Haystack构建问答Pipeline] 的简单整理

AGI 之 【Hugging Face】 的【问答系统】的 [Haystack构建问答Pipeline] 的简单整理 目录 AGI 之 【Hugging Face】 的【问答系统】的 [Haystack构建问答Pipeline] 的简单整理 一、简单介绍 二、构建问答系统 三、用Haystack构建问答pipeline 1、检索器 2、阅读器 3、初…

24暑假计划

暑假计划&#xff1a; 1.从明天起开始将C语言的部分补充完整&#xff0c;这部分的预计在7月24日前完成 2.由于之前的文章内容冗余&#xff0c;接下来进行C语言数据结构的重新编写和后面内容的补充预计8月10号前完成 3.后续开始C的初级学习

YOLOv5改进 | 注意力机制| 对小目标友好的BiFormer【CVPR2023】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录&#xff1a; 《YOLOv5入门 改…

从新手到进阶:高效设计 Tableau 可视化的 5 种技巧 | 数据可视化分析

让我们一起跟着大神学习五个超实用的技巧&#xff0c;加速你的可视化分析之旅&#xff01; 在日常分析中&#xff0c;人人都想实现可视化最佳实践。然而&#xff0c;对于很多初学者来说&#xff0c;在还未熟练掌握 Tableau 的情况下&#xff0c;这种愿望貌似不太符合实际。 为…

【HarmonyOS】获取通讯录信息

【HarmonyOS】获取通讯录信息 一、问题背景&#xff1a; 在Android和IOS中&#xff0c;获取手机通讯录信息的方式&#xff0c;一般是申请通讯录权限后&#xff0c;获得手机所有的通讯录列表信息。 在鸿蒙中&#xff0c;因为权限方式安全性提高的变更&#xff1a;将用户权限限…

【八股系列】CSS盒模型:掌握网页布局的核心

&#x1f389; 博客主页&#xff1a;【剑九 六千里-CSDN博客】 &#x1f3a8; 上一篇文章&#xff1a;【Vue中的&#xff1c;keep-alive&#xff1e;组件&#xff1a;深入解析与实践指南】 &#x1f3a0; 系列专栏&#xff1a;【面试题-八股系列】 &#x1f496; 感谢大家点赞&…

爬虫管理解决方案:让数据收集变得高效且合规

一、为何数据收集的效率与合规性同等重要&#xff1f; 随着大数据技术的飞速发展&#xff0c;数据收集已成为企业决策与市场洞察的核心驱动力。然而&#xff0c;在信息海洋中精准捕捞的同时&#xff0c;如何确保这一过程既高效又不触碰法律的红线&#xff0c;是每个数据实践者…

使用嵌入式知识打造智能手环:nRF52蓝牙开发实战(C++/BLE/传感器)

项目概述 现代人越来越注重健康管理&#xff0c;智能穿戴设备应运而生。本项目旨在利用低功耗蓝牙芯片nRF52832&#xff0c;结合加速度计、心率传感器、陀螺仪等传感器&#xff0c;开发一款功能完善、性能稳定的智能运动手环。该手环能够实时采集用户的运动数据和生理指标&…

用MATLAB绘制三向应力圆

% 定义主应力值 sigma1 100; % MPa sigma2 50; % MPa sigma3 -33; % MPa sigma_m1(sigma1 sigma3)/2; sigma_m2(sigma1 sigma2)/2; sigma_m3(sigma2 sigma3)/2; % 计算半径 r1 (sigma1 - sigma3) / 2; r2 (sigma1 - sigma2) / 2; r3 (sigma2 - sigma3…

《mysql篇》--JDBC编程

JDBC是什么 JDBC就是Java DataBase Connectivity的缩写&#xff0c;翻译过来就很好理解了&#xff0c;就是java连接数据库。所以顾名思义&#xff0c;JDBC就是一种用于执行SQL语句的JavaApl&#xff0c;是Java中的数据库连接规范。为了可以方便的用Java连接各种数据库&#xff…

【Python】从基础到进阶(三):深入了解Python中的运算符与表达式

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、引言二、运算符1. 算术运算符2. 比较运算符3. 逻辑运算符4. 位运算符5. 赋值运算符6. 其他运算符 三、表达式1. 表达式的定义2. 运算符的优先级3. 使用括号提升可读性4. 组合运算符与复合表达式 四、案例&#xff1a;计…

微信管理神器能解决哪些问题?

1、为了自身利益&#xff0c;销售离职单独干&#xff08;带走客户&#xff09; 企业的客户都被销售带走了&#xff0c;那企业会遭到一定的损失&#xff0c;客户的资源是非常保密或重要的。 2、销售客户难以统计&#xff0c;不知道销售整体在干啥&#xff08;没有统计&#xf…

redis学习(009 实战:黑马点评:缓存穿透、缓存雪崩 、缓存击穿)

黑马程序员Redis入门到实战教程&#xff0c;深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目 总时长 42:48:00 共175P 此文章包含第40p-第p45的内容 文章目录 缓存穿透解决方案缓存空对象布隆过滤 解决方案实现缓存穿透总结 缓存雪崩解决方案 缓存击穿解决方…

Codeforces Round 957 (Div.3)

传送门 A. Only Pluses 时间限制&#xff1a;1秒 空间限制&#xff1a;256MB 输入&#xff1a;标准输入 输出&#xff1a;标准输出 问题描述 Kmes 写下了三个整数 a、b 和 c&#xff0c;以记住他要给 Noobish_Monk 的香蕉数量是 a b c。 Noobish_M…

vue3<script setup>自定义指令

main.ts // 自定义指令 app.directive(color,(el,binding) > {el.style.color binding.value })这段代码定义了一个名为color的自定义指令&#xff0c;并将其注册到Vue应用实例app上。自定义指令接收两个参数&#xff1a;el和binding。el是绑定指令的元素&#xff0c;而bi…