【Linux】静态库和动态库
动静态库
- 一、静态库
- 1. 静态库概念
- 2. 制作静态库
- (1)朴素方法 --- 不打包
- (2)对静态库打包
- 3. 使用静态库
- (1)朴素方法 --- 直接使用
- (2)使用打包好的静态库
- 二、动态库
- 1. 动态库概念
- 2. 制作动态库
- 3. 使用动态库
- (1)头文件和库文件安装到系统中
- (2)软链接
- (3)环境变量
- (4)更改关于动态库的配置文件
- 三、动态库加载
- 1. 引入概念
- 2. 理解动态库加载
- 3. 程序的运行过程
一、静态库
1. 静态库概念
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
2. 制作静态库
(1)朴素方法 — 不打包
下面我们实现两个简单的方法,加法和减法的计算方法,使用头文件和源文件分开的形式呈现出来,例如:
那么这一堆头文件和源文件该如何形成一个静态库给别人使用呢?注意,我们打包的库中是没有 main 函数的,我们也不能把 main 函数打入库中。
接下来我们在该目录下创建一个测试以上方法的主函数 TestMain.c:
假设现在我们需要将上面的所有文件形成一个可执行程序测试,该如何编译呢?使用 gcc!如:
如上图,为什么我们在编译的时候没有编译头文件呢?因为头文件在当前路径下,所以编译器 gcc 是可以直接找到的!
那么对于上面形成的可执行程序,我们也是可以正常运行的,如下:
其实实际上,我们形成可执行程序,我们不建议把所有的源文件直接全部编译,因为这样的话每一个源文件都要经过预处理、编译、链接等。所以一般情况下对于这种多文件项目时,要先把源文件编译成 .o
文件,然后再把所有的 .o
文件进行链接,形成可执行程序!
所以我们先创建一个 Makefile,进行对上面的实践:
如上图,我们解释一下各项的作用。首先第1、2行不需要解释,因为这是我们常用的语法,但是所有的 .o
文件在当前目录下并不存在,所以还需要形成下面的依赖关系。对于 %.o:%.c
,首先 %.c
就是将当前路径下的所有 .c
文件一个一个展开,经过 gcc -c $<
形成一个一个同名的 .o
文件。其中 $^
和 $<
的区别在于,$^
是将冒号右边的所有文件看作一个整体形成冒号右边的程序;而 $<
是将冒号右边的一个一个分开编译,形成一个一个的冒号右边的程序。
接下来我们编译一下,就形成了一堆的 .o
和可执行程序:
所以我们想要形成一个库,我们就需要将上面的所有源文件和 main 函数和 Makefile 全部删除,将剩下的所有 .o
和头文件打包即可。然后使用者就可以将它自己写的 main 函数编译成 .o
文件,和我们的 .o
文件一链接即可!
接下来我们将代码逻辑修改一下,首先新建一个目录 user 将用户写的主函数放进去:
接下来我们形成 .o
文件和头文件给到 user,我们暂时先不将这些文件进行打包:
所以使用者就可以自己将 main 函数形成 .o
文件再和我们的文件链接即可使用:
但是这样对于用户来说太麻烦了,所以我们需要将所有的 .o
文件打包生成库。首先现在我们先需要生成静态库,而生成静态库的命令为(假设以我们上面的文件打包为例):
ar -rc libmylib.a Add.o Sub.o
其中 ar
命令是将所有的 .o
文件形成库文件的过程;选项 -rc
代表如果 .o
文件存在则替换,不存在则创建。而 libmylib.a 是静态库,库要以 lib 开头,所以我们的库的真正名字是 mylib.
下面我们使用 Makefile 生成一个静态库:
static_lib=libmylib.a$(static_lib):Add.o Sub.oar -rc $@ $^%.o:%.cgcc -c $<.PHONY:cleanclean:rm -f *.o *.a
如上图,首先我们为该静态库的名字设置一个变量 static_lib
,然后下面使用 .o
文件生成该静态库。
所以我们得出结论:静态库的本质就是将库中的源代码直接翻译成 .o
目标二进制文件,然后打包!
(2)对静态库打包
下面我们对静态库和头文件分别进行打包,对 Makefile 进行修改,如下:
static_lib=libmylib.a$(static_lib):Add.o Sub.oar -rc $@ $^%.o:%.cgcc -c $<.PHONY:outputoutput:mkdir -p mylib/includemkdir -p mylib/lib cp -f *.h mylib/includecp -f *.a mylib/lib .PHONY:cleanclean:rm -rf *.o *.a mylib
我们多加了一个伪目标,就是用来发布我们的静态库的,本质就是创建一个目录,把头文件放入 include 中,把库文件放入 lib 中。我们直接发布,会形成一个库:
我们使用 tree 查看一下该库:
生成了对应的静态库之后,我们需要给别人使用,所以我们也可以对该库进行打包:
然后将该打包的文件给别人即可。
3. 使用静态库
(1)朴素方法 — 直接使用
上面我们可以形成静态库了,那么我们该如何使用别人的静态库呢?现在我们回到用户的角度,我们只有一个主函数:
我们现在需要用到库中的方法,直接编译是会报错的,因为我们还没有对应的库。所以我们先使用最朴素的方法,先不进行打包。我们将所有的头文件给到用户:
还需要将对应的静态库给到用户:
如上,我们对应的头文件和静态库都有了,所以我们尝试编译一下:
我们发现出现了链接错误,这是为什么呢?现在我们需要知道,我们自己写的库或者别人写的库,叫做第三方库,而 gcc 默认不认识!所以我们需要在 gcc 中加上 -l
选项,让编译器去链接指定的库!如下:
gcc TestMain.c -l mylib
其中,-l
后面带的是库的真正的名字,即去掉前面的 lib 和后缀 .a,我们尝试链接一下:
但是我们发现它还是找不到库。所以我们还需要加上一个选项 -L
,后面跟上该库的路径,如下:
如上图,我们就生成了可执行程序。
但是为什么我们以前生成可执行程序的时候,不需要指定库名称和库路径呢?因为 gcc 就是默认处理C语言的,所以C标准库不需要指定链接哪个库还有路径,它自身就会帮我们找到并链接。
接下来我们使用 ldd
查看该可执行程序依赖的库文件:
如上图,为什么我们的程序没有依赖到 mylib 的库呢?那是因为我们的可执行程序默认是动态链接的,ldd 是只能查动态库的!而静态库已经被拷贝到可执行程序里了!gcc 默认是动态链接的,但个别库,如果我们只提供 .a,gcc 也会局部性的把我们指定的 .a 进行静态链接,其它库正常动态链接,如果加上 -static 选项,gcc 就只能链接 .a。
(2)使用打包好的静态库
我们在上面已经生成了一个打包好的静态库,现在我们将该压缩文件拿到用户这里:
然后对该压缩文件进行解压:
如上,我们就把静态库拿到手了。然后我们就可以将该库安装一下,怎么安装呢?系统搜索头文件默认是在 /usr/include
这样的目录下的;而库文件一般都是在 /lib64
目录下的;所以我们将第三方库安装,本质就是将所有的头文件拷贝到 /usr/include
路径下;将所有的库文件拷贝到 /lib64
路径下!
但是我们也可以选择不这样做,我们还有另一种方法,我们可以直接使用。首先我们先看看直接编译会有什么问题:
首先出现的问题是头文件找不到的问题,有一种方法可以直接在代码中使用头文件时带上路径,例如 #include "mylib/include/Add.h"
,但是我们不选择这样做,因为这样不太好。那么现在我们的头文件既不在当前目录下,也不在系统路径下,没关系,我们可以在 gcc 中带上选项 -I
,后面带上头文件的路径即可,意思就是告诉 gcc 编译器除了在上面两个路径下找之外,还需要在我们指定的路径下找!那么我们尝试编译一下:
那么现在就不会报头文件的错误了,而是链接报错了。那么这个报错我们在上面已经解决过了,只需要带上 -l
指定库名称和带上 -L
指定库文件的路径即可,如下:
gcc TestMain.c -I mylib/include/ -l mylib -L mylib/lib/
如上,我们就能使用别人制作的静态库了。如果我们将头文件和库文件都安装到系统中了,-I
和 -L
就不需要带了。
二、动态库
1. 动态库概念
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
2. 制作动态库
首先我们需要介绍一下生成动态库使用的指令是 gcc,带上 -shared
选项即可。另外在生成 .o
文件的时候,需要带上 -fPIC
选项,意思是产生位置无关码,这个我们后面再解释。动态库的库名规则:libxxx.so。
接下来我们修改一下 Makefile 文件,如下:
dynamic_lib=libmylib.so $(dynamic_lib):Add.o Sub.ogcc -shared -o $@ $^%.o:%.cgcc -fPIC -c $<.PHONY:outputoutput:mkdir -p mylib/includemkdir -p mylib/lib cp -f *.h mylib/includecp -f *.so mylib/lib .PHONY:cleanclean:rm -rf *.o *.so mylib
接下来我们编译一下:
如上图,就多了一个动态库文件,接下来我们 make output 发布起来,当前目录就会生成一个动态库,我们可以 tree 查看一下:
如上,头文件就包含在 include 中;库文件就包含在 lib 中。
接下来我们就可以将该动态库给别人使用了,现在我们将该动态库拷贝到 user 目录下:
现在别人就可以使用我们的库了。
3. 使用动态库
接下来我们按照使用静态库的方式尝试使用动态库,首先先生成可执行程序:
接下来我们开始运行:
我们会发现,报错了,报的是不能打开该动态库,找不到该文件或目录。这是为什么呢?我们不是将路径和库名称都告诉 gcc 了吗?
首先动态库是可执行程序和库分离开的,我们的可执行程序加载到内存中了,但是库还没有加载到内存中。而静态库是直接拷贝到可执行程序中的,所以它们会被一起加载到内存中。也就是说,动态链接非常依赖这个动态库!
而我们在上面将路径和库名称都告诉了编译器,但是程序已经形成了,编译器的工作周期已经结束了,接下来运行的时候,和编译器就没有关系了!那么接下来就和系统有关系了,所以当我们加载运行的时候,我们也要告诉系统动态库在哪里!
我们可以使用 ldd
观察一下:
我们发现我们的动态库是找不到的。
所以解决方法有如下几种:
(1)头文件和库文件安装到系统中
既然在系统默认的搜索路径下找不到我们的库文件和头文件,我们就将它们拷贝到系统的默认搜索路径中。
先拷贝头文件:
sudo cp mylib/include/*.h /usr/include/
再拷贝库文件:
sudo cp mylib/lib/*.so /lib64
然后我们在系统的目录中可以查看一下我们拷贝的文件:
如上图,我们的库文件和头文件已经拷贝到系统中了。接下来我们可以重新生成可执行程序,现在我们就不需要带路径了,只需要指定库名称即可:
然后我们也可以直接运行可执行程序了:
ldd 也能看到它能找到对应的库了:
(2)软链接
我们还可以通过在当前目录下建立软链接的方式找到库文件,从而执行可执行程序。如下:
ln -s mylib/lib/libmylib.so libmylib.so
我们发现,当前路径下的软链接是可以被找到的,那么就说明,动态链接默认会在当前路径下搜索库文件!
ldd 查看:
(3)环境变量
我们知道,系统在运行的时候会去帮我们找我们的库,去哪里找呢?除了系统默认库路径下去找,还会去 LD_LIBRARY_PATH
加载库的环境变量中去找!
所以我们将库文件所在的路径添加到 LD_LIBRARY_PATH
中即可。接下来我们尝试一下,首先我们需要找到该库对应的路径:
系统是知道我们需要链接哪一个库的,只是找不到它在哪里,所以只需要给它所在的路径即可,不需要包含库名字了。接下来我们导一下环境变量,再查看 LD_LIBRART_PATH
下的环境变量就可以找到了:
添加之后是即时生效的,这时候系统就知道我们的库的路径了:
接下来我们的可执行程序也就可以执行了:
但是当前这种方法导环境变量是内存级的,当我们退出后重新进入它就没了,所以我们想永久生效的话还需要去改有关环境变量的配置文件,这里就不再介绍了。
(4)更改关于动态库的配置文件
在系统中存在一个 /etc/ld.so.conf.d/
这样的一个配置文件目录,这是系统管理所有系统动态库加载相关的配置文件。如下:
我们可以任意查看一个文件内部的内容是什么:
我们会发现,它里面的内容只有一个路径,就是我们需要查找的动态库所对应的路径!所以我们想要自己的动态库永久有效,只需要在 /etc/ld.so.conf.d/
目录下创建一个文件,在该文件中写入我们动态库的路径即可!
下面我们尝试一下,现在 /etc/ld.so.conf.d/
目录下新建文件:
sudo touch /etc/ld.so.conf.d/temp.conf
然后进入该文件添加我们动态库的路径:
如上,我们添加成功,一般来说也是即时生效的。如果没有生效,我们使系统在加载配置上进行刷新:
sudo ldconfig
此时我们查看的时候就生效了:
此时可执行程序也可以运行了:
所以,我们从网上下载的第三方库的原理就是将一系列的头文件和库文件拷贝到系统的默认搜索路径下!
另外,如果别人给我们的库中既包含动态库也包含静态库,即同一个库中提供动静态两种库,gcc 默认使用动态库!
- 使用 Makefile 生成多个可执行程序
上面我们都是使用 Makefile 生成一个动态库和一个静态库,接下来我们要使用 Makefile 一次性生成动态库和静态库,下面直接参考 Makefile 文件:
static_lib=libmylib.adynamic_lib=libmylib.so .PHONY:allall:$(dynamic_lib) $(static_lib)$(static_lib):Add.o Sub.oar -rc $@ $^%.o:%.cgcc -c $<$(dynamic_lib):Add.o Sub.ogcc -shared -o $@ $^%.o:%.cgcc -fPIC -c $<.PHONY:outputoutput:mkdir -p mylib/includemkdir -p mylib/lib cp -f *.h mylib/includecp -f *.a mylib/lib cp -f *.so mylib/lib .PHONY:cleanclean:rm -rf *.o *.so *.a mylib
三、动态库加载
1. 引入概念
我们上面在形成动态库时,还有一个问题没有讲,那就是 gcc -fPIC -c xxx.c
中的 -fPIC
选项,它的意思是与位置无关码,到底是什么意思呢?接下来我们需要了解一下。
首先我们要知道,在 Linux 下,形成的可执行程序是 ELF 格式的可执行程序,它其中包含有一张类似于符号表的东西,里面包含各种函数依赖的库以及地址,符号表就是动态链接这些动态库的。当我们需要将可执行程序加载到内存中时,动态链接的程序,不光光自己要加载,链接的库也要加载到内存中!
然后我们要知道,程序没有被加载到内存的时候,程序内部有地址吗?有的!当我们的程序编译成为二进制文件之后,变量名、函数名等,还有吗?没有了!编译的时候,对代码进行编址,基本遵守虚拟地址空间那一套!虚拟地址空间不仅仅是操作系统里面的概念,编译器编译的时候,也要按照这样的规则编译可执行程序,这样才能在加载的时候,进行从磁盘文件到内存,再进行映射。所以,在程序没有被加载到内存的时候,就已经具有了“虚拟地址”,通俗地说是逻辑地址,这种逻辑地址的概念就是基地址+偏移量。这种以基地址从0开始的我们把它叫做平坦模式。
那么基地址+偏移量就是可以确定一个段的,比如说代码区基地址从0开始,偏移量是200,那么 [0, 200] 这个段就是代码区。所以这个可执行程序中就会形成许多段,它就是采用偏移量的不同这种方式定位每个段的。
在计算机里面,对程序进行编址的时候,会有两种编址方式,一种是绝对编址,另一种是相对编址。
绝对地址比较依赖起始位置,比如说我们当前站在距离一棵树的20米处,我们的起始位置是0,那么绝对位置就是我们相对于起始位置的距离,但是我们的位置会因为起始位置的变化而发生变化。但是相对地址就是我们的位置相对于这个树的距离,当起始位置发生变化的时候,我们的位置相对于树也就没有变化,这就是相对地址。
绝对编址比较适合我们上面说的那一套可执行执行的逻辑地址;而相对编址比较适合形成库中函数的地址,因为库中我们把函数的地址形成之后,所有库中的函数里面只需要记录它自己相比较于库的起始的偏移量是多少,只记录偏移量,所以未来这个库在内存的任意位置加载,库里面的所有函数的地址都不变,所以这就叫做与位置无关码!
2. 理解动态库加载
接下来我们回到地址空间中理解动态库的加载,首先磁盘中有我们的 ELF 可执行程序,可执行程序中的符号表中依赖了 libmylib.so 这样的动态库,如下图:
但是当我们将可执行程序加载到内存中后,我们也需要找到该动态库,数据和代码肯定是被加载到内存中了,而且经过页表映射关系也能建立好。但是动态库也要被加载到内存的,所以动态库被加载至内存后,也要经过页表映射,映射到地址空间中的共享区!所以进程可以通过地址空间找到代码和数据,并且可以在共享区找到动态库中的代码。可以结合下图理解:
也就是说,库被加载后,要被映射到指定使用了该库的进程的地址空间中的共享区部分。那么我们可以保证每次都能将动态库映射到固定的地址空间吗?并不是的。但是我们想做到让库在共享区的任意位置,都可以正确运行呢?
下面我们先了解一下,我们动态库中的方法是如何编址的,其实就是以 库名称+方法偏移量 来确定的。也就是当可执行程序用到动态库中的方法时,它只需要记录在哪个库里面,在这个库的偏移量是多少即可,例如下图:
当可执行程序加载到内存中,代码和数据也加载到内存中后,当执行执行的时候,发现需要用到库中的方法时,也要把该库加载到内存里,然后经过页表映射到进程地址空间中,一旦库加载之后,它在地址空间中的位置就是确定了,我们假设该库加载到地址空间后的地址为 0x1111,那么我们就可以将库中的符号用 0x1111 替换掉,如下图:
所以进程在执行代码的时候,当识别到库中的方法时,该怎么找到库中的方法呢?因为我们已经知道库在地址空间所在的位置,也知道该方法在库中的偏移量,所以就能在地址空间中跳转就可以找到该方法!
所以未来动态库在地址空间中的共享区中随意加载都可以了,因为我们库中的方法编址方式都是相对编址的方式,是相对于该库的偏移量是多少,所以无论该库的地址在共享区中如何变化,偏移量在该库中是不变的,所以我们就能很快地找到对应的方法!
所以动态库采用的就是一种相对编址的方式,然后就可以做到动态库中的与位置无关性,所以以前在 gcc 中形成动态库需要加上 fPIC,形成与位置无关码。
3. 程序的运行过程
首先要知道,CUP 中有一个指令寄存器,当 CUP 需要执行指令时,只需要把正文部分的代码直接读到指令寄存器,然后让 CUP 执行指令。
我们也能从上面的引入概念中知道,程序没有被加载的时候,程序内部就已经有地址了,即在磁盘的时候就已经有虚拟地址了,已经被编译好了。也就是说在磁盘的时候,可执行程序已经将代码、数据编址好了,暂时不考虑动态库,因为牵扯动态链接。
假设我们的可执行程序中 main 函数的起始地址为 0x11111111;然后我们的代码中还调用了其它方法,假设有两个方法,地址分别为 0x2222 和 0x3333,如下图:
而上面的地址可以说是虚拟地址,其实是可以看成是起始地址+偏移量,只是统一编址的时候起始地址从0编址,也就是可以看成是 0: 11111111、0: 2222、0: 3333,我们一般把这种地址叫做逻辑地址,但它实质上还是地址空间上的虚拟地址。
当可执行程序加载到内存后,程序内的地址不变,那么可执行程序的代码加载到内存需要占据物理内存吗?要的,所以它一定要有自己对应的物理地址,所以该可执行程序的代码每一段都需要有自己的物理地址,如下图:
此时加载到内存之后,物理地址有了,那么页表的右侧就可以填上了。更重要的是,ELF 可执行程序会在特定的位置,记录下来自己程序的入口地址 entry;也就是,编译器在编译的时候,可执行程序在符号表中有专门的字段记录 main 函数的地址,供操作系统读取!
那么在程序加载进内存后,首个虚拟地址就有了,就是 main 函数的地址,那么,程序在加载进来的时候又有了物理地址,所以在最开始时,在页表中就可以构建最简单的 k-v 的映射关系。所以当 CUP 执行这个程序的时候,操作系统只需要将 main 函数的地址加载到指令寄存器中,指令寄存器拿到最开始的程序入口地址,它是虚拟地址 0x11111111,然后找到进程,找到进程地址空间,找到页表,进行虚拟到物理的转化,而在上面已经建立好了虚拟地址到物理地址的 k-v 映射关系,所以此时 CUP 就能读取第一条指令了。
然后读取指令时,读到了 call 0x2222,如果后续的代码和数据不在内存里,就重新加载,重新构建映射,因为起始地址都已经有了,剩下的虚拟地址都是连续的!因为正文代码区中的代码都是在一起的!当读取到 call 0x2222 方法,CUP 就读取对应的指令,call 方法就要做函数跳转,因为 0x2222 也是虚拟地址,它也加载到内存中了,所以可以根据页表的映射关系找到对应的 A函数,然后就可以找到代码和数据。所以整个程序运行时,整个虚拟到物理的转化就轮转起来了!
相关文章:

【Linux】静态库和动态库
动静态库 一、静态库1. 静态库概念2. 制作静态库(1)朴素方法 --- 不打包(2)对静态库打包 3. 使用静态库(1)朴素方法 --- 直接使用(2)使用打包好的静态库 二、动态库1. 动态库概念2. …...
LeetCode 0292.Nim 游戏:脑筋急转弯
【LetMeFly】292.Nim 游戏:脑筋急转弯 力扣题目链接:https://leetcode.cn/problems/nim-game/ 你和你的朋友,两个人一起玩 Nim 游戏: 桌子上有一堆石头。你们轮流进行自己的回合, 你作为先手 。每一回合,…...

ctfshow-web1~10-WP
web1 右键查看源码就能看到flag web2 打开网页提示无法查看源代码,右键也使用不了,那我们就在url前面加上view-source: view-source:http://83a83588-671e-4a94-9c6f-6857f9e20c2f.chall.ctf.show/ 访问后即可获得flag web3 右键源码也没看到信息,去查看一下请求头和响应…...

集合问题(并查集)
本题链接:登录—专业IT笔试面试备考平台_牛客网 题目: 样例1: 输入 4 5 9 2 3 4 5 输出 YES 0 0 1 1 样例2: 输入 3 3 4 1 2 4 输出 NO 思路: 这道题关键点在于。 当集合中有一个元素均存在于集合 A 和集合 B 的时…...
Ubuntu文件系统结构
Ubuntu文件系统结构 介绍 Ubuntu是一种备受欢迎的Linux发行版,其文件系统结构以及组织方式是每个使用者和系统管理员都应该了解的重要主题。本篇博客将带您深入探索Ubuntu文件系统的结构,以便更好地理解Linux操作系统的工作原理。 1. 根目录ÿ…...

vue element 组件 form深层 :prop 验证失效问题解决
此图源自官网 借鉴。 当我们简单单层验证的时候发现是没有问题的,但是有的时候可能会涉及到深层prop,发现在去绑定的时候就不生效了。例如我们在form单里面循环验证,在去循环数据验证。 就如下图的写法了 :prop"pumplist. i .device…...
前端开发:入门(一)
当我们开始学习前端开发时,首先接触到的是HTML(超文本标记语言)。HTML是构建网页结构的基础。 1. HTML(超文本标记语言) 介绍和基础语法 HTML,即超文本标记语言,是一种用于创建网页结构的标记…...

简单实验 java spring cloud 自定义负载均衡
1.概要 1.1 说明 这个是在前一个测试上的修改,所以这里只体现修改的内容。前一个测试的地址:检查实验 spring cloud nacos nacos-server-2.3.0-CSDN博客 1.2 记忆要点 1.2.1 引入对象 Autowired DiscoveryClient discoveryClient; 1.2.2 获取服务实…...

简单说说redis分布式锁
什么是分布式锁 分布式锁(多服务共享锁)在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问/操作。 为什么需要分布式锁 在单体应用服务里,不同的客户端操作同一个资源,我们可以通过操作系统提供…...
什么是 Java 中的 IO 和 NIO?它们之间有什么区别?什么是 Java 中的内存管理和垃圾回收?常见的垃圾回收算法有哪些?
什么是 Java 中的 IO 和 NIO?它们之间有什么区别? 在 Java 中,IO(Input/Output)和NIO(New IO)都是用于处理输入输出操作的API。它们之间有以下区别: IO(传统IOÿ…...

【图论】基环树
基环树其实并不是树,是指有n个点n条边的图,我们知道n个点n-1条边的连通图是树,再加一条边就会形成一个环,所以基环树中一定有一个环,长下面这样: 由基环树可以引申出基环内向树和基环外向树 基环内向树如…...
如何快速捕获和验证用户软件需求,实现快速迭代
在软件开发过程中,快速捕获和验证用户需求,以及迅速迭代功能,是保持项目敏捷性和用户满意度的关键。下面将介绍一些建议,帮助你在软件开发过程中更有效地满足用户需求。 1. 深入沟通与用户互动 要捕获用户需求,必须与…...

爱上算法:每日算法(24-2月4号)
🌟坚持每日刷算法,😃将其变为习惯🤛让我们一起坚持吧💪 文章目录 [232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/)思路CodeJavaC 复杂度 [225. 用队列实现栈](https://leetcode.cn/…...
【Node系列】创建第一个服务器应用
文章目录 一、node介绍二、node创建应用三、node创建应用步骤四、相关链接 一、node介绍 Node.js是一个基于Chrome V8引擎的JavaScript运行环境,可以用于构建高性能的网络应用程序。它采用事件驱动、非阻塞I/O模型,使得程序可以以高效地方式处理并发请求…...
Linux命令基础学习 (2月4日打卡
1. ls - 列出目录内容 命令格式:ls [选项] [文件/目录] 常用选项: -l:以详细列表格式显示-a:显示所有文件,包括以.开头的隐藏文件 2. mkdir - 创建新目录 命令格式:mkdir [选项] 目录名 常用选项&…...
Python 基础知识概览
Python是一种简洁、易学、强大的编程语言,广泛应用于各种领域,包括Web开发、数据分析、人工智能等。本文将介绍一些Python的基础知识,帮助初学者建立对这门语言的基本了解。 1. Python 的简介 Python是一种高级、面向对象、解释型的编程语言…...

Adobe Camera Raw for Mac v16.1.0中文激活版
Adobe Camera Raw for Mac是一款强大的RAW格式图像编辑工具,它能够处理和编辑来自各种数码相机的原始图像。以下是关于Adobe Camera Raw for Mac的一些主要特点和功能: 软件下载:Adobe Camera Raw for Mac v16.1.0中文激活版 RAW格式支持&…...

zabbix自定义监控项
zabbix自定义监控项 1.安装zabbix_get软件 [rootchang local]# yum install zabbix-get2.编辑自定义监控项文件 [rootchang ~]# vim /etc/zabbix/zabbix_agentd.d/cpu.conf UserParametercheck_cpu,top -bn 1 -i -c |grep id |cut -d , -f 4 | tr -d id #UserParameter表示…...

使用Pycharm在本地调用chatgpt的接口
目录 1.安装环境 2.建立多轮对话的完整代码(根据自己使用的不同代理需要修改端口(port)) 3.修改代码在自己的Pycharm上访问chagpt的api并实现多轮对话,如果不修改是无法成功运行的。需要确定秘钥和端口以保证正常访…...

HarmonyOS远程真机调试方法
生成密钥库文件 打开DevEco Studio,点击菜单栏上的build, 填一些信息点击,没有key的话点击new一个新的key。 生成profile文件 AppGallery Connect (huawei.com) 进入该链接网站,点击用户与访问将刚生成的csr证书提交上去其中需…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...

C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...

【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...
MySQL 8.0 事务全面讲解
以下是一个结合两次回答的 MySQL 8.0 事务全面讲解,涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容,并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念(ACID) 事务是…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...