库的建立与使用(二)——动态连接库(上)

库的建立与使用(二)——动态连接库(上)

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

认识动态连接库

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

与静态库的对比

  1. 更加灵活

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

静态库由于是在编译以前就要引入到工程中的,因此必须一次指定要在程序用到的全部静态库并告知连接器。而动态库则否则,调用动态库的工做能够在程序真正须要使用动态库中的某些功能时才进行连接,而且随时能够卸载所调用的动态库。ui

  1. 资源利用更合理

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

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

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

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

    动态连接库最功能强大的特性就是,一个动态库能够同时被若干程序调用,而内存中只须要加载一份代码。这引伸含义就是说, 一个程序的动态连接库能够被其余程序调用。再说得直白一些,任何一个动态连接库,理论上咱们写的程序均可以调用其提供的函数和数据。但注意,是 理论上, 具体缘由下面会讲解。命令行

  4. 更多的高级特性

    动态连接库由于其灵活的特性,实际使用的时候每每能够作出不少更高级的特技,好比在Windows下很实用的DLL注入技术。这些高级特技在熟悉了动态连接库以后,天然都会在程序中慢慢发掘、应用。code

建立动态连接库

建立动态连接库和静态库的步骤差很少,只不过要选中相应的选项。<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下的动态连接库的强大功能。

相关文章
相关标签/搜索