2

库的创建与使用(二)——动态链接库(上)

上一篇文章介绍了静态库的基本概念与使用,但是静态库在有些场合下使用起来有明显的资源浪费问题、个别时候使用静态库将会极其麻烦甚至无法使用。那么这个时候我们就需要用别的方式,也就是本节所要介绍的主角——动态链接库。

认识动态链接库

什么是动态链接库?答案很简单:库。动态库与静态库一样,都是库,也就是都是实现代码重用的一种手段,都是函数与数据的集合。从名字上能看出,两者的区别在于静态库是要在编译时进行链接——此时链接器已经知道要去链接哪一个静态库,而动态库则是在程序中任何时间都可以由程序编写者手工控制进行动态地链接。

与静态库的对比

  1. 更加灵活

    上面的解释可能会显得动态库有些多余,因为静态库明明可以完成工作,为什么还要再引入动态库这个概念?所以下面重点介绍下静态库的局限性与不得不使用动态库的理由。

静态库因为是在编译之前就要引入到工程中的,所以必须一次指定要在程序用到的所有静态库并告知链接器。而动态库则不然,调用动态库的工作可以在程序真正需要使用动态库中的某些功能时才进行链接,并且随时可以卸载所调用的动态库。

  1. 资源利用更合理

    静态库中包括的源代码将会在链接时全部塞进编译后的可执行文件中,所以编译出的程序都比较大——参考<u>静态编译</u>。而动态库则不然,动态库文件虽然在磁盘中,但是只有程序运行才会把需要用到的部分加载到内存中供程序调用。

  2. 支持更复杂的引入与调用

    静态库是不支持在一个库中再引入另一个库的,而动态库则没有这个限制,这在完成一些复杂调用的时候还是很有帮助的。

  3. 支持多个模块程序同时调用

    动态链接库最功能强大的特性就是,一个动态库可以同时被若干程序调用,而内存中只需要加载一份代码。这引申含义就是说, 一个程序的动态链接库可以被其他程序调用。再说得直白一些,任何一个动态链接库,理论上我们写的程序都可以调用其提供的函数和数据。但注意,是 理论上, 具体原因下面会讲解。

  4. 更多的高级特性

    动态链接库因为其灵活的特性,实际使用的时候往往可以做出很多更高级的特技,比如在Windows下很实用的DLL注入技术。这些高级特技在熟悉了动态链接库之后,自然都会在程序中慢慢发掘、应用。

创建动态链接库

创建动态链接库和静态库的步骤差不多,只不过要选中相应的选项。<br>
为了方便理解,这里使用最简单的代码示例。
先创建一个calc.h,在里面写上如下代码:

#ifndef CALC_H__2832ab37_92f0_4fa4_ad9c_7c5570c90c7f
#define CALC_H__2832ab37_92f0_4fa4_ad9c_7c5570c90c7f

//define CALC_API macro to export or import
#ifdef DYNAMICLIBRARY_EXPORTS
#define CALC_API __declspec(dllexport)
#else
#define CALC_API __declspec(dllimport)
#endif

CALC_API int Add(int x, int y);

class CALC_API Rectangle
{
public:
    Rectangle(unsigned int length = 0, unsigned int width = 0);
    unsigned int Area() const;
private:
    unsigned int m_uiLength;
    unsigned int m_uiWidth;
};

#endif

导出与导入

其中,最重要的就是中间的 条件预编译指令 ,这是为了能让动态库能把我们想要供外界调用的接口导出;否则,没有导出的函数与类,外部程序是不能调用的。如果每个要导出的接口前都手动地去添加导出代码,不仅写起来不方便,而且程序也不易读。另一方面,在使用DLL中导出的接口时,用户程序也需要将其导入,否则链接器还是没法正常工作的。更重要的是,在发布DLL的时候,我们要把对应的头文件也发布出去——当然,也可以不发布,如果你认为其他人可以猜出来你导出的函数和类的声明的话——那这样我们就相当于得写两份声明头文件,这种可笑的事情肯定是不会有开发者会去做的。所以,为了避免麻烦,我们用如上的方式来让这个头文件既能让库的开发者使用,又能供用户程序使用。
首先,预编译器先检查当前是否定义了DYNAMICLIBRARY_EXPORTS这个宏,如果定义了,那么把宏CALC_API定义为导出用的宏;如果没定义,那么宏CALC_API就被定义为导入用的宏。而宏DYNAMICLIBRARY_EXPORTS是库的开发者定义的,所以用户程序的预编译器是找不到这个宏定义的。使用这种方式,可以将这个头文件供双方使用。当然,还差一个问题,宏DYNAMICLIBRARY_EXPORTS是在哪定义的呢?这里,我使用命令行级别定义,在项目属性中如下操作:
命令行级别定义宏
当然,也可以用其他的思路来修改这时的条件编译指令,比如:

#ifndef CALC_API
#define CALC_API __declspec(dllimport)
#else
#endif

然后在库的实现文件中的第一行加入:

#define CALC_API __declspec(dllexport)
#include "calc.h"
...

这种方式也是可以的,但就是需要在每个实现文件中都加上这些宏定义。然后我们来实现:

#include "calc.h"
#include <iostream>
using namespace std;

int Add(int x, int y)
{
    return x + y;
}

Rectangle::Rectangle(unsigned int length /* = 0 */, unsigned int width /* = 0 */)
{
    this->m_uiLength = length;
    this->m_uiWidth = width;
}

unsigned int Rectangle::Area() const
{
    return this->m_uiLength * this->m_uiWidth;
}

然后编译,生成DLL文件。

调用动态链接库

生成的文件中,有.lib文件和.exp文件,当然,还有最重要的.dll文件。这里的.lib文件并不是之前介绍的静态库,而是导入库,里面并没有实际的代码。.exp这里我们用不到,不在本文介绍。在程序中使用动态链接库也有两种办法,但是不要和静态库相混淆。

方法一

第一种办法是利用导入库.lib文件和刚刚所写的头文件,这种方法也是我最推荐的,尽管在发布库的时候要同时发布这三个文件。
在我们的示例调用客户程序中,代码如下:

#include "include/calc.h"
#include <iostream>
using namespace std;

#pragma comment(lib, "DynamicLibrary.lib")

int main()
{
    cout << "1 + 3 = " << Add(1, 3) << endl;

    return EXIT_SUCCESS;
}

方法二

这种办法我并不推荐,但是还是要了解一下:

#include <iostream>
#include <Windows.h>
#include <tchar.h>
using namespace std;

typedef int(*pAdd)(int a, int b);

int main()
{
    HMODULE hInst = LoadLibrary(_T("DynamicLibrary.dll"));
    pAdd Add = (pAdd)GetProcAddress(hInst, "?Add@@YAHHH@Z");
    if (Add != nullptr)
    {
        cout << "1 + 3 = " << Add(1, 3) << endl;
    }
    else
    {
        cout << "failure" << endl;
    }
    FreeLibrary(hInst);
    return EXIT_SUCCESS;
}

显然,这种方法只需要有拥有.dll文件就可以了,不过也能看出来,最大的问题就在在获取导出的函数的入口地址的时候,我们需要提供 函数名。这个名字是被编译器修改过后的,这涉及到了编译器在编译过程中的名字修饰,这和调用约定有关,我将在之后的文章中讨论这些进阶内容,不在本文阐述。
这里的示例代码只测试了导出的函数,导出的类请自行测试。并且在导出类时,可以不把整个类都导出,而是只导出某些成员,在声明文件中修改如下:

    unsigned int CALC_API Area() const;

而且要记住,导出并不能改变类内成员的访问权限。

中场休息

本文限于篇幅暂且结束,在下一篇文章中将会更进一步地看到Windows下的动态链接库的强大功能。


Soap
78 声望25 粉丝