庫的建立與使用(二)——動態連接庫(上)

庫的建立與使用(二)——動態連接庫(上)

上一篇文章介紹了靜態庫的基本概念與使用,可是靜態庫在有些場合下使用起來有明顯的資源浪費問題、個別時候使用靜態庫將會極其麻煩甚至沒法使用。那麼這個時候咱們就須要用別的方式,也就是本節所要介紹的主角——動態連接庫。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下的動態連接庫的強大功能。

相關文章
相關標籤/搜索