靜態鏈接庫在鏈接時,編譯器會將 .obj 文件和 .LIB 文件組織成一個 .exe 文件,程序運行時,將全部數據加載到內存。
如果程序體積較大,功能較爲複雜,那麼加載到內存中的時間就會比較長,最直接的一個例子就是雙擊打開一個軟件,要很久才能看到界面。這是靜態鏈接庫的一個弊端。
動態鏈接庫有兩種加載方式:隱式加載和顯示加載。
- 隱式加載又叫載入時加載,指在主程序載入內存時搜索DLL,並將DLL載入內存。隱式加載也會有靜態鏈接庫的問題,如果程序稍大,加載時間就會過長,用戶不能接受。
- 顯式加載又叫運行時加載,指主程序在運行過程中需要DLL中的函數時再加載。顯式加載是將較大的程序分開加載的,程序運行時只需要將主程序載入內存,軟件打開速度快,用戶體驗好。
隱式加載
首先創建一個工程,命名爲 cDemo,添加源文件 main.c,內容如下:- #include<stdio.h>
- extern int add(int, int); // 也可以是 _declspec(dllimport) int add(int, int);
- extern int sub(int, int); // 也可以是 _declspec(dllimport) int sub(int, int);
- int main(){
- int a=10, b=5;
- printf("a+b=%d\n", add(a, b));
- printf("a-b=%d\n", sub(a, b));
- return 0;
- }
前面已經說過:.lib 文件包含DLL導出的函數和變量的符號名,只是用來爲鏈接程序提供必要的信息,以便在鏈接時找到函數或變量的入口地址;.dll 文件才包含實際的函數和數據。所以首先需要將 dllDemo.lib 引入到當前項目。
選擇”工程(Project) -> 設置(Settings)“菜單,打開工程設置對話框,選擇”鏈接(link)“選項卡,在”對象/庫模塊(Object/library modules)“編輯框中輸入 dllDemo.lib,如下圖所示:
但是這樣引入 .lib 文件有一個缺點,就是將源碼提供給其他用戶編譯時,也必須手動引入 .lib 文件,麻煩而且容易出錯,所以最好是在源碼中引入 .lib 文件,如下所示:
#pragma comment(lib, "dllDemo.lib")
更改上面的代碼:
- #include<stdio.h>
- #pragma comment(lib, "dllDemo.lib")
- _declspec(dllimport) int add(int, int);
- _declspec(dllimport) int sub(int, int);
- int main(){
- int a=10, b=5;
- printf("a+b=%d\n", add(a, b));
- printf("a-b=%d\n", sub(a, b));
- return 0;
- }
Congratulations! DLL is loaded!
a+b=15
a-b=5
在 main.c 中除了用 extern 關鍵字聲明 add() 和 sub() 函數來自外部文件,還可以用 _declspec(dllimport) 標識符聲明函數來自動態鏈接庫。
爲了更好的進行模塊化設計,最好將 add() 和 sub() 函數的聲明放在頭文件中,整理後的代碼如下:
dllDemo.h
- #ifndef _DLLDEMO_H
- #define _DLLDEMO_H
- #pragma comment(lib, "dllDemo.lib")
- _declspec(dllexport) int add(int, int);
- _declspec(dllexport) int sub(int, int);
- #endif
main.c
- #include<stdio.h>
- #include "dllDemo.h"
- int main(){
- int a=10, b=5;
- printf("a+b=%d\n", add(a, b));
- printf("a-b=%d\n", sub(a, b));
- return 0;
- }
顯式加載
顯式加載動態鏈接庫時,需要用到 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,內容如下:
- #include<stdio.h>
- #include<stdlib.h>
- #include<windows.h> // 必須包含 windows.h
- typedef int (*FUNADDR)(); // 指向函數的指針
- int main(){
- int a=10, b=5;
- HINSTANCE dllDemo = LoadLibrary("dllDemo.dll");
- FUNADDR add, sub;
- if(dllDemo){
- add = (FUNADDR)GetProcAddress(dllDemo, "add");
- sub = (FUNADDR)GetProcAddress(dllDemo, "sub");
- }else{
- printf("Fail to load DLL!\n");
- system("pause");
- exit(1);
- }
- printf("a+b=%d\n", add(a, b));
- printf("a-b=%d\n", sub(a, b));
- system("pause");
- return 0;
- }
運行程序,輸出結果與上面相同。
HMODULE 類型、HINSTANCE 類型在 windows.h 中定義;LoadLibrary() 函數、GetProcAddress() 函數是Win32 API,也在 windows.h 中定義。
通過以上的例子,我們可以看到,隱式加載和顯式加載這兩種加載DLL的方式各有 優點,如果採用動態加載方式,那麼可以在需要時才加載DLL,而隱式鏈接方式實現起來比較簡單,在編寫程序代碼時就可以把鏈接工作做好,在程序中可以隨時調用DLL導出的函數。但是,如果程序需要訪問十多個DLL,如果都採用隱式鏈接方式加載它們的話, 那麼在該程序啓動時,這些DLL都需要被加載到內存中,並映射到調用進程的地址空間, 這樣將加大程序的啓動時間。而且,一般來說,在程序運行過程中只是在某個條件滿足時才需要訪問某個DLL中的某個函數,其他情況下都不需要訪問這些DLL中的函數。但是這時所有的DLL都已經被加載到內存中,資源浪費是比較嚴重的。在這種情況下,就可以採用顯式加載的方式訪問DLL,在需要時才加載所需的DLL,也就是說,在需要時DLL纔會被加載到內存中,並被映射到調用進程的地址空間中。有一點需要說明的是,實際上, 採用隱式鏈接方式訪問DLL時,在程序啓動時也是通過調用LoadLibrary() 函數加載該進程需要的動態鏈接庫的。