孫鑫 Lesson19 動態鏈接庫 顯示與隱式

靜態鏈接庫在鏈接時,編譯器會將 .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. HINSTANCE dllDemo = LoadLibrary("dllDemo.dll");
  8. FUNADDR add, 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() 函數加載該進程需要的動態鏈接庫的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章