Andrew Hunt/David Thomas程序员修炼之道
在计算机科学中,库(library)是用于开发软件的子程序集合。库和可执行文件的区别是,库不是独立程序,他们是向其他程序提供服务的代码。
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
动态库和静态库
库链接是指把一个或多个库包括到程序中,有两种链接形式:静态链接和动态链接,相应的,前者链接的库叫做静态库,后者的叫做动态库。
动态库和静态库编译过程
静态链接
静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。
当程序与静态库连接时,库中目标文件所含的所有将被程序使用的函数的机器码被复制到最终的可执行文件中。这就会导致最终生成的可执行代码量相对变多,占用的磁盘空间和内存空间也会变多。
在Windows中静态库是以.lib为后缀的文件,在Linux中静态库是以.a为后缀的文件。
动态链接
动态链接,在可执行文件装载时或运行时,由操作系统的装载程序加载库。
与共享库连接的可执行文件只包含它需要的函数的引用表,而不是所有的函数代码,只有在程序执行时, 那些需要的函数代码才被拷贝到内存中。这样就使可执行文件比较小, 节省磁盘空间。不过由于运行时要去链接库会花费一定的时间,执行速度相对会慢一些。
在Windows中动态库(dynamic link library)是.dll为后缀的文件,在Linux中动态库(共享库shared library)是以.so为后缀的文件。
动态库和静态库对比
静态链接的最大缺点是生成的可执行文件太大,另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态链接的最大缺点是可执行程序依赖分别存储的库文件才能正确执行。如果库文件被删除、移动、重命名或者被替换为不兼容的版本,那么可执行程序就可能工作不正常。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。应用程序只需要更新动态库即可实现程序的更新。
Windows下使用VC++创建动态库
Windows下创建动态库有两种方式:使用_declspec(dllexport)和_declspec(dllimport)关键字、指定.def文件。
声明_declspec(dllexport)和_declspec(dllimport)关键字
声明函数为_declspec(dllexport),说明该函数为dll导出函数;声明函数为_declspec(dllimport)说明该函数从dll中导出。
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void test()
{
printf("dll test\n");
}
#ifdef __cplusplus
}
#endif
使用eXeScope加载该dll,查看导出表可以看出如下信息:
序列 地址 名字
00000001 10011127 test
指定.def文件
.def指定函数,并告知编译器不要以修饰后的函数名作为导出函数名,而以指定的函数名导出函数。
1、需要创建一个Module-Definition File(.def)文件,添加导出函数名
EXPORTS
test
注意:如果是将.txt文件改成.def文件,则需要在Visual Studio里设置:Project Property Pages→Configuration Properties→Linker→Input→Module Definition File中添加该文件
2、添加test函数代码
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
void test()
{
printf("dll test\n");
}
#ifdef __cplusplus
}
#endif
使用eXeScope加载该dll,查看导出表可以看出如下信息:
序列 地址 名字
00000001 10011127 test
如果只添加.def文件,而不添加导出函数名,那么导出函数表存在,但是函数表内是空的。
如何选择?
如果导出函数调用方式采用cdecl,可以不用.def文件;如果要采用stdcall调用方式,又不想函数名被修饰,那么久采用.def文件。有关调用方式的讲解见后文。
如何调用DLL动态链接库
在Windows平台下有两种调用方式:显式调用和隐式调用。以下面的例子来阐述该问题:
// test.h
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void test();
#ifdef __cplusplus
}
#endif
// test.cpp
#include "test.h"
#include <stdio.h>
__declspec(dllexport) void test()
{
printf("dll test\n");
}
显式调用
显式调用通过LoadLibrary来载入动态链接库,再通过GetProcAddress函数来获取导出函数地址。
#include <windows.h>
int main()
{
typedef void(*TESTFUNC)(void);
TESTFUNC pTestFunc = NULL;
HINSTANCE hInstance = ::LoadLibrary(L"dlltest.dll");
if (!hInstance)
{
return -1;
}
pTestFunc = (TESTFUNC)GetProcAddress(hInstance, "test");
if (pTestFunc)
{
pTestFunc();
}
return 0;
}
隐式调用
隐式调用通过#pragma comment(lib, “xx.lib”)的方式,将xx.lib这直接加入到工程中来链接,然后通过#include相关头文件就可以直接使用导出函数。
1、设置Include Directories和Library Directories
如果使用Visual Studio,则在Project Property Pages→Configuration Properties→VC++ Directories中设置这两个选项,这两个选项分别用来包括”test.h”所在目录和”test.lib”所在目录。
2、设置库依赖
如果使用Visual Studio,则在Project Property Pages→Configuration Properties→Linker→Input→Additional Dependencies中添加“test.lib”库。或者是直接使用pragma:
#pragma comment(lib, "test.lib")
3、调用代码如下:
#include "test.h"
#pragma comment(lib, "test.lib")
int main()
{
test();
return 0;
}
有关动态库的一些概念
extern C 和 Name-Mangling
Name Mangling就是一种规范编译器和链接器之间用于通信的符号表表示方法的协议,其目的在于按照程序的语言规范,使符号具备足够多的语义信息以保证链接过程准确无误的进行。
然而,C++标准并没有规定Name-Mangling的方案,这就导致了不同编译器使用了不同的方案,进而编译出来的obj文件并非通用。C标准规定了C语言Name-Mangling的规范,任何一个支持C语言的编译器,编译出来的obj文件可以共享。我们来看C和C++编译后的结果:
#include <stdio.h>
__declspec(dllexport) void test()
{
printf("dll test\n");
}
我们分别用C++编译器和C编译器来编译这段代码,使用eXeScope加载该dll,查看导出表可以看出如下信息:
1、C++:
序列 地址 名字
00000001 10011078 ?test@YAXXXZ
2、C
序列 地址 名字
00000001 10011127 test
对于不同的C++编译器,其所得到的函数导出名称很可能不一样,在显示调用这些导出函数时就可能遇到问题,而C编译不会出现这样的问题。
使用extern C声明包裹代码,可以在C++编译器里以C编译器的方式编译代码。
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void test()
{
printf("dll test\n");
}
#ifdef __cplusplus
}
#endif
这段代码在C编译器和C++编译器中编译的结果都是C风格导出函数。
序列 地址 名字
00000001 10011127 test
调用约定
C和C++的默认调用方式为cdecl,在导出函数的时候,使用cdecl和__stdcall调用方式,导出的函数名字也是不一样的。见下例:
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void __cdecl/__stdcall test()
{
printf("dll test\n");
}
#ifdef __cplusplus
}
#endif
我们分别将test函数的调用约定声明为cdecl和stdcall,其结果如下:
1、__cdecl
序列 地址 名字
00000001 10011127 test
2、__stdcall
序列 地址 名字
00000001 10011097 _test@0
建议使用__cdecl调用方式,这样在显式调用导出函数时只需要GetProcess(hinstance, “test”)即可。
为什么需要用C语言封装动态库
在我们平时开发动态库时,即使我们的源码是C++语言,但是在导出的时候通常也会封装成C语言风格,这是为什么呢?
DLL是对应C语言的动态链接技术,如果我们的库只给C++语言使用,不会涉及到多语言调用,那么用C++封装动态库也是可以的。但是如果我们的库需要给VB、C#等语言调用时,C++语言封装的动态库需要通过各种手段才能正常使用,而用C语言封装的动态库只需要显示调用即可。