文章转载https://blog.csdn.net/helloworld201456/article/details/29540719和https://blog.csdn.net/nirendao/article/details/50572087

一.动态链接库的原理:

Windows系统平台上,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。Microsoft Windows自己就将一些主要的系统功能以DLL模块的形式实现。例如IE中的一些基本功能就是由DLL文件实现的,它可以被其它应用程序调用和集成。一般来说,DLL是一种磁盘文件(通常带有DLL扩展名,是标准win32可执行文件-“PE”格式),它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分,进程中所有线程都可以调用其中的函数。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。DLL模块中包含各种导出函数,用于向外界提供服务。Windows在加载DLL模块时将进程函数调用与DLL文件的导出函数相匹配。

在Win32环境中,每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。DLL模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。

DLL文件中包含一个导出函数表(存在于PE的.edata节中)。这些导出函数由它们的符号名和称为标识号的整数与外界联系起来。函数表中还包含了DLL中函数的地址。当应用程序加载DLL模块时时,它并不知道调用函数的实际地址,但它知道函数的符号名和标识号。动态链接过程在加载的DLL模块时动态建立一个函数调用与函数地址的对应表。如果重新编译和重建DLL文件,并不需要修改应用程序,除非你改变了导出函数的符号名和参数序列。

简单的DLL文件只为应用程序提供导出函数,比较复杂的DLL文件除了提供导出函数以外,还调用其它DLL文件中的函数。

每个DLL都有一个入口函数(DLLMain),系统在特定环境下会调用DLLMain。在下面的事件发生时会调用dll入口函数:1.进程装载DLL。2.进程卸载DLL。3.DLL在被装载之后创建了新线程。4. DLL在被装载之后一个线程被终止了。

应用程序导入函数与DLL文件中的导出函数进行链接有两种方式:隐式链接和显式链接。

隐式链接(load-time dynamic linking)是指在应用程序中不需指明DLL文件的实际存储路径,程序员不需关心DLL文件的实际装载(由编译器自动完成地址分配)。采用隐式链接方式,程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。我们使用的大部分系统Dll就是通过这样的方式链接的。若找不到需要的Dll则会给出一个Dll缺少的错误消息。

显式链接(run-time dynamic linking)与此相反。用户程序在编译的时候并没有指明需要哪些Dll,而是在运行起来之后调用Win32 的LoadLibary()函数,去装载Dll。若没有找到Dll则这个函数就会返回一个错误。在用LoadLibary()函数装载Dll之后,应用程序还需要用GetProcAdress()函数去获得Dll输出函数的地址。显式链接方式对于集成化的开发语言比较适合。有了显式链接,程序员就不必再使用导入文件,而是直接调用Win32 的LoadLibary()函数,并指定DLL的路径作为参数。还要说明一点的就是Known Dlls就是保证在通过LoadLibary()去装载系统Dll的时候,只从特定的系统目录去装载,防止装载错。装载的时候会去看注册表下是否有一样的注册表键名。如果是装载windows\system32\目录下的对应的Dll。

Dll的搜索顺序,在Windows上有个注册表键值决定了Dll的搜索顺序:HKLM\System\CurrentControlSet\SessionManager\SafeDllSearchMode。在vista,server2003,xp sp2中这个值为1,在xp,2000 sp4中为0。1值时的搜素顺序为:1.可执行文件所在目录,2.系统目录windows\system32\,3. 16位系统目录,4.windows目录,5.当前进程目录。6.环境变量PATH中的目录。0值时的搜素顺序为:1.可执行文件所在目录,2. 当前进程目录。3.系统目录windows\system32\,4. 16位系统目录,5.windows目录,6.环境变量PATH中的目录。

DLL的加载与连接

Windows DLL装入(除ntdll.dll外)和连接是通过ntdll.dll中一个函数LdrInitializeThunk实现的。先对LdrInitializeThunk()这个函数名作些解释“Ldr显然是“Loader”的缩写。而“Thunk”意为“翻译”、“转换”、或者某种起着“桥梁”作用的东西。这个词在一般的字典中是查不到的,但却是个常见于微软的资料、文档中术语。这个术语起源于编译技术,表示一小片旨在获取某个地址的代码,最初用于函数调用时“形参”和“实参”结合。后来这个术语有了不少新的特殊含义和使用,但是DLL的动态连接与函数调用时“形实结合”确实有着本质的相似。

由于Windows没有公开这个函数的代码,所以学习起来比较困难,只能通过查阅一些资料来大概猜测这个函数的实现。这个过程中也参看了很多ReactOS(ReactOS是一个免费而且完全兼容 Microsoft Windows XP 的操作系统。ReactOS 旨在通过使用类似构架和提供完整公共接口实现与 NT 操作系统二进制下的应用程序和驱动设备的完全兼容。)的LdrInitializeThunk()函数实现源代码。

在进入这个函数之前,目标 EXE映像已经被映射到当前进程的用户空间,系统DLL ntdll.dll的映像也已经被映射,但是并没有在EXE映像与ntdll.dll映像之间建立连接 (实际上 EXE映像未必就直接调用ntdll.dll中的函数)。LdrInitializeThunk()是ntdll.dll中不经连接就可进入的函数,实质上就是ntdll.dll的入口。除ntdll.dll以外,别的 DLL都还没有被装入(映射)。此外,当前进程(除内核中的“进程控制块”EPROCESS等数据结构外)在用户空间已经有了一个“进程环境块”PEB,以及该进程的第一个“线程环境块”TEB。这就是进入 LdrInitializeThunk()前的“当前形势”。

PEB中有一个字段Ldr是个PEB_LDR_DATA结构指针,所指向的数据结构用来为本进程维持三个“模块”队列、即InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList。这里所谓“模块”就是PE格式的可执行映像,包括EXE映像和DLL映像。前两个队列都是模块队列,第三个是初始化队列。两个模块队列的不同之处在于排列的次序,一个是按装入的先后,一个是按装入的位置。每当为本进程装入一个模块、即.exe映像或DLL映像时,就要为其分配,创建一个LDR_DATA_TABLE_ENTRY数据结构,并将其挂入InLoadOrderModuleList。然后,完成对这个模块的动态连接以后,就把它挂入InInitializationOrderModuleList队列,以便依次调用它们的初始化函数。相应地,LDR_DATA_TABLE_ENTRY数据结构中有三个队列头,因而可以同时挂在三个队列中。在我做的小实验当中就是通过查找这三个队列,来将当前进程的Dll加载信息显示出来的。具体的实例请见我的实验说明文档。

在LdrInitializeThunk()中,最开始为做的事情就是将加载的模块信息存放在PEB中的ldr字段,如上面一段文字中所述。之后,LdrInitializeThunk()函数又调用了一个叫LdrPEStartup()的函数。LdrPEStartup()函数首先判断了“期望地址”是否可用,PE映像的NtHeader(peb中有个ImageBaseAddress的地址,代表exe映像在用户空间的位置,在这个地址指向的数据结构中就有NtHeader的结构)中有个指针,指向一个OptionalHeader。在OptionalHeader中有个字段ImageBase,是具体映像建议、或者说希望被装入的地址,我们称之为“愿望地址”。在装入一个映像时,只要相应的区间(取决于它的期望地址和大小)空闲,就总正常装入。但是如果与已经被占用的区间相冲突,就只好利用LdrPerformRelocations()换个地方。

那么映像的愿望地址有着什么物理的或者逻辑的意义呢?我们知道,软件在编译以后有个连接的过程,即为函数的调用者落实被调用函数的入口地址、为全局变量(按绝对地址)的使用者落实变量地址的过程。连接有静态和动态两种,静态连接是在“制造”软件时进行的,而动态连接则是在使用软件时进行的。尽管EXE模块和DLL模块之间的连接是动态连接,但是EXE或DLL模块内部的连接却是静态连接。既是静态连接,就必须为模块的映像提供一个假定的起点地址。如果以此假定地址为基础进行连接以后就不可变更,使用时必须装入到这个地址上,那么这个地址就是固定的“指定地址”了。早期的静态连接往往都是使用指定地址的。但是,如果允许按假定地址连接的映像在实际使用时进行“重定位”,那么这假定地址就是可浮动的“愿望地址”了。可“重定位”的静态连接当然比固定的静态连接灵活。事实上,要是没有可“重定位”的静态连接技术,DLL的使用就无法实现,因为根本就不可能事先为所有可能的DLL划定它们的装入位置和大小。至于可“重定位”静态连接的实现,则一般都是采用间接寻址,通过指针来实现。

所谓重定位,就是计算出实际装入地址与建议装入地址间的位移a,然后调整每个重定位块中的每一个重定位项、即指针,具体就是在指针上加a。而映像中使用的所有绝对地址(包括函数入口、全局量数据的位置)实际上用的都是间接寻址,每个这样的地址都有个指针存在于某个重定位块中。

完成了可能需要的EXE映像重定位以后,下一个主要的操作就是LdrFixupImports()了。实际上这才是关键所在,它所处理的就是当前模块所需DLL模块的装入和连接。各DLL的程序入口记录在它们的LDR_DATA_TABLE_ENTRY数据结构中借助InInitializationOrderModuleList队列就可依次调用所有DLL的初始化函数。

NtHeader的OptionalHeader中有个数组DataDirectory[],其中之一是重定位目录。除此之外,数组中还有“(普通)引入(import)”、“绑定引入(bound import)”以及其它多种目录,但是我们在这里只关心“引入”和“绑定引入”。这两个目录都是用于库函数的引入,但是作用不同,目录项的数据结构也不同。每个引入目录项都代表着一个被引入模块,其模块名、即文件名在dwRVAModuleName(ReactOS中的名字,下同)所指的地方。需要从同一个被引入模块引入的函数通常有很多个,dwRVAFunctionNameList指向一个字符串数组,数组中的每一个字符串都是一个函数名;与此相对应,dwRVAFunctionAddressList则指向一个指针数组。这两个数组是平行的,同一个函数在两个数组中具有相同的下标。从一个被引入模块中引入一个函数的过程大体上就是:根据函数名在被引入模块的引出目录中搜索,找到目标函数以后就把它实际装入后的入口地址填写到指针数组中的相应位置上。但是,这个过程可能是个开销相当大、速度比较慢的过程。为此,又发展起一种称为“绑定”的优化。

所谓绑定,就是在软件的编译,连接过程中先对使用时的动态连接来一次预演,预演时假定所有的DLL都被装入到它们的愿望地址上,然后把预演中得到的被引入函数的地址直接记录在引入者模块中相应引入目录下的指针数组中。这样,使用软件时的动态连接就变得很简单快捷,因为实际上已经事先连接好了。其实“绑定引入”和静态连接并无实质的不同。但是,各模块的版本配套就成为一个问题,因为万一使用的某个DLL不是当初绑定时的版本,而且其引出目录又发生了变化,就有可能引起混乱。为此,PE格式增加了一种“绑定引入”目录,相关的机制会进行判断。但是,“绑定引入”毕竟不是很可靠的,万一发现版本不符就不能使用原先的绑定了。所以“绑定引入”不能单独存在,而必须有普通引入作为后备。如果不符就不能按“绑定引入”目录处理引入,而只好退而求其次,改成按普通“引入”目录处理引入。另一方面,所谓“绑定”是指当被引入模块装入在预定位置上时的地址绑定,如果被引入模块的装入位置变了,就得对原先所绑定的地址作相应的调整、即“重定位”。

LdrFixupImports()函数首先从映像头部获取指向“引入”目录和“绑定引入”目录的指针。若存在“绑定引入”目录,则先通过LdrpGetOrLoadModule()找到或装入(映射)被引入模块的映像。首先当然是在模块队列中寻找,找不到就从被引入模块的磁盘文件装入。之后检查绑定版本是否一致,如果不一致就退而求其次,通过LdrpProcessImportDirectory()处理引入。当然,那样一来效率就要降低了。如果一致,则返回(因为在“预演”中已经连接好,效率当然高了)。而LdrpProcessImportDirectory()才是真正意义上的动态连接!!(说了这么多原来才开始……)。

LdrpProcessImportDirectory()首先根据目录项中的两个位移量取得分别指向函数名字符串数组和函数指针数组的指针。这两个数组是平行的(前面有介绍),然后对字符串数组中的元素计数,得到该数组的大小IATSize。显然,函数指针数组的大小也是IATSize。这里IAT是“引入地址表(Imported Address Table)”的缩写,其实就是函数指针数组。这个数组在映像内部,其所在的页面在装入映像时已被加上写保护,而下面要做的事正是要改变这些指针的值,所以先要通过NtProtectVirtualMemory()把这些页面的访问模式改成可读可写。做完这些准备之后,下面就是连接的过程了,那就是根据需要把被引入模块所引出的函数入口(地址)填写到引入者模块的IAT中。与当前模块中的两个数组相对应,在被引入模块的“引出”目录中也有两个数组,说明本模块引出函数的名称和入口地址(在映像中的位移)。当然,这两个数组也是平行的。要获取被引入模块中的函数入口有两种方法,即按序号(Ordinal)引入和按函数名引入。从而分别调用LdrGetExportByOrdinal()和LdrGetExportByName()。这两个函数都返回目标函数在本进程用户空间中的入口地址,把它填写入当前模块引入目录函数指针数组中的相应元素,就完成了一个函数的连接。当然,同样的操作要循环实施于当前模块需要从给定模块引入的所有函数,并且(在上一层)循环实施于所有的被引入模块。完成了对一个被引入模块的连接之后,又调用NtProtectVirtualMemory()恢复当前模块中给定目录项内函数指针数组所在页面的保护。

到此,我们大概的清楚Windows Dll的加载与连接过程。

二.静态库的原理:

函数和数据被编译进一个二进制文件(扩展名通常为.lib),在使用静态库的情况下,在编译链接可执行文件时,链接器从静态库中复制这些函数和数据,并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.exe)。当发布产品时,只需要发布这个可执行文件,并不需要发布被使用的静态库。因此,使用了静态库的可执行程序存储在磁盘上的空间就比较大。Windows上的静态库是.lib文件(但和dll文件的.lib文件是不同的,下面会有阐述)。

三.静态库和动态库的区别以及选择:

1. 使用了静态库的程序存储在磁盘上的空间比使用了动态库的程序要多。

2. 使用了动态库的程序,若有多个副本在内存中执行,又或者是不同的程序但都使用了同一个动态库,则它们所调用的动态库在内存中只有一份,所以很节省内存空间;而使用了静态库的程序的多个副本在内存中时,它们所使用的库所占的内存也是多份,因此浪费空间。
    3. 在库需要升级的时候,使用动态库的程序只需要升级动态库就好(假设接口不变),而使用了静态库的程序则需要升级整个程序。

4.在Windows多模块项目开发时,尽量使用动态链接库,不要使用静态库,尤其是公共模块。因为该模块被其他多个模块引用后,在最终的程序中会存在多份,调用时可能出错。动态链接库不会出现这种现象。比如本人在项目中遇到这种现象,模块A是单例模式,被做成静态库,但是被其他多个动态库模块引用,在调用A模块创建函数时,会创建多个对象,导致程序异常。

5.在使用动态库时,往往提供两个文件:一个引入库(.lib,非必须)和一个.dll文件。这里的引入库和静态库文件虽然扩展名都是.lib,但是有着本质上的区别,对于一个动态链接库来说,其引入库文件包含该动态库导出的函数和变量的符号名,而.dll文件包含该动态库实际的函数和数据。

四、__declspec(dllexport) 和 __declspec(dllimport)
    这2个宏是Windows对动态库(dll)进行编程和使用的时候所特有的,在Linux系统上则不需要这2个宏。
先来看看它们的作用:
     1.  __declspec(dllexport)可以被用来修饰一个函数或一个类,以表明这是一个导出函数或导出类。所以,这个宏是用在为dll做编程实现的时候的。
当修饰类的时候,该宏要写在class关键字的后面、类名的前面;
当修饰函数的时候,要写在函数声明的前面,而因为name mangling的问题,在此宏的前面还要写上extern“C”. 比如:

extern "C" MYDLL_DECL_EXPORT void say_hello();

2. __declspec(dllimport) 被用来在调用dll的程序里,表明该程序要调用的某个函数是import自某动态库的。所以,该宏的具体位置是在对dll进行描述的头文件中的。

3. 从以上可以看出,在dll的实现中,我们需要__declspec(dllexport)来表明这些函数和类是导出函数和导出类,而在使用dll的程序中,又要用__declspec(dllimport)来表明它所描述的函数或类是来自于某dll。那么这样的话,岂不是需要2个不同但又很相近的头文件来做这些函数和类的声明了吗?能否将这2个函数合并成一个呢?答案是可以的 – 使用宏进行判断:当宏A存在时,就认为宏B是__declspec(dllexport),否则就认为宏B是__declspec(dllimport)。具体实例如下:

#ifdef MYDLL_EXPORTS#define MYDLL_DECL_EXPORT __declspec(dllexport)#else#define MYDLL_DECL_EXPORT __declspec(dllimport)#endif

所以,在dll的实现中,需要在Preprocessor Definitions里定义MYDLL_EXPORTS,而在dll的使用者那里就不需要定义MYDLL_EXPORTS了。
 
 五、动态链接库创建图解:

1.

2.

3.

注:在主工程目录下添加lib,libd,bin 3个目录,bin中添加Debug和Release目录。lib和libd分别存放导出文件.lib,bin中的Debug和Realse分别存放dll和主目录生成的exe文件。

4.设置dll的名字和输出路径,当前在bin/debug中

5.输入依赖的库目录

6.输入依赖的库名称

7.设置导入文件.lib 的路径

8.添加代码

MyDll.h如下:#ifndef MYDLL_H
#define MYDLL_H #ifdef MYDLL_EXPORTS# define MYDLL_DECL_EXPORT __declspec(dllexport)
#else# define MYDLL_DECL_EXPORT __declspec(dllimport)
#endif class MYDLL_DECL_EXPORT MyDll
{
public:    MyDll(int x=0, int y=0);   MyDll(const MyDll &) = delete;    ~MyDll(){}    MyDll operator = (const MyDll &) = delete;     long Add();    long Sub();     int GetFirstNumber();    void SetFirstNumber(int x);    int GetSecondNumber();    void SetSecondNumber(int y);
private:    int mFirstNum;    int mSecondNum;
}; extern "C" MYDLL_DECL_EXPORT void say_hello(); #endif
mydll.cpp如下:#include "MyDll.h"
#include <iostream>
MyDll::MyDll(int x, int y) :mFirstNum(x), mSecondNum(y){}
long MyDll::Add()
{    return long(mFirstNum) + long(mSecondNum);
}
long MyDll::Sub()
{    return long(mFirstNum - mSecondNum);
} int MyDll::GetFirstNumber()
{    return mFirstNum;
} int MyDll::GetSecondNumber()
{    return mSecondNum;
}
void MyDll::SetFirstNumber(int x)
{    mFirstNum = x;
}
void MyDll::SetSecondNumber(int y)
{    mSecondNum = y;
} // standalone function but also exposedMYDLL_DECL_EXPORT void say_hello() {    std::cout << "Hello, World!" << std::endl;}

9.这个dll工程build后,就可以生成MyDll.dll和MyDll.lib了。但是要注意,在属性->预处理器中要记得添加上MYDLL_EXPORTS宏。否则就没啥导出的了。

六、动态链接库的使用:

1.先创建好一个win32控制台应用程序,然后设置项目依赖项,依赖动态链接库工程,这样编译exe项目的时候就可以根据依赖关系,先编译dll工程。如图:

2.设置导入库路径:

3.设置连接器,设置附加依赖项,添加dll工程中生成的导入文件.lib

七、动态链接库的经验总结:

1.导出类的时候最好导出接口类,只暴露外部调用的接口,不要导出实现类。而且导出实现类的时候会发生很多告警。接口类只声明纯虚函数供外部调用,其他的都不要出现,留在实现类中。

2.导出函数的时候一定要如下形式导出:  extern "C" MYDLL_DECL_EXPORT void say_hello();

3.创建动态库的过程中一定要在导出函数和类的前面冠上MYDLL_DECL_EXPORT,否则不会生成.lib导入文件。

4.创建可升级的DLL时一定要记住: 只导出接口类,不要导出实现类。此举是避免DLL地狱陷阱。

5.动态链接库调用静态链接库的时候,只引用调用的部分,其他不用。比如OnvifGsoap编译成lib库有430M,被Onvif动态库模块引用后生成的Onvif动态库只有4M。此时Onvif只能被编译成静态库。

Windows动态链接库DLL和静态库的原理以及创建方法相关推荐

  1. 动态链接库dll,静态链接库lib, 导入库lib 转

    动态链接库dll,静态链接库lib, 导入库lib 在用VS编译工程的时候,我们会选择动态链接库dll,静态链接库lib(static library),可是为什么在编译动态链接库的时候也可以指定输出 ...

  2. 动态链接库dll,静态链接库lib, 导入库lib

    目前以lib后缀的库有两种,一种为静态链接库(Static Libary,以下简称"静态库"),另一种为动态连接库(DLL,以下简称"动态库")的导入库(Imp ...

  3. MFC模块的动态链接库DLL以及静态链接库LIB编译后的调用

    静态链接库LIB和动态链接库DLL的区别,创建和示例   1.什么是静态连接库,什么是动态链接库   静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都 ...

  4. C++动态链接库dll及静态链接库lib制作及使用教程

    现需将C++函数封装成动态链接库dll,网上看了好多博客教程,说的都不够全面,现提供一个很有用的视频,亲测有效,启发很大,附上链接: 视频网址 下面自己根据视频记录下制作动态链接库dll过程,防止忘记 ...

  5. C++ 调用lib 和 dll的 方法 及 动态库DLL与静态库lib的区别

    C++ 调用.lib的方法: 一: 隐式的加载时链接,有三种方法 1  LIB文件直接加入到工程文件列表中 在VC中打开File View一页,选中工程名,单击鼠标右键,然后选中"Add F ...

  6. Windows 动态链接库 DLL 浅析

    一.概念 DLL:Dynamic Link Library,即动态链接库,这种库包含了可由多个程序同时使用的代码和数据. 它是microsoft在windows操作系统中实现共享函数库概念的一种实现方 ...

  7. Windows 动态链接库DLL浅解

    为什么80%的码农都做不了架构师?>>>    动态链接库(DLL),即:Dynamic Link Library.一个包含可由多个程序同时使用的代码和数据的库,DLL不是可执行文件 ...

  8. 小心DLL链接静态库时的内存错误

    最近写的模块,在独立的应用程序中测试是没问题的,但把它装配成DLL后,再在另一个应用程序中调用时却出现了内存错误.程序的模块链接关系大概是这样的: module就是我所写的模块,在这里被封装为DLL, ...

  9. windows下多个静态库合并的方法

    方法一: VS项目->属性->配置属性->库管理器->常规->附加依赖项.附加库目录  添加需要合并的静态库 方法二: 开始->所有程序->Microsoft ...

  10. php扩展 静态库,编译PHP扩展的方法

    [相关学习推荐:php编程(视频)] 构建PHP扩展 你已经知道如何去编译PHP本身,下一步我们将编译外部扩展.我们将讨论扩展的构建过程和可用的编译选项. 载入共享扩展 在前一个章节你已经知道,PHP ...

最新文章

  1. 二十四、redis发布订阅
  2. 队列化栈栈化队列(力扣)
  3. docker 安装redis
  4. 【网络编程】之三、socket网络编程
  5. TeamToy - 创新团队的效率工具 一个好用的 团队协作软件
  6. java 设计作业——学生类的基本练习
  7. Spring AOP动态代理实现,解决Spring Boot中无法正常启用JDK动态代理的问题
  8. [摘录]调动员工积极性的七个关键
  9. 使用JDBC创建出版社和书籍管理系统
  10. java智能点餐系统研究内容_JAVA课程实践报告 基于web的点餐系统毕业设计
  11. 选股策略与技巧 选股策略报告
  12. 视频帧数(图片)和音频提取及保存方法图片合成视频方法---ffmpeg
  13. 打造云原生大型分布式监控系统(三): Thanos 部署与实践
  14. 如何给深度学习加速——模型压缩、推理加速
  15. Cuba studio6.9 图文安装
  16. 铂德发布换弹型电子烟新琥珀,3.5ml超大容量创行业纪录
  17. 【TFS-CLUB社区 第7期赠书活动】〖从零开始利用Excel与Python进行数据分析 自动化办公实战宝典〗等你来拿,参与评论,即可有机获得
  18. LAN、WAN、WLAN、VLAN的区别
  19. 标普500指数下跌2.4%至盘中低点
  20. 深入理解如何不费吹灰之力搭建一个无人驾驶车(二)2D-小车其他部分(独创导航各参数解析)

热门文章

  1. 17 CoCos Creator-Node Tree 层级管理器
  2. 2020年内蒙古自治区第十五届大学生程序设计竞赛榜单
  3. ansible中变量注册 register的使用
  4. python初学问题:IndentationError: expected an indented block
  5. Anycubic Vyper 3D打印机串口屏改造开源项目之串口屏项目启动篇(一)
  6. 简单概率dp-hdu-4487-Maximum Random Walk
  7. python版武侠小说男女侠姓名生成器
  8. bat脚本 - 通过bat脚本一键启动[开机启动]日常应用
  9. 【6】python生成数据曲线平滑处理——(Savitzky-Golay 滤波器、convolve滑动平均滤波)方法介绍,推荐玩强化学习的小伙伴收藏
  10. Excel单元格下拉选择,单元格自动计算