Linux - 动静态库(下篇)

前言

在上篇博客当中,对静态库是什么,怎么使用,简单实现自己的静态库,这些做了描述,具体请看上篇博客:
 

本篇博客将会对 动态库是什么,怎么使用,简单实现自己的动态库,这些做描述。

动态库

动态库的创建

 动态库的创建其实说和 静态库是非常类似的,都是先想要把 源程序文件生成 ".o" 为后缀的文件,也就是编译成功过的文件,然后再 进行链接。

只是在链接过程当中链接方式不同。

在静态库当中,我们使用的编译源文件的方式是直接使用 "-c" 选项的方式生产源程序的 ".o" 编译文件。

gcc -c $^   // -c 选项不在后续 $@ 确认生成的目标文件名,默认生成的 与源程序名相同的 .o 文件

但是在动态库当中,我们在上述生成 ".o" 文件使用 "-c" 选项 之外,还需要带上 "-fPIC" (其中 PIC 要大小)选项,这个  fPIC 代表的意思是 产生位置无关码

gcc -fPIC -c xxx.c

而gcc 是默认 优先链接 动态库的,也就是,当 同一个动态库和静态库同时存在的时候,gcc 会优先链接动态库吗,除非是 当前只有 静态库,没有动态库,那么 gcc 没有办法只能使用静态库;又或者是 用户强制要求要用某一个静态库。

所以,静态库在安装的过程当中,需要用户手动去安装,但是对于 动态库,gcc 就可以自己帮助我们实现一个 ".so" 文件。

利用gcc命令,使用 "-shared" 选项,生成一个动态库:
 

gcc -shared -o libxxx.so *.o

上述就是利用在当前目录当中的所有后缀为 ".o" 的文件,打包为一个 xxx 动态库(动态库必须是 "lib" 开头 ,".o" 结尾的文件名)

上述的 "-shared" 选项就是生成一个 分享动态库。


静态库只是该源程序提供代码,本质上也就是提供一些二进制数据,所以,静态库的作用其实就是把源程序需要的代码拷贝到源程序当中,拷贝完之后,源程序当中是如何进行执行的,就和 这个静态库没关系了;静态库是不用被加载到内存当中执行的

 而动态库不一样,当源程序当中执行到 动态库当中的代码的时候,是直接跳转到 动态库当中去执行,所以,注定了 动态库当中的代码是需要被加载到内存当中供 cpu 执行的

从 两 ".a" 和 ".so" 文件的 默认权限你也可以看出,动态库的文件权限当中是带 "x" 可执行的,而 静态库不是。

动态库:

 静态库:所以,上述动态库我们直接执行就会报错:
 

 并不是 这个程序不能执行,而是这个动态库不能单独执行,他需要源程序来调用其中的代码来执行。

而,动态库和静态库不是不能执行,只是不能单独执行,他们都是未来 链接出的 可执行程序当中代码的一部分。


现在我们给出几个源程序文件:

// mylog.c
#include "mylog.h"void Log(const char*info)
{printf("Warning: %s\n", info);
}//mylog.h
#pragma once#include <stdio.h>void Log(const char*);//mymath.c
#include "mymath.h"int myerrno = 0;int add(int x, int y)
{return x + y;
}
int sub(int x, int y)
{return x - y;
}
int mul(int x, int y)
{return x * y;
}
int div(int x, int y)
{if(y == 0){myerrno = 1;return -1;}return x / y;
}//mymath.h
#pragma once#include <stdio.h>extern int myerrno;int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);//myprint.c
#include "myprint.h"void Print()
{printf("hello new world!\n");printf("hello new world!\n");printf("hello new world!\n");printf("hello new world!\n");
}//myprint.h#pragma once#include <stdio.h>void Print();

所以,有了上述动态库创建,和上篇博客当中 对于 静态库的 创建,现在我们在makefile 当中写上对应 生成的目标文件,就可以一键生成的 对应的 动态库 和 静态库了:
 

dy-lib=libmymethod.so   #目标动态库文件的生成
static-lib=libmymath.a  # 目标静态库文件的生成.PHONY:all              #同时生成多个目标文件
all: $(dy-lib) $(static-lib)   all 这个文件依赖于 dy-lib 和 static-lib$(static-lib):mymath.o   # 生成静态库(把 .o 文件打包生成 libmymath.a文件)ar -rc $@ $^
mymath.o:mymath.c        # 由源程序 生成 对应 .o 文件gcc -c $^           $(dy-lib):mylog.o myprint.o  # 生成动态库(把 .o 文件打包生成 libmymethod.so文件)gcc -shared -o $@ $^
mylog.o:mylog.c              # 由源程序 生成 对应 .o 文件gcc -fPIC -c $^    
myprint.o:myprint.c          # 由源程序 生成 对应 .o 文件gcc -fPIC -c $^.PHONY:clean
clean:rm -rf *.o *.a *.so mylib.PHONY:output            # 用于发布的出来 的 生成的目标文件
output:mkdir -p mylib/includemkdir -p mylib/libcp *.h mylib/includecp *.a mylib/libcp *.so mylib/lib

先 make 把对应的 源程序文件 编译生成 ".o" 文件:

下述就是上述生成之后的结果:

   把上述这些  ".o" 文件 打包到 各自的 动静态库 当中:

此时就发布完成了:
 

 上述的这个 lib 文件夹当中的内容,就是我们上述由 各个源程序文件生成的 各自的 .o 文件的动态库,和 静态库的 。

此时,直接使用这种 "#include<mylog.h>" ,然后直接编译的话,他就会提示找不到这个头文件,所以,我们还需要把 这个存储 动静态库 和 各自头文件的 mylib 文件夹,拷贝到 系统默认路径 当中,这种才能使用 这种 "#include<mylog.h>" 方式来引用头文件,和使用其中的代码。

 上述拷贝到 默认 路径当中,其实就是库的安装,这种方式使用最多的,当然,你也可以使用 gcc 命令当中的 "-i"  "-L" "-l" 三个选项来实现。

上述我们就创建了一个 动态库 和 一个静态库。


如何让可执行程序找到动态库(LD_LIBRARY_PATH 环境变量

现在我们来用上述的 动态库,来实现一个 可执行程序,然后编译运行:

在上述,生成的 动态库 和 静态库的基础之上,我们来使用上述的库,来写一个程序,看看能不能编译成功:
 

//main.c#include "mylog.h"
#include "myprint.h"int main()
{Print();Log("hello log function");return 0;
}

上述引用的两个头文件都是动态库当中头文件,也就是说上述源程序当中只引入了 我们上述实现的 动态库。

但是我们发现,虽然我们上述的 动静态库,没有在 系统默认路径下安装,但是我们还用了gcc 当中的三个指令来引入 头文件 和 库;虽然成功生成了 可执行文件,但是,当我们运行这个可执行程序的时候,却报错:找不到我们上述实现的  动态库:

 但是,当我们 file 命令 这个 a.out 可执行文件之时,却告知我们 这个可执行文件是有 动态库链接的

但是在 ldd 命令当中,显示这个 动态链接是 not found 的 :

 但是我们上述在 gcc 命令当中不是已经对 对应的 头文件 和 库名称,和库所在路径做了声明吗?已经告诉编译器了,头文件是什么,什么库,在哪里了。

是的,你已经告诉了编译器了,编译器已经知道了,但是别忘了,动态库是在加载到 内存当中,进行调度执行的,他不像静态库一样单独拷贝一份给对应的源程序当中,拷贝完就不管了;

动态库要加载到内存当中进行调度执行。

所以,上述只是 编译器知道了这个动态库要被执行,也只是编译器知道这个 库是什么,在哪里;但是,系统并不知道。

所以,系统要知道,也就是 加载器 要知道。

 所以,首先你要明白的是,动态库是一个单独的文件,他和 可执行文件是两个文件,我没在执行可执行文件之时,是带上了 路径的,就算没有带上路径,要想成功运行,必须在 PATH 环境变量当中存储的路径下,有这个可执行文件,才能够不带上路径执行。

所以,我们要想执行一个 动态库,就要让 系统找到这个动态库

 第一个方法就是把 这个 动态库拷贝到 C程序默认的系统路径当中,这样,系统就可以在 这个默认路径当中找到这个 动态库,从而执行。

如上所示,拷贝到 lib64 这个目录当中,系统 就可以找到这个 动态库,从而执行。


除了上述 拷贝 的方式,还可以 ln 命令 在上述 lib64 当中创建软链接,链接到 libmymethod.so 这个 动态库 所在的存储路径当中。

 软链接建立结果:

此时他直接就指向了 动态库的存储位置。

如上所示,我们都没有重新对 a.out 重新编译,但是上述使用 ldd 命令却已经可以看到链接 到 libmymethod.so 动态库的链接的结果了。 也可以运行了。


除了上述的 在 默认目录当中进行 拷贝,软链接 的操作,和 系统找命令的可执行程序的 PATH 环境变量一样,上述的 在C当中和 默认 查找 库的也有一个环境变量 --- LD_LIBRARY_PATH

说得更详细一点,LD_LIBRARY_PATH  环境变量当中存储的就是 系统用来搜索动态库的路径 

 

这个环境变量,可能你的系统当中会没有,但是如果你配置过 VIM 的话,可能会有。

如果没有也没关系,可以自己配置一下:

在冒号后面跟上 动态库所在的路径就可以了 ,不需要加上 动态库文件名。 

 因为,需要链接哪一个库,在 gcc 当中 选项当中已经给出了。

此时,我们再 ldd 命令查看 链接情况,发现也是链接成功了的。 


但是,上述我们是修改了 环境变量,但是我们修改的是 bash 的环境变量,因为 当前我们所有的运行的命令,都是bash 的为我们生成的子命令,export 命令是一个内建命令,因为它要修改 bash 的环境变量,所以,export 是不需要创建子进程,而是由 bash 自己的来执行。

但是,修改的终究是 bash 的环境变量,bash 当中的环境变量是在 程序启动之时,由对应的 环境变量生成脚本,一层一层 加载(添加)出来的。

所以,也就是说,如果我们不修改 每一次开机,系统脚本对 bahs 当中的环境变量的添加进行修改的话,那么,当我们关机之后,再次启动,脚本生成的还是之前的 那些环境变量。

所以,要想根本解决上述 由 LD_LIBRARY_PATH 环境变量当中存储的 路径的话,需要修改 生成的 bash 环境变量的脚本。


除了上述方式,还有一个访问可以实现:
在系统当中,有一个目录当中存储的是 后缀为 ".conf" 的文件,这些文件我们称之为 配置文件,在这些文件当中存储的都是路径信息

 这个目录是 :  /etc/ld.so.conf.d

 我们可以随便打开一个看看:

 所以,如果像系统找到 对应 动态库的话,就可以在   /etc/ld.so.conf.d 这个 路径下,创建一个 后缀为 ".conf"  的文件(文件名随便取),在这个文件当中,直接像上述一样,存放 对应 动态库的存储位置(注意此时 不需要带上 库名称)

 然后,很重要的一步,需要使用 ldconfig 命令 ,重新加载一下 上述路径下的配置文件

 上述这种 在 /etc/ld.so.conf.d 路径当中进行修改的操作,只要我们不修改我们对于其中的配置文件,那么这个修改就是永久有效的;

其实只有 上述的第三种使用 LD_LIBPARY_PATH 环境变量这个操作,如果不修改 bash 启动的脚本的话,关机之后就无效了


同样的,如果我们在上述程序当中加上 我们在上篇博客当中实现的静态库的话,只要我们gcc 编译链接成功了,把静态库当中的代码已经拷贝到 可执行程序当中用于链接了,而且此时我们运行程序是可以正常运行的,结果也是正确的。

那么,在之后,不管这个静态库被移动到什么其他位置了,不在之前链接的位置了,或者更加夸张一点,直接把静态库删除;

我们上述生成的可制成程序一样说可以跑的,因为,静态库链接,就是把静态库当中的代码直接拷贝到 对应源程序的当中,成为 源程序的 一部分。

而不像 动态库链接,链接成功之后,动态库的位置是不能移动的,因为编译链接成功之后,不仅仅是编译器在编译之时需要找到这个动态库位置,因为 动态库是要被加载到 内存当中进行执行的。

连接成功后,执行可执行程序,当 可执行程序执行到 需要动态库当中的代码之时, 就需要直接跳转到 动态库 当中执行,此时动态库需要被加载到内存当中,配合 原可执行程序 进行 执行。

所以,动态库在被加载之前,还需要被 系统知道在什么位置存储的,也就是 加载器需要知道 此动态库在什么位置存储的。

至此才能保证 动态库可以被加载到 内存当中进行运行。

如下所示:

在上述代码当中:"mymath.h" 就是上一篇博客当中实现的 静态库。 

上图就在之前 gcc 链接动态库的基础之上,加上 "-lmymath.h" 链接上静态库。 

删除静态库,并且在此执行程序:

 由上图你会发现,当我们删除掉静态库,没有重新编译链接 形成可执行文件,但是,在之前形成的 可执行文件 还是可以运行。没有出错。


当然,不管是静态库 还是 动态库,都是可以被多个 源程序所引用的。但是,由静态库 链接生成 的 多个 可执行文件,如果把此静态库删除掉,这些个 可执行程序还是可以跑的;

但是如果是 由 动态库 链接形成的多个可执行文件,如果是把此动态库删除掉了,那个由此动态库链接的所有 可执行程序就都不能跑了。

如上图所示:我们把上述 main 和 mytest 两个可执行文件 需要的 libmymethod.so 动态库删除了,发现就不能运行了。 

所以,动态库再被系统加载之后,会被所有的 进程所共享!!!

使用了动态链接的方式 形成 的 可执行文件,都是需要系统加载到内存,运行这个 动态库的。

而且,这个动态库被加载到内存,只需要加载一次,因为一次也已经够所有 进程所使用了。这样的话,在系统当中, 就减少了重复的代码。

所以,一个进程,在磁盘(外存)上有自己独立的一份代码,而在 内存当中又有 和其他 进程 有相同的 代码,共享这些相同的代码

所以,我们把 动态库 又叫做 共享库

那么,一个系统当中肯定会加载很多个 动态库到内存当中,操作系统如何管理这些海量的 库呢?

我们说,只要是加载到内存当中,被操作系统所管理的,都离不开 这 六字真言:先描述,再组织 

 先把 管理一个库,所需要的所有属性,都用一个 struct 结构体描述起来(这叫做 先描述对象),然后用某种数据结构,把上述生成的 很多个 struct 结构体都链接起来(这叫做 再组织)。


动态库是如何做到被所有进程共享的?(重谈进程地址空间)

 

如上所示,在每一个进程的 地址空间当中,每一个地址都是一个 虚拟地址,通过 页表映射到 物理内存地址。

 在磁盘当中,每一个进程 独立的 可执行程序,从磁盘当中被加载到 内存当中,通过 页表来映射到 进程地址空间的 代码区当中。

而,如果在 进程 独立的 可执行程序 当中发现,比如是 printf()函数并没有在 可执行程序当中实现,但是,在编译链接之时,又有一个动态库当中有 printf()函数的实现,那么在运行到 printf()函数之时,就会 跳转到 共享区 当中去寻找 printf()实现的虚拟地址,通过页表映射到物理内存当中。

在执行完之后,就会跳回 代码区当中继续执行 这个进程的 代码。

至此,动态库当中的代码就被进程所使用,同样的,上述只是一个进程,同样可以是多个进程。

那么,动态库加载到内存当中的 代码数据,就可以被 多个进程所使用了。

所以,在页表当中建立映射之后,从此往后,进程在执行任何的代码,不管是自己 可执行程序当中的代码,还是 共享区当中的 动态库的共享代码,都是可以在 进程地址空间当中进行 执行的。

 此时,对于动态库 和 自己可知程序当中的代码,就都可以看做是一份代码了,只不过在执行到 动态库当中的代码之时,需要跳转到 动态库当中代码进行执行,只是可能两份代码跳转的比较远罢了。


所以,在操作系统是如何 跳转到共享区当中,找到对应的 共享代码区执行的。其实就是上述所说的,先描述在组织。

操作系统只需要管理好 先描述在组织 的数据结构,就可以管理好其中的 对象,而管理好一个一个的 存储 库 属性的 对象,就可以管理好 一个一个库在内存当中的 使用情况:比如 , 当前是否正在被使用?大小占多少?进程要使用的库,有没有被加载到内存当中?起始地址是什么?·········

所以,在上述 操作之后,OS 对于 库的 加载情况就非常清楚了。


共享库当中的 errno 变量存储的问题

 共享库当中的代码是共享的,那么其中的数据是不是共享的呢?

如果我们在一个 C 程序当中,对 errno 这个 C库当中的全局变量进行了修改,其他C 程序会不会受到影响?

答案是不会的,因为 共享区是在 栈 和 堆区当中的,这也就意味着,当 某一个进程对 errno 变量做了修改,就会发生写时拷贝,谁写谁就自己拷贝一份 自己存储 自己修改的数据。

这个写时拷贝的 出来的空间 在共享区当中存储,同时,在 C 管理 要输出到文件当中的数据,也有一个 struct_file 结构体来管理,在这个结构体当中就有 C 帮我们提供的 语言级别的缓冲区(用户级缓冲区)。

 


总结 

静态库 只需要在 编译时期 ,可以找到这个 静态库在哪,把这个静态库当中的数据,拷贝到 对应源程序当中。

但是动态链接不一样,如果要是用到 动态库的话,动态库就需要再 内存当中被加载,从而执行。而 静态库是不需要 加载到内存当中执行的,它完成了 拷贝的工作就可以 下岗了,在当前可执行程序的链接就不需要他了。

在上述说明的 四种 可执行程序链接 动态库 的方式的当中,其实用得最多就是 拷贝 这种方式。因为这种方式最直接了当,你会发现,这种方式其实是最方便的。

第三方库链接操作例子:【Linux拓展】ncurses库的安装和使用 {ncurses库的安装方法,ncurses库的使用手册,基于终端的贪吃蛇游戏}_芥末虾的博客-CSDN博客

上述的 ncurses库 是一个 基于 终端的 图形界面的库,有兴趣的可是尝试链接这个第三方库。

上述的 ncurses 库是在Linux 下的,如果是在windows 当中其实还有 easyx 库,在windows 当中这些库会做的更酷炫一些。还有 QT。

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

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

相关文章

ESP32-Web-Server 实战编程-使用文件系统建立强大的 web 系统

ESP32-Web-Server 实战编程-使用文件系统建立强大的 web 系统 概述 在前述章节我们讲述了在网页端控制多个 GPIO 的案例。当程序开始变得复杂&#xff0c;让一些功能“自动起来”是一个好的选择。 在前面的示例中&#xff0c;我们需要在后端为每个前端代码的 URL 指定一个对…

Matplotlib直方图的创建_Python数据分析与可视化

Matplotlib直方图的创建 概念区分绘制直方图 概念区分 什么是直方图&#xff1f; 直方图&#xff08;Histogram&#xff09;又称质量分布图&#xff0c;是统计报告图的一种&#xff0c;由一系列高度不等的纵向条纹或线段表示数据分布的情况&#xff0c;一般用横轴表示数据所属…

解锁文件安全新境界!迅软DSE带您领略数据加密的魅力!

随着信息技术的不断发展&#xff0c;企业数据信息的安全与保护受到愈发广泛的关注。而文件加密软件得益于其强大的系统功能能够有效地保护企业重要数据的隐私和安全&#xff0c;成为越来越多企事业单位在进行内部数据安全防护工作时的优选。 一、文件加密软件的作用 文件加密软…

国产数据库

当今世界&#xff0c;数据已成为重要的生产要素&#xff0c;数据库管理系统更是广泛应用于信息化行业各领域&#xff0c;国内数据库产业能否健康可持续的发展&#xff0c;在很大程度上影响着国民经济发展和网络空间安全。 当前&#xff0c;国产数据库行业竞争非常激烈&#xf…

HCIE 01:基于前缀列表的BGP ORF功能

当运行BGP协议的某台设备上&#xff0c;针对入方向配置了基于ip-prefix的路由过滤&#xff0c;过滤了邻居发送的路由&#xff1b; 目前想&#xff0c;通过在peer关系的两端设备上都配置ORF功能&#xff0c;实现路由发送端只能送路由接收端过滤后的路由&#xff1b; ORF功能的说…

Leetcode—1670.设计前中后队列【中等】

2023每日刷题&#xff08;四十三&#xff09; Leetcode—1670.设计前中后队列 实现代码 erase(iterator position)在删除vector中的元素后&#xff0c;会将该元素的后面所有元素都往前挪一位。因此&#xff0c;原先的迭代器指向的元素就不是原来那个了&#xff0c;而是它的后…

2023年05月 Scratch图形化(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共10题,每题3分,共30分) 第1题 下列积木运行后的结果是?( )(说明:逗号后面无空格) A:我 B:爱 C:中 D:国 答案:B 两个字符串连接后的第8个字符是“爱”。 第2题 接鸡蛋游戏中,天空掉下来有鸡蛋、石…

Flink Flink中的合流

一、Flink中的基本合流操作 在实际应用中&#xff0c;我们经常会遇到来源不同的多条流&#xff0c;需要将它们的数据进行联合处理。所以 Flink 中合流的操作会更加普遍&#xff0c;对应的 API 也更加丰富。 二、联合&#xff08;Union&#xff09; 最简单的合流操作&#xf…

开源免费跨平台数据同步工具-Syncthing

Syncthing是一款开源免费跨平台的文件同步工具&#xff0c;是基于P2P技术实现设备间的文件同步&#xff0c;所以它的同步是去中心化的&#xff0c;即你并不需要一个服务器&#xff0c;故不需要担心这个中心的服务器给你带来的种种限制&#xff0c;而且类似于torrent协议&#x…

卡码网语言基础课 | 16. 出现频率最高的字母

目录 一、 哈希表 二、 编写解题 2.1 统计出现次数 2.2 解答 通过本次练习&#xff0c;将学习到C中哈希表的基础知识 题目&#xff1a; 给定一个只包含小写字母的字符串&#xff0c;统计字符串中每个字母出现的频率&#xff0c;并找出出现频率最高的字母&#xff0c;如果…

比尔盖茨:GPT-5不会比GPT-4好多少,生成式AI已达到极限

比尔盖茨一句爆料&#xff0c;成为机器学习社区热议焦点&#xff1a; “GPT-5不会比GPT-4好多少。” 虽然他已不再正式参与微软的日常运营&#xff0c;但仍在担任顾问&#xff0c;并且熟悉OpenAI领导团队的想法。 消息来自德国《商报》&#xff08;Handelsblatt&#xff09;对…

搞定这三个问题 伦敦金止损就没问题

笔者多次强调&#xff0c;做伦敦金交易&#xff0c;重要的是风险控制。而止损是我们风险控制中一个很重要的概念。设定好止损&#xff0c;就是风险控制的好开始。下面我们通过三个问题&#xff0c;来解决止损的问题。 问题一&#xff0c;你的止损位在哪里&#xff1f;要做止损&…

数据结构与算法之美学习笔记:27 | 递归树:如何借助树来求解递归算法的时间复杂度?

目录 前言递归树与时间复杂度分析实战一&#xff1a;分析快速排序的时间复杂度实战二&#xff1a;分析斐波那契数列的时间复杂度实战三&#xff1a;分析全排列的时间复杂度内容小结 前言 本节课程思维导图&#xff1a; 今天&#xff0c;我们来讲这种数据结构的一种特殊应用&am…

YOLO改进系列之SKNet注意力机制

摘要 视皮层神经元的感受野大小受刺激的调节即对于不同的刺激&#xff0c;卷积核的大小应该不同&#xff0c;但在构建CNN时一般在同一层只采用一种卷积核&#xff0c;很少考虑因采用不同卷积核。于是SKNet被提出&#xff0c;在SKNet中&#xff0c;不同大小的感受视野&#xff…

深度学习框架配置

目录 1. 配置cuda环境 1.1. 安装cuda和cudnn 1.1.1. 显卡驱动配置 1.1.2. 下载安装cuda 1.1.3. 下载cudnn&#xff0c;将解压后文件复制到cuda目录下 1.2. 验证是否安装成功 2. 配置conda环境 2.1. 安装anaconda 2.2. conda换源 2.3. 创建conda环境 2.4. pip换源 3.…

【工作记录】spider-flow使用插件连接并操作mongodb数据库

前言 前面说过&#xff0c;spider-flow有着非常优秀的插件机制&#xff0c;可以通过插件实现功能的扩展。前面有小伙伴问到mongodb的集成使用&#xff0c;本文就来梳理下spider-flow中使用mongodb插件的过程&#xff0c;其实非常简单。 PS: spider-flow的作者已经实现了一些常…

飞翔的小鸟小游戏

主类 package APP;import 框架.GameFrame;public class GameApp {public static void main(String[] args) {//游戏的入口new GameFrame();} }场景实物 package 框架;import 图导.Constant; import 图导.GameUtil;import java.awt.*; import java.awt.image.BufferedImage; …

C语言——数字金字塔

实现函数输出n行数字金字塔 #define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>void pyramid(int n) {int i,j,k;for (i1; i<n; i){//输出左边空格&#xff0c;空格数为n-i for (j1; j<n-i; j){printf(" "); } //每一行左边空格输完后输出数字&#…

STM32g70开启定时器死机原因

在做低功耗产品时&#xff0c;检查发现由于之前开启了BOOTLOADER升级程序&#xff0c;修改了中断向量FALSH起始地址&#xff0c;只在KEIL TARGET IROM1中修改了&#xff0c; 而忘记在程序文件system_stm32f10x.c里修改中断向量表flash起始地址 system_stm32f10x.c里&#xff0…

8款前端特效动画及源码分享

3D立体数字时钟滚动特效 基于Splitting制作的一款3D立体数字时钟滚动特效&#xff0c;创意感满满&#xff0c;可以下载使用。 预览获取 核心代码 <div class"clock"><span class"cog hours tens" data-splitting>0123456789</span>&l…