動態連接庫DLL_第1篇

動態連接庫一般不能直接運行,也不能接收消息。它們是一些獨立的文件,其中包含能被可執行程序或其餘DLL調用來完成某項工做的函數。只有在其餘模塊調用動態連接庫中的函數時,它才發揮做用。實際編程時,可把完成某種功能的函數放在一個動態連接庫中,提供給其餘程序調用。編程

Windows API中全部函數都包含在DLL中,比較重要的有3個,分別爲:函數

一、Kernel32.dllspa

包含用於管理內存、進程和線程的函數。線程

二、User32.dllcode

包含用於執行用戶界面任務(如窗口建立和消息傳遞的函數等)的函數。orm

三、GDI32.dllblog

包含用於畫圖和顯示文本的函數。進程

 

靜態庫和動態庫內存

一、靜態庫文檔

函數和數據被編譯成一個二進制文件,擴展名爲.LIB。使用靜態庫時,編譯連接可執行文件時,連接器從庫中複製這些函數和數據並把它們和應用程序的其餘模塊組合起來建立最終的可執行文件。發佈產品時,只須要發佈這個可執行文件,並不須要發佈被使用的靜態庫。

二、動態庫

須要提供2個文件:引入庫.lib文件和DLL文件。動態庫的引入庫.lib文件與靜態庫雖而後綴相同,但含義徹底不一樣。引入庫文件.lib包含DLL導出的函數和變量的符號名,.dll文件包含DLL實際的函數和數據。編譯連接時,只須要連接DLL的引入庫文件,DLL中的函數代碼和數據並不複製到可執行文件中,直到可執行程序運行時,纔去加載須要的DLL,將該DLL映射到進程的地址空間,而後訪問DLL中的導出函數。發佈產品時,除了發佈可執行文件,還要同時發佈動態連接庫。

 

若是多個應用程序使用同一個DLL,該DLL的頁面只須要放入內存一次,全部的應用程序均可以共享它的頁面。

有兩種加載動態連接庫的方式:隱式連接和顯式加載。

應用程序若是想訪問某個DLL中的函數,該函數必須是已經被導出的函數。爲了讓DLL導出一些函數,須要在每個將要被導出的函數前面添加標誌符_declspec(dllexport)。

客戶調用DLL的導出函數時,必須先申明該函數是外部的extern。除了使用extern關鍵字代表函數是外部定義的以外,還可使用_declspec(dllimport)來代表函數是從動態連接庫中引入的。使用_declspec(dllimport)聲明外部函數,可以明確告訴編譯器函數是從動態連接庫中引入的,編譯器能夠生成運行效率更高的代碼。

一般在編寫動態連接庫時,都會提供一個頭文件,在此文件中提供DLL導出函數原型的聲明,以及函數的有關注釋文檔。程序編譯時,頭文件不參與編譯,源文件單獨編譯。

dll_1.h文件

#ifdef DLL1_API
#else 
#define DLL1_API _declspec(dllimport)
#endif

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

dll_1.cpp文件

#define DLL1_API _declspec(dllexport)
#include "dll_1.h"

DLL1_API int add(int a, int b)
{
    return a + b;
}

DLL1_API int subtract(int a, int b)
{
    return a - b;
}

 

動態連接庫還能夠導出C++類。

dll_1.h文件

#ifdef DLL1_API
#else 
#define DLL1_API _declspec(dllimport)
#endif

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

class DLL1_API point {
public:
    void Output(int x, int y);
};

dll_1.cpp文件

#define DLL1_API _declspec(dllexport)
#include "dll_1.h"

DLL1_API int add(int a, int b)
{
    return a + b;
}

DLL1_API int subtract(int a, int b)
{
    return a - b;
}

void point::Output(int x, int y)
{
    HWND hwnd = GetForegroundWindow();
    HDC hdc = GetDC(hwnd);
    TCHAR buf[20] = { '\0' };
    swprintf(buf, 20, _T("x = %d, y = %d"), x, y);
    TextOut(hdc, 0, 0, buf, wcslen(buf));
    ReleaseDC(hwnd, hdc);
}

若是隱式連接加載DLL,.dll更新後,須要將新的.dll和.lib文件複製到相應工程目錄。

動態連接庫還能夠不導出整個C++類,而只導出C++類的某些函數。

dll_1.h文件

class/* DLL1_API*/ point {
public:
    void DLL1_API Output(int x, int y);
    void test();
};

若是聲明類時,指定了導出標誌,該類中的全部函數都將被導出,不然只有那些聲明時指定了導出標誌的類成員函數才被導出。

 

名字改編問題

C++編譯器生成DLL時,會對導出函數的名字進行改編,不一樣編譯器使用的改編規則不一樣,改編後的名字是不同的。若是利用不一樣的編譯器分別生成DLL和訪問DLL的客戶端程序,客戶端程序訪問DLL的導出函數時就會出現問題。所以,但願動態連接庫文件編譯時,導出函數的名稱不要發生改變。作法是,定義導出函數時,加上限定符extern 「C」,字母C大寫。

dll_1.h文件

#ifdef DLL1_API
#else 
#define DLL1_API extern "C" _declspec(dllimport)
#endif

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

dll_1.cpp文件

#define DLL1_API extern "C" _declspec(dllexport)
#include "dll_1.h"

DLL1_API int add(int a, int b)
{
    return a + b;
}

DLL1_API int subtract(int a, int b)
{
    return a - b;
}

extern 「C」能夠解決C++和C語言之間相互調用時函數命名的問題,但該方法不能用於導出一個類的成員函數,只能用於導出全局函數。若是導出函數的調用約定發生改變,即便使用限定符extern 「C」,函數的名字仍會發生改變。

 

顯示加載方式加載DLL

LoadLibrary的做用是將指定的可執行模塊映射到調用進程的地址空間,返回值是加載模塊的句柄。GetProcAddress函數用來得到導出函數的地址。動態加載DLL時,客戶端再也不須要包含導出函數聲明的頭文件和引入庫文件,只須要.dll文件便可。

void CMFCApplication1Dlg::OnBnClickedBtnSub()
{
    HMODULE HInst = LoadLibrary(_T("ConsoleApplication1.dll"));
    if (HInst == NULL)
        return;
    typedef int (*ADDPROC)(int a, int b);

    ADDPROC Add = (ADDPROC)GetProcAddress(HInst, "add");
    if (!Add) {
        MessageBox(_T("獲取函數地址失敗!"));
        return;
    }
    CString str;
    str.Format(_T("5 + 3 = %d."), Add(5, 3));
    MessageBox(str);
}

實際上採用隱式連接方式訪問DLL時,程序啓動時也是經過調用LoadLibrary函數加載該進程的動態連接庫的。

若是採用動態加載方式使用DLL,訪問DLL的客戶端程序,若是對DLL的訪問已經完成,再也不須要訪問該DLL時,應調用FreeLibrary函數釋放該DLL。FreeLibrary主要減小被加載的DLL的引用計數。當計數變爲0時,該DLL模塊將從調用進程的地址空間卸載。

 

DllMain函數

DLL的入口函數是DllMain,該函數是可選的。編寫DLL程序時,能夠提供也能夠不提供DllMain函數。若是提供了DllMain函數,系統加載該DLL時,就會調用該函數。

不該該在DllMain函數中進行復雜的調用,由於加載該動態連接庫時,可能還有一些核心動態連接庫沒有被加載。若是本身編寫的DllMain函數須要調用核心動態連接庫中的某些函數,就會致使程序終止。

 

參考資料:

一、《VC++深刻詳解》

相關文章
相關標籤/搜索