1. 程式人生 > >C語言動態連結庫DLL的載入

C語言動態連結庫DLL的載入

靜態連結庫在連結時,編譯器會將 .obj 檔案和 .LIB 檔案組織成一個 .exe 檔案,程式執行時,將全部資料載入到記憶體。

如果程式體積較大,功能較為複雜,那麼載入到記憶體中的時間就會比較長,最直接的一個例子就是雙擊開啟一個軟體,要很久才能看到介面。這是靜態連結庫的一個弊端。

動態連結庫有兩種載入方式:隱式載入和顯示載入。
  • 隱式載入又叫載入時載入,指在主程式載入記憶體時搜尋DLL,並將DLL載入記憶體。隱式載入也會有靜態連結庫的問題,如果程式稍大,載入時間就會過長,使用者不能接受。
  • 顯式載入又叫執行時載入,指主程式在執行過程中需要DLL中的函式時再載入。顯式載入是將較大的程式分開載入的,程式執行時只需要將主程式載入記憶體,軟體開啟速度快,
    使用者體驗
    好。

隱式載入

首先建立一個工程,命名為 cDemo,新增原始檔 main.c,內容如下:
  1. #include<stdio.h>
  2. extern int add(int, int); // 也可以是 _declspec(dllimport) int add(int, int);
  3. extern int sub(int, int); // 也可以是 _declspec(dllimport) int sub(int, int);
  4. int main(){
  5. int a=10, b=5;
  6. printf("a+b=%d\n", add(a, b));
  7. printf("a-b=%d\n", sub(a,
     b));
  8. return 0;
  9. }
找到上節建立的 dllDemo 工程,將 debug 目錄下的 dllDemo.lib 和 dllDemo.dll 複製到當前工程目錄下。

前面已經說過:.lib 檔案包含DLL匯出的函式和變數的符號名,只是用來為連結程式提供必要的資訊,以便在連結時找到函式或變數的入口地址;.dll 檔案才包含實際的函式和資料。所以首先需要將 dllDemo.lib 引入到當前專案。

選擇”工程(Project) -> 設定(Settings)“選單,開啟工程設定對話方塊,選擇”連結(link)“選項卡,在”物件/庫模組(Object/library modules)“編輯框中輸入 dllDemo.lib,如下圖所示:


但是這樣引入 .lib 檔案有一個缺點,就是將原始碼提供給其他使用者編譯時,也必須手動引入 .lib 檔案,麻煩而且容易出錯,所以最好是在原始碼中引入 .lib 檔案,如下所示:
#pragma comment(lib, "dllDemo.lib")

更改上面的程式碼:
  1. #include<stdio.h>
  2. #pragma comment(lib, "dllDemo.lib")
  3. _declspec(dllimport) int add(int, int);
  4. _declspec(dllimport) int sub(int, int);
  5. int main(){
  6. int a=10, b=5;
  7. printf("a+b=%d\n", add(a, b));
  8. printf("a-b=%d\n", sub(a, b));
  9. return 0;
  10. }
點選確定回到專案,編譯、連結並執行,輸出結果如下:
Congratulations! DLL is loaded!
a+b=15
a-b=5

在 main.c 中除了用 extern 關鍵字宣告 add() 和 sub() 函式來自外部檔案,還可以用 _declspec(dllimport) 識別符號宣告函式來自動態連結庫。

為了更好的進行模組化設計,最好將 add() 和 sub() 函式的宣告放在標頭檔案中,整理後的程式碼如下:

dllDemo.h
  1. #ifndef _DLLDEMO_H
  2. #define _DLLDEMO_H
  3. #pragma comment(lib, "dllDemo.lib")
  4. _declspec(dllexport) int add(int, int);
  5. _declspec(dllexport) int sub(int, int);
  6. #endif

main.c
  1. #include<stdio.h>
  2. #include "dllDemo.h"
  3. int main(){
  4. int a=10, b=5;
  5. printf("a+b=%d\n", add(a, b));
  6. printf("a-b=%d\n", sub(a, b));
  7. return 0;
  8. }

顯式載入

顯式載入動態連結庫時,需要用到 LoadLibrary() 函式,該函式的作用是將指定的可執行模組對映到呼叫程序的地址空間。LoadLibrary() 函式的原型宣告如下所示:
HMODULE  LoadLibrary(LPCTSTR 1pFileName);

LoadLibrary() 函式不僅能夠載入DLL(.dll),還可以載入可執行模組(.exe)。一般來說,當載入可執行模組時,主要是為了訪問該模組內的一些資源,例如點陣圖資源或圖示資源等。LoadLibrary() 函式有一個字串型別(LPCTSTR)的引數,該引數指定了可執行模組的名稱,既可以是一個.dll檔案,也可以是一個.exe檔案。如果呼叫成功, LoadLibrary() 函式將返回所載入的那個模組的控制代碼。該函式的返回型別是HMODULE。 HMODULE型別和HINSTANCE型別可以通用。

當獲取到動態連結庫模組的控制代碼後,接下來就要想辦法獲取該動態連結庫中匯出函式的地址,這可以通過呼叫 GetProcAddress() 函式來實現。該函式用來獲取DLL匯出函式的 地址,其原型宣告如下所示:
FARPROC  GetProcAddress(HMODULE hModule, LPCSTR 1pProcName);

可以看到,GetProcAddress函式有兩個引數,其含義分別如下所述:
  • hModule:指定動態連結庫模組的控制代碼,即 LoadLibrary() 函式的返回值。
  • 1pProcName:字串指標,表示DLL中函式的名字。

首先建立一個工程,命名為 cDemo,新增原始檔 main.c,內容如下:
  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<windows.h> // 必須包含 windows.h
  4. typedef int (*FUNADDR)(); // 指向函式的指標
  5. int main(){
  6. int a=10, b=5;
  7. HINSTANCEdllDemo = LoadLibrary("dllDemo.dll");
  8. FUNADDRadd, sub;
  9. if(dllDemo){
  10. add = (FUNADDR)GetProcAddress(dllDemo, "add");
  11. sub = (FUNADDR)GetProcAddress(dllDemo, "sub");
  12. }else{
  13. printf("Fail to load DLL!\n");
  14. system("pause");
  15. exit(1);
  16. }
  17. printf("a+b=%d\n", add(a, b));
  18. printf("a-b=%d\n", sub(a, b));
  19. system("pause");
  20. return 0;
  21. }
找到上節建立的 dllDemo 工程,將 debug 目錄下的 dllDemo.dll 複製到當前工程目錄下。注意,只需要 dllDemo.dll,不需要 dllDemo.lib。

執行程式,輸出結果與上面相同。

HMODULE 型別、HINSTANCE 型別在 windows.h 中定義;LoadLibrary() 函式、GetProcAddress() 函式是Win32 API,也在 windows.h 中定義。

通過以上的例子,我們可以看到,隱式載入和顯式載入這兩種載入DLL的方式各有 優點,如果採用動態載入方式,那麼可以在需要時才載入DLL,而隱式連結方式實現起來比較簡單,在編寫程式程式碼時就可以把連結工作做好,在程式中可以隨時呼叫DLL匯出的函式。但是,如果程式需要訪問十多個DLL,如果都採用隱式連結方式載入它們的話, 那麼在該程式啟動時,這些DLL都需要被載入到記憶體中,並對映到呼叫程序的地址空間, 這樣將加大程式的啟動時間。而且,一般來說,在程式執行過程中只是在某個條件滿足時才需要訪問某個DLL中的某個函式,其他情況下都不需要訪問這些DLL中的函式。但是這時所有的DLL都已經被載入到記憶體中,資源浪費是比較嚴重的。在這種情況下,就可以採用顯式載入的方式訪問DLL,在需要時才載入所需的DLL,也就是說,在需要時DLL才會被載入到記憶體中,並被對映到呼叫程序的地址空間中。有一點需要說明的是,實際上, 採用隱式連結方式訪問DLL時,在程式啟動時也是通過呼叫LoadLibrary() 函式載入該程序需要的動態連結庫的。