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

【Linux】编写简易shell 深度理解命令行解释器 环境变量 内建命令

 🌻个人主页:路飞雪吖~

       🌠专栏:Linux


目录

主体:

一、命令行提示符 

二、获取用户命令

三、分析命令

四、执行命令

五、内建命令

🌠 小贴士: 

 六、环境变量 

七、总结

八、完整代码


如若对你有帮助,记得关注、收藏、点赞哦~ 您的支持是我最大的动力🌹🌹🌹🌹!!!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢 ヾ(≧▽≦*)o  \( •̀ ω •́ )/

主体:

 int main(){while(true){PrintCommandLine();   // 1. 命令行提示符GetCommandLine();    // 2. 获取用户命令AnalysisCommandLine(); // 3. 分析命令ExecuteCommand(); // 4. 执行命令}return 0;}

一、命令行提示符 

命令行提示符一般由:用户名+主机名+当前工作路径  组成。我们如何获取呢?这就不得不提到我们之前提到过的 环境变量表 了,我们知道每个子进程都会继承父进程的环境变量表,所以我们可以从环境变量表来获取用户名、主机名、当前工作路径。

  1 #include <iostream>2 #include <cstdio>3 #include <cstdlib>4 #include <cstring>5 #include <unistd.h>6 #include <sys/types.h>7 #include <wait.h>8                                9 using namespace std;                  10  11 int basesize = 1024;              12                                                                                            13 string GetUserName()                                                          14 {                               15     string username = getenv("USER");                16     return username.empty() ? "None" : username;                            17 }                                            18                             19 string GetHostName()                         20 {                                          21     string hostname = getenv("HOSTNAME");  22     return hostname.empty() ? "None" : hostname;  23 }                           24                             25 string GetPWD()             26 {                           27     string pwd = getenv("PWD");                   28     return pwd.empty() ? "None" : pwd;  29 }                           30                                                     31 string MakeCommandLine()    32 {                                               33     char command_line[basesize];  34     snprintf(command_line, basesize, "[%s@%s %s]# ",\  35             GetUserName().c_str(), GetHostName().c_str(), GetPWD().c_str());  36     return command_line;    37                             38 }                     39 void PrintCommandLine()  // 1. 命令行提示符  40 {    41     printf("%s",MakeCommandLine().c_str());  42     fflush(stdout);  43 }                     44    45 int main()       46 {47     while(true)48     {49          PrintCommandLine();  // 1. 命令行提示符50          printf("\n");51          sleep(1);52          //GetCommandLine();    // 2. 获取用户命令53 54          //ParseCommandLine();  // 3. 分析命令55 56          //ExecuteCommand();    // 4. 执行命令57     }58     return 0;59 }

二、获取用户命令

• 从键盘当中获取出来的字符串,放到command_buffer缓冲区里面
• 将用户输入的命令行,当作完整的字符串
• "ls -a -l -n" 包括空格
•  fgets(获取字符串, 指定大小, 从特定的文件流当中获取)

• 获取失败返回null,获取成功返回获取成功的字符串的起始地址

 46 bool GetCommandLine(char command_buffer[], int size)    // 2. 获取用户命令47 {                                     48     char *result = fgets(command_buffer, size, stdin);49     if(!result)                       50     {                                 51         return false;                 52     }                                 53     command_buffer[strlen(command_buffer)-1] = 0; // 把最后输入的回车字符改为0,即纯净版输入                                    54     if(strlen(command_buffer) == 0) return false;55     return true;                      56 }                                     57                                       58 int main()                            59 {                                     60     char command_buffer[basesize];    61     while(true)                       62     {                                 63          PrintCommandLine();  // 1. 命令行提示符64                                       65          if(!GetCommandLine(command_buffer, basesize))    // 2. 获取用户命令66          { // 依次获取每个子字符串                           67             continue; // 获取到空格继续                68          }                            69          printf("%s\n",command_buffer);70                                       71          //ParseCommandLine();  // 3. 分析命令                                             72 73          //ExecuteCommand();    // 4. 执行命令74     }75     return 0;76 }

三、分析命令

解析出来的命令,放到自己构建的全局的环境变量表当中
• strtok(一个字符串分多个,分隔符) 返回值:按照该分隔符从源字符串里切出来的第一个区域
• strtok(str, " ");      如果切割有效的字符串,返回具体的字符串的第一个地址,
• strtok(nullptr, " ");    否则,返回nullptr,即切到最后没有了,自动返回nullptr

      const int argvnum = 64;const int envnum = 64;char *gargv[argvnum];// 全局的环境变量表char gargc = 0; // 记录环境变量的个数65 void ParseCommandLine(char command_buffer[], int len)  // 3. 分析命令66 {67     (void)len;68     memset(gargv, 0, sizeof(gargv)); // 初始化/清空环境变量表里面的内容69     gargc = 0;70 71     const char *sep = " ";72 73     gargv[gargc++] = strtok(command_buffer, sep); // 放到全局的环境列表里面74 75     // 循环放入环境变量表中76     while((bool)(gargv[gargc++] = strtok(nullptr, sep)));77     gargc--;78 }79 80 void debug()81 {82     printf("argc: %d\n",gargc);83     for(int i=0; gargv[i]; i++)84     {85         printf("argv[%d]: %s\n", i, gargv[i]);86     }87 }88 89 int main()90 {91     char command_buffer[basesize];92     while(true)93     {94          PrintCommandLine();  // 1. 命令行提示符95          96          if(!GetCommandLine(command_buffer, basesize))    // 2. 获取用户命令97          {98             continue;99          }100          printf("%s\n",command_buffer);101 102          ParseCommandLine(command_buffer, strlen(command_buffer));  // 3. 分析命令103          debug();104 105          //ExecuteCommand();    // 4. 执行命令106     }107     return 0;108 }

四、执行命令

到底是谁在执行命令?是shell自己执行吗?

shell不能自己执行命令,如果shell自己可以自行命令,当命令错误的时候,整个程序就会全挂掉了。shell是一个单进程。shell 创建子进程来执行命令。

在命令行中输入命令,是如何解析的?谁解析的?如何传递给目标子进程的?

shell来帮助我们做解析,形成一张表,然后在进行程序替换的时候,直接调用系统的execvp()函数来执行我的程序,并把这张表传递给我自己对应的进程。

   89 bool ExecuteCommand()    // 4. 执行命令90 {91     pid_t id = fork(); // 创建子进程92     if(id < 0) return false;93     if(id == 0)94     {95         // child96         // 1.执行命令 调用系统函数97         execvp(gargv[0], gargv);98         // 2.退出99         exit(1);100     }101     // father102     int status = 0;103     pid_t rid = waitpid(id, &status, 0); 104     if(rid > 0)105     {106         return true;107     }108     return false;109 }

五、内建命令

每个进程都会有一个当前路径的概念,改变当前路径【chdir】,[cd ..] 就会改变当前工作路径【改变子进程的工作路径】。

当我们在自己写的shell中,我们 [cd ..] 应该改变的是子进程的路径还是自己写的shell进程的路径?

改变shell的工作路径【shell本身就是父shell的一个子进程】,因为子进程执行完命令就会退出,shell当前的工作路径根本就没有改变;改变shell的工作路径,之后执行的命令都会使用shell当前的路径。即:在shell中,有些命令必须由子进程来执行,有些命令不能由子进程执行,要由shell自己执行 --- 内建命令。

我们自己所在的cwd是在进程的PCB当中的,pwd查找的时候也是在PCB里面查找的,PWD是一个环境变量,所以当路径发生变化的时候,要更新一下环境变量。

  114 void AddEnv(const char *item)115 {116     int index = 0;117     while(genv[index])118     {119         index++;120     }121     // 指向为空的下标122     genv[index] = (char*)malloc(strlen(item)+1);// 不能使用局部变量,要重新申请          123     strncpy(genv[index], item, strlen(item)+1);                                  124                                                                                  125     genv[++index] = nullptr;                                                     126 }                                                                                   127                                                                                     128 bool CheckAndExecBuiltCommand()                                                     129 {                                                                                   130     if(strcmp(gargv[0], "cd") == 0)                                                 131     {                                                                               132         if(gargc == 2)                                                              133         {                                                                           134             chdir(gargv[1]);                                                        135         }                                                                           136         return true;                                                                137     }                                                                               138     else if(strcmp(gargv[0], "export") == 0)// 导入环境变量                  139     {                                                                        140         if(gargc == 2)                                                       141         {                                                                    142             AddEnv(gargv[1]);                                                143         }                                                                    144         return true;                                                         145     }                                                                        146     else if(strcmp(gargv[0], "env") == 0)                                    147     {                                                                        148         for(int i=0; genv[i]; i++)                                           149         {                                                                    150             printf("%s\n",genv[i]);151         }          152         return true;153     }              154     return false;155 }

我们虽然改变了工作路径,但是命令行提示符并没有发生改变,这是为什么呢?

🌠 小贴士: 

• 环境变量是由shell自己维护的,即当路径发生变化时,环境变量表需要由shell自己取更新。

• 我们可以更改环境变量的原因:shell支持用户自己去更改,shell本来就是为用户服务的。

shell不是从0开始读配置文件的,它是从系统额shell直接启动的,所以我们写的shell启动的时候,它默认继承的是系统所对应的环境变量。

• ./myshell 它的环境变量,根本就没有维护环境变量表,它的环境变量其实是从(父进程)系统的shell直接继承的。

• 环境变量表的指针数组,默认是在系统的shell的全局数组给我们维护好的,它也是一张全局的表。

• putenv() 实则就是在父进程所对应的全局指针数组里面,找到一个没有使用的位置,把这个环境变量加进来。

• 导入到系统的shell里面,并不是子进程修改父进程的表,因为当发生修改时,会发生写时拷贝,当子进程不修改,父子进程就是共享的,一旦子进程进行修改,操作系统就会对父shell的指针数组进行写诗拷贝【把整张表全部拷贝一份给子进程】,此时父子进程的环境变量表就是分开的。

• getcwd(字符串,大小);获取当前工作路径【系统级接口,直接从进程的PCB里面拿】。

• 不能用子进程来导入环境变量,子进程不能影响当前的shell。

• 内建命令无法被子进程继承,环境变量可以,所以环境变量具有全局性。

   36 string GetPWD()37 {         38     //string pwd = getenv("PWD");39     //return pwd.empty() ? "None" : pwd;40           41     // 系统调用42     // 1. 获取当前工作路径43     if(getcwd(pwd, sizeof(pwd)) == nullptr) return "None";44           45     // 2. 更新环境变量46     snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);47     putenv(pwdenv);// PWD=XXXX48     return pwd;                                                                          49 }

 六、环境变量 

我们在自己写的shell里面创建子进程,这个子进程继承的环境变量表是继承谁的?我自己写的shell的环境变量表?

自己写的shell 和 自己写的shell创建的子进程 的环境变量表,都是继承父shell的

那我们如何让自己写的shell 创建的子进程继承的环境变量表是自己写的shell的环境变量表呢?

父shell的环境变量表是从系统的配置文件里面来的【shell脚本】,我们从系统里把系统的环境变量拷一份到我们自己的shell里面,模拟一下从配置文件里面去读。

  const int envnum = 64; 
// 自己维护的环境变量表char *genv[envnum];void InitEnv(){// 从父shell获取环境变量extern char **environ;int index = 0;while(environ[index]){// 导入环境变量,实则就是向shell自己的环境变量表当中,进行插入一个新的环境变量// 即 再malloc一段空间,让指针数组指向它对应的那个位置genv[index] = (char*)malloc(strlen(environ[index]+1));strncpy(genv[index], environ[index], strlen(environ[index])+1);index++;}genv[index] = nullptr;}

所以当我们执行命令的时候,我们要把自己的环境变量参数传给系统去执行,因此 在执行一个新的程序时,把命令行参数表、环境变量表都传给execvpe() 系统调用,程序执行时,就能获得这两个参数。

  101 bool ExecuteCommand()    // 4. 执行命令102 {103     pid_t id = fork(); // 创建子进程104     if(id < 0) return false;105     if(id == 0)106     {107         // child108         // 1.执行命令 调用系统函数109         //execvp(gargv[0], gargv);110         execvpe(gargv[0], gargv, genv);                                                  111         // 2.退出               112         exit(1);                113     }                           114     // father                   115     int status = 0;             116     pid_t rid = waitpid(id, &status, 0);117     if(rid > 0)                 118     {                           119         return true;            120     }                           121     return false;               122 }                               

七、总结

1、在命令行中,一个命令是如何执行的?对于普通命令来讲,就是解析命令行,然后通过exec*系列的函数接口,进行fork()程序替换。

2、命令行参数是从命令行依次获取的,被shell自己解析自己维护。

3、环境变量表是从系统的配置文件读取出来的,由shell自己维护,维护好就是一个全局的指针数组,通过 execvpe() 接口函数来调用,这个系统的调用接口把环境变量传递给所有的子进程。

4、echo命令,是内建命令。系统除了维护环境变量表、命令行参数表,还维护了一张本地变量表,这张本地变量表无法通过 exec* 这样的接口传递给子进程,所以子进程看不到。在本地变量的这张表,让shell自己去维护,在echo的时候,就去打印。本地变量表属于在shell自己内部维护的全局数据当中的一个字符串。

八、完整代码

  1 #include <iostream>2 #include <cstdio>3 #include <cstdlib>4 #include <cstring>5 #include <string>6 #include <unistd.h>7 #include <sys/types.h>8 #include <wait.h>9 10 using namespace std;11 12 const int basesize = 1024;13 const int argvnum = 64;14 const int envnum = 64;15 16 // 全局的命令行参数表17 char *gargv[argvnum];// 全局的环境变量表18 int gargc = 0; // 记录环境变量的个数19 20 // 自己维护环境变量表21 char *genv[envnum];22 23 // 系统的环境变量默认是从配置文件来的【shell脚本】,24 // 从系统里把系统环境变量拷一份到我的环境变量里面,模拟一下从配置文件里面去读25 26 // 全局的当前shell工作路径 27 char pwd[basesize];28 char pwdenv[basesize];29 30 string GetUserName()31 {32     string user = getenv("USER");33     return user.empty() ? "None" : user;34 }35 36 string GetHostName()37 {38     string hostname = getenv("HOSTNAME");39     return hostname.empty() ? "None" : hostname;40 }                                                                                          41 42 string GetPwd()43 { 44     // 获取当前的工作路径不能从环境变量里面获取,因为环境变量要更新45     // string pwd = getenv("PWD"); 46     // return pwd.empty() ? "None" : pwd;47  48     // 系统调用49 50     // 直接从系统里面获取环境变量51     // 获取完之后,把环境变量更新                                                          52     // getcwd(字符串,大小) 获取当前工作路径【系统级接口,直接从进程的PCB里面拿】被封装的接    口gunc封装的接口53     // 失败返回值为NULL,成功就把当前工作路径写到buff里面54     55     // 1、获取当前工作路径56     if(nullptr == getcwd(pwd, sizeof(pwd))) return "None"; // 获得当前工作路径57     58     // 2、更新环境变量59     snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);60     putenv(pwdenv); // PWD=XXXX61     return pwd;62 63 // 环境变量是由shell自己维护的,即当路径发生变化时,环境变量表需要由shell自己去更新64 // 我们可以更改环境变量的原因:shell支持用户自己去更改,shell本来就是为用户服务的65 // shell不是从0开始读配置文件的,它时从系统的shell直接启动的,66 // 所以我们写的shell启动的时候,它默认继承的是系统所对应的环境变量67 // ./myshell它自己的环境变量,根本就没有维护环境变量表,68 // 它的环境变量其实是从(父进程)系统的shell直接继承69 //70 // getenv 71 //72 // 环境变量表的指针数组,默认是在系统的shell的全局数组给我们维护好的,它也是一张全局的表73 // putenv()实则就是在父进程所对应的全局指针数组里面,74 // 找一个没有使用的位置,然后把这个环境变量加进来75 //76 // 导入到系统额shell里面,并不是子进程修改父进程的表,77 // 因为发生修改时,会发生写时拷贝机制,78 // 当子进程不修改,父子进程时共享的,一旦子进程进行修改,79 // 操作系统就会对父shell的指针数组进行写时拷贝。【把整张表全部拷贝一份给子进程】80 // 此时父子进程的环境变量表就是分开的81 82 }83 84 string MakeCommandLine()85 {86     char command_line[basesize];// 输入的命令行长度87     // snprintf(写入到指定的缓冲区里,指定长度,按照指定的格式)88     snprintf(command_line, basesize, "[%s@%s %s]# ",\89             GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());// c_str() 为C风格的字符串90         return command_line;91 }92 93 void PrintCommandLine()    // 1.命令行提示符94 {95     printf("%s",MakeCommandLine().c_str());96     fflush(stdout);                                                                        97 }98          99 bool GetCommandLine(char command_buffer[], int size)      // 2.获取用户命令
100 {
101     // 从键盘当中获取出来的字符串,放到command_buffer缓冲区里面
102     // 将用户输入的命令行,当作完整的字符串
103     // "ls -a -l -n" 包括空格
104     // fgets(获取字符串, 指定大小, 从特定的文件流当中获取) 获取失败返回null,获取成功返回获    取成功的字符串的起始地址
105     char *result = fgets(command_buffer, size, stdin);
106     if(!result)
107     {
108         return false;
109     }
110     command_buffer[strlen(command_buffer)-1] = 0;// 最后输入的回车,把回车的字符改为0,字符    串为空,即输入就是纯净版的没有换行
111     if(strlen(command_buffer) == 0) return false; // 若输入为空,就终止
112     return true;
113 }
114 
115 void ParseCommandLine(char command_buffer[], int len)    // 3.分析命令
116 {
117     (void)len;
118     // 解析出来的命令,放到全局的环境变量表当中
119     // strtok(一个字符串分多个,分隔符) 返回值:按照该分隔符从源字符串里切出来的第一个区域
120     // strtok(str, " ");      如果切割有效的字符串,返回具体的字符串的第一个地址,
121     // strtok(nullptr, " ");    否则,返回nullptr,即切到最后没有了,自动返回nullptr
122     // "ls -a -l -n"
123     memset(gargv, 0 , sizeof(gargv)); // 初始化/清空环境变量表里面的内容
124     gargc = 0;
125 
126     const char *sep = " ";
127     // 把首个子字符串拿出来
128     gargv[gargc++] = strtok(command_buffer, sep);// 放到全局的环境列表里面
129     
130     // =是刻意写的  循环放入环境变量表中
131     while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
132     gargc--;
133 }
134 
135 void debug()                                                                               
136 {
137     printf("argc: %d\n",gargc);
138     for(int i=0; gargv[i]; i++)
139     {
140         printf("argv[%d]: %s\n", i, gargv[i]);
141     }
142 }
143 
144 bool ExecuteCommand()      // 4.执行命令
145 {
146     // shell 不能自己执行这个命令,如果shell可以自己执行命令,当命令错误的时候就全挂掉了,
147     // shell是一个单进程的。
148     // 让子进程执行命令
149     pid_t id = fork();// 创建子进程
150     if(id < 0) return false;
151     if(id == 0)
152     {
153         // child
154         // 1. 执行命令
155         // exec*
156         // execvp(gargv[0], gargv);
157         // 在命令行当中输入的命令,是如何解析的?谁解析的?如何传递给目标子进程的?
158         // shell来帮我们做解析,形成一张表,然后在进行程序替换的时候,
159         // 直接以execvp的形式执行起来我的程序,并把这张表传递给对应的自己的进程
160         
161         // 如果我们在自己写的shell里面创建子进程,
162         // 那这个子进程继承的环境变量表是父shell的,自己写的shell也是继承父shell的环境表
163         // 那我们如何让我们自己写的shell,创建的子进程继承的环境变量表是我自己写的shell的环    境变量表呢?
164         execvpe(gargv[0], gargv, genv);// 把自己写的环境变量参数传给它
165         // 在执行一个新的程序时,把命令行参数表、环境变量表都传给execvpe系统,
166         // 所以自己的程序是父进程通过系统调用,把这两个参数交给了你的程序,
167         // 程序执行时,就能获得这两个参数
168         
169 
170 
171         // 2. 退出
172         exit(1);
173     }
174     
175     // father
176     int status = 0;
177     pid_t rid = waitpid(id, &status, 0);
178     if(rid > 0)
179     {
180         // Do Nothing                                                                      
181         return true;
182     }
183     return false;
184 }
185 
186 void AddEnv(const char *item)
187 {
188     int index = 0;
189     while(genv[index])
190     {
191         index++;
192     }
193     // 指向为空的下标
194     genv[index] = (char*)malloc(strlen(item)+1);// 不能使用局部的变量,要重新申请
195     strncpy(genv[index], item, strlen(item)+1);
196 
197     genv[++index] = nullptr;
198 }
199 
200 // shell自己执行命令,本质是shell调用自己的函数
201 bool CheckAndExexcBuiltCommand() // shell调用自己的内部函数,把自己的路径发生变化
202 {
203     if(strcmp(gargv[0], "cd") == 0)
204     {
205         // 内建命令
206         if(gargc == 2) // 
207         {
208             // 系统接口
209             chdir(gargv[1]); // 切换路径
210         }
211         return true;
212     } 
213     else if(strcmp(gargv[0], "export") == 0)
214     {
215         // export也是内建命令
216         if(gargc == 2)
217         {
218             AddEnv(gargv[1]);
219         }
220         return true;
221     }
222     else if(strcmp(gargv[0], "env") == 0)                                                  
223     {
224         for(int i=0; genv[i]; i++)
225         {
226             printf("%s\n",genv[i]);
227         }
228         return true;
229     }
230     return false; 
231 }
232 
233 // 从系统里把系统环境变量拷一份到我的环境变量里面,模拟一下从配置文件里面去读
234 // 作为一个shell,获取环境变量应该从系统的配置文件来获取
235 // 我们现在是直接从父shell中获取环境变量
236 void InitEnv()
237 {
238     // 从父进程获取环境变量
239     extern char **environ;
240     int index = 0;
241     while(environ[index])
242     {
243         // 导入环境变量,实则就是向shell自己的环境变量表当中,进行插入一个新的环境变量
244         // 即 再malloc 一段空间,让指针数组指向它对应的那个位置
245         genv[index] = (char*)malloc(strlen(environ[index]+1));
246         strncpy(genv[index], environ[index], strlen(environ[index])+1);
247         index++;        
248     }
249     genv[index] = nullptr;
250 }
251 
252 int main()
253 {
254     InitEnv();
255     char command_buffer[basesize];// 命令行缓冲区大小
256     while(true)
257     {
258          PrintCommandLine();    // 1.命令行提示符
259          
260          // command_buffer --> 输出型参数
261          if(!GetCommandLine(command_buffer, basesize))      // 2.获取用户命令
262          {  // 依次获取每个子字符串
263              continue;// 获取到空格继续获取
264          }
265          //printf("%s\n",command_buffer);
266 
267          // "ls -a -l -n" --> "ls" "-a" "-l" "-n" 拆成一个一个的子字符串                   
268          ParseCommandLine(command_buffer, strlen(command_buffer));    // 3.分析命令
269          //debug();
270 
271          // 先判断该命令是否是内建命令
272          if(CheckAndExexcBuiltCommand())
273          {
274              continue;
275          }
276          
277          ExecuteCommand();      // 4.执行命令   
278     
279     }
280 
281     return 0;
282 }

http://www.mrgr.cn/news/81600.html

相关文章:

  • 牛客周赛73B:JAVA
  • 推荐一个C#轻量级矢量图形库
  • 2024大模型在软件开发中的具体应用有哪些?(附实践资料合集)
  • 基于单片机的智能递口罩机器人设计
  • CoinShares预测2025年加密市场前景看涨
  • QT--信号与槽
  • 数据库概论
  • 将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
  • SpringCloudAlibaba实战入门之路由网关Gateway初体验(十)
  • 【可靠有效】springboot使用netty搭建TCP服务器
  • 《机器学习》从入门到实战(1)
  • 《机器学习》——KNN算法
  • QT集成intel RealSense 双目摄像头
  • 新浪微博C++面试题及参考答案
  • 细说EEPROM芯片24C02的基础知识及其驱动程序设计
  • 【达梦数据库】小版本升级之bin文件替换
  • 是德 皮安表Keysight B2980 系列常用指令 附带说明书原件
  • E-commerce .net+React(一)——项目初始化
  • Java数组深入解析:定义、操作、常见问题与高频练习
  • 高性能编程,C++的无锁和有锁编程方法的性能对比
  • 2023 年 12 月青少年软编等考 C 语言四级真题解析
  • 字节跳动Java开发面试题及参考答案(数据结构算法-手撕面试题)
  • Anaconda搭建Python虚拟环境并在Pycharm中配置(小白也能懂)
  • 【物联网技术与应用】实验16:模拟霍尔传感器实验
  • YOLOv9-0.1部分代码阅读笔记-detect.py
  • 高精度问题