一、动静态库的基本原理
需要完成一个可执行程序需要经历以下四个步骤:
- 预处理:完成头文件的展开,去掉注释,宏替换,条件编译等,最终形成***.i文件
- 编译:完成语法分析,词法分析,语义分析,符号汇总等,检查无误后将代码翻译成汇编指令,最终形成***.s文件
- 汇编:将汇编指令转换成二进制指令,最终形成***.o文件
- 链接:将生成的各个.o文件进行链接,最终形成可执行程序
库的本质就是一堆.o集合,库的文件当中并不包含主函数而只是包含了大量写好的方法以供调用。
二、静态库和动态库的区别
静态库和动态库是计算机编程中两种不同的库文件形式,它们在编译链接阶段、内存使用以及更新维护等方面有着显著的区别:
1. 编译链接阶段:
静态库(Static Library):
静态库在编译时会被直接整合到应用程序的可执行文件中。当程序编译链接时,编译器将静态库中的代码复制并连接到目标程序中,生成的可执行文件包含了所有必要的库函数实现,因此不依赖运行时外部的库文件。 通常静态库文件扩展名为 .a(Unix/Linux系统)或 .lib(Windows系统)。
动态库(Dynamic Library / Shared Library):
动态库在编译时并不直接合并到应用程序中,而是在链接阶段仅添加对库函数的引用信息。实际的库文件在程序运行时加载到内存,并通过操作系统共享给多个进程。因此,程序运行时需要相应的动态库文件存在于系统的特定目录下。 动态库文件扩展名通常为 .so(Linux系统)、.dylib(Mac OS系统)或 .dll(Windows系统)。
2. 内存使用方式:
静态库:
因为静态库的内容被直接包含在程序内部,所以每个使用静态库的应用程序都会拥有该库的一份完整副本。这意味着占用较大的磁盘空间和内存资源。
动态库:
动态库可以在多个进程中共享同一份物理内存拷贝。一个动态库文件可以被多个运行时的程序加载和使用,从而节约了内存资源。
3. 更新与维护:
静态库:
当静态库有更新时,所有依赖它的程序都需要重新编译链接才能获得新的功能或修复的问题。这可能会带来一定的部署复杂性。
动态库:
更新动态库时,只需替换系统或应用指定路径下的库文件即可。所有依赖此动态库的已编译程序,在下次运行时会自动加载新版本的库,无需重新编译这些程序本身。但这也意味着必须确保所有依赖它的程序都能兼容新的库接口。
4. 依赖问题:
静态库:
使用静态库编译出的程序具有良好的独立性,不需要关心用户的系统环境是否安装了相应的库。
动态库:
使用动态库编译出的程序在运行时依赖于外部的库文件存在,如果用户环境中缺少对应的库文件或者版本不匹配,可能导致程序无法正常运行。
5. 性能考量:
静态库:
静态库由于代码在编译时就已集成到程序中,理论上启动速度较快,无运行时加载库的开销。
动态库:
动态库在首次加载时可能产生额外的时间开销,但因为可以共享内存,对于大量使用相同库的不同程序来说,从整体上讲可能更节省系统资源。同时,它支持热更新,即无需重启程序就能更新库的功能。
三、DLL动态库的创建,隐式加载和显式加载
1. 动态库创建
演练:创建和使用自己的动态链接库 (C++)
(1) 使用VS创建动态库工程,工程创建成功后会自动创建出xx.cpp和xx.h文件;
(2) 在xx.h中添加以下代码
#ifndef _DLLTEST_H_
#define _DLLTEST_H_
extern "C" _declspec(dllexport) int Add ( int a, int b);
extern "C" _declspec(dllexport) int Sub ( int a, int b);
#endif
#ifndef
和#define
的配合使用可以防止头文件被重复引用。declspec(dllexport)
将一个函数声明为导出函数,就是说这个函数是要其他程序调用的,作为DLL的一个对外函数接口。extern “C”
的主要作用是为了让C++代码能够正确调用其他用C语言编写的代码,加上这个限定之后,编译器会把其后的代码段按照C语言的编译方式编译,而不是按照C++语言的编译方式编译。
C++编译方式跟C编译方式的不同之处在于:C++支持函数的重载,编译器在编译函数时会附带将函数的参数个数、参数类型信息一起添加到编译之后的代码中,而不仅仅只是包含函数名称,C++正是通过这种编译机制实现函数重载的。而编译器对C语言代码中的函数在编译时只包含函数名称,不附带参数类型或参数个数信息。所以extern “C”
声明的主要作用是为了实现C语言代码在C++代码中的混合编程。
(3) 在xx.cpp中加入以下代码
// DllTest.cpp : 定义 DLL 应用程序的导出函数。
//
#include "stdafx.h"
#include "DllTest.h"
int Add(int a, int b)
{
return a+b;
}
int Sub(int a, int b)
{
return a-b;
}
以上操作相当于在xx.h文件中对所有DLL导出的函数进行导出声明,并且方便引用DLL的工程查看导出函数列表。xx.cpp文件中是对导出函数的具体实现。
编译xx工程,会在工程目录Debug/Release目录下生成xx.dll和xx.lib文件,加上xx.h文件,这三个文件就是导入DLL文件所需要的全部文件了。
在另一个工程中对DLL文件的加载有两种实现方式,分别是隐式加载(加载时动态链接)和显式加载(运行时动态加载)。
2. 隐式加载
隐式加载是在系统启动时一次性把所有的DLL的导出函数加载到可执行文件中,需要用到.h和.lib文件。隐式加载的步骤为:
(1) 新建一个测试工程,把xx工程中的头文件xx.h拷入工程目录下,并在工程的头文件中添加该头文件。
(2) 在工程中的cpp文件中加入以下代码:
#include <iostream>
#include "DllTest.h"
using namespace std;
#pragma comment (lib,"DllTest.lib")
extern "C" _declspec(dllimport) int Add( int a, int b);
extern "C" _declspec(dllimport) int Sub( int a, int b);
int main(int argc,char *argv[])
{
int num1=5,num2=3;
cout<<num1<<"+"<<num2<<" = "<<Add(num1,num2)<<endl;
cout<<num1<<"-"<<num2<<" = "<<Sub(num1,num2)<<endl;
system("pause");
return 0;
}
_declspec(dllimport)
指明从DLL文件中导出的函数。注意通过隐式链接加载的dll,也可以不用列出_declspec(dllimport)
,实际效果是一样的(可以尝试把这两段代码注释掉,也可以正常运行)。
所不同的是,加入_declspec(dllimport)
后相当于明确告知编译器其后的函数是从外部dll加载的,调用的时候直接取dll文件的对应函数入口处调用。而不加_declspec(dllimport)
则编译器不能分辨出当前函数是普通函数还是从别的dll文件中加载的函数,调用的时候需要在二进制代码中通过一个JMP指令跳转到dll文件中函数的入口地址。
(3) 隐式加载需要使用到动态库的导入库——.lib文件,在工程中加入.lib文件主要有以下3中方式:
- 拷贝.lib文件到工程目录下,然后在程序中通过
#pragma comment(lib, "xx.lib")
链接即可; - 不直接拷贝.lib文件,而是通过.lib文件所在的绝对路径访问,在程序中通过
#pragma comment(lib, "D:\\XX\\xx.lib")
链接绝对路径; - 在工程上右键->属性->链接器->输入->附加依赖项,在附加依赖项栏中输入.lib文件所在的绝对路径,如“D:\XX\xx.lib”,在程序中就不再需要指令
#pragma comment(lib, "xx.lib")
;
无论是.lib文件采取哪种方式加入到工程中,在编译成功生成.exe可执行文件之后就不再需要.lib文件了,发布的时候也不需要带着.lib文件发布。
(4) 把.dll文件拷贝到程序根目录下的Debug/Release文件夹里,跟.exe文件同一目录,发布的时候也随.exe文件一起发布。
完成以上4个步骤之后就把Dll文件成功加载到程序中了。
3. 显示加载
显式加载不需要通过.lib和.h文件链接(当然前提是要知道.dll文件中包含的函数列表)。新建一个测试工程,在.cpp文件中加入以下代码:
#include <Windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc) (int a, int b);
typedef int (*SubFunc) (int a, int b);
int main(int argc,char *argv[])
{
int numb1=5, numb2=3;
HMODULE hDll=LoadLibrary(L"DllTest.dll");
if(hDll!=NULL)
{
AddFunc add=(AddFunc)GetProcAddress(hDll,"Add");
SubFunc sub=(SubFunc)GetProcAddress(hDll,"Sub");
if(add)
{
cout<<numb1<<"+"<<numb2<<" = "<<add(numb1,numb2)<<endl;
}
if(sub)
{
cout<<numb1<<"-"<<numb2<<" = "<<sub(numb1,numb2)<<endl;
}
system("pause");
}
FreeLibrary(hDll);
return 0;
}
之后拷贝DllTest.dll文件到程序根目录下的Debug/Release文件夹中即可,在发布程序的时候也需要随着程序一起发布。
在显示加载中,程序会在需要的时候才去加载DLL文件,获取到DLL文件中相关的函数入口地址,然后执行,执行完之后可以立即释放掉资源。显示加载具有更好的灵活性,能更加有效的使用内存,在编写大型程序时往往使用显示加载方式。
参考:
[1]VS下动态库dll的显式调用(动态调用)_vs显式调用dll库-CSDN博客;
[2]【Linux系统编程】Linux动态库详解;
[3]DLL动态库的创建,隐式加载和显式加载_dll 隐式加载 一定要导入符号库吗-CSDN博客;