關(guān)于靜態(tài)鏈接庫(Lib)與動態(tài)鏈接庫(DLL)
Posted on 2011-04-19 22:58 RTY 閱讀(1303) 評論(0) 編輯 收藏 引用 所屬分類: C/C++靜態(tài)鏈接庫(Lib)和動態(tài)鏈接庫(DLL)的問題困擾了我很長時間,而當(dāng)中關(guān)鍵的問題是兩者有何聯(lián)系?又有何區(qū)別呢?怎么創(chuàng)建?怎么使用?使用的過程中要注意什么?一直想把這個問題總結(jié)一下。
在windows下一般可以看到后綴為dll和后綴為lib的文件,但這兩種文件可以分為三種庫,分別是動態(tài)鏈接庫(Dynamic-Link Libraries),目標(biāo)庫(Object Libraries)和導(dǎo)入庫(Import Libraries),下面一一解釋這三種庫。
目標(biāo)庫(Object Libraries)
目標(biāo)庫又叫靜態(tài)鏈接庫,是擴展名為.LIB的文件,包括了用戶程序要用到的各種函數(shù)。它在用戶程序進行鏈接時,“靜態(tài)鏈接”到可執(zhí)行程序文件當(dāng)中。例如,在VC++中最常使用到的C運行時目標(biāo)庫文件就是LIBC.LIB。在鏈接應(yīng)用程序時常使用所謂“靜態(tài)鏈接”的方法,即將各個目標(biāo)文件(.obj)、運行時函數(shù)庫(.lib)以及已編譯的資源文件(.res)鏈接到一起,形成一個可執(zhí)行文件(.exe)。使用靜態(tài)鏈接時,可執(zhí)行文件需要使用的各種函數(shù)和資源都已包含到文件中。這樣做的缺點是對于多個程序都使用的相同函數(shù)和資源要重復(fù)鏈接到exe文件中,使程序變大、占用內(nèi)存增加。
導(dǎo)入庫(Import Libraries)
導(dǎo)入庫是一種特殊形式的目標(biāo)庫文件形式。和目標(biāo)庫文件一樣,導(dǎo)入庫文件的擴展名也是.LIB,也是在用戶程序被鏈接時,被“靜態(tài)鏈接”到可執(zhí)行文件當(dāng)中。但是不同的是,導(dǎo)入庫文件中并不包含有程序代碼。相應(yīng)的,它包含了相關(guān)的鏈接信息,幫助應(yīng)用程序在可執(zhí)行文件中建立起正確的對應(yīng)于動態(tài)鏈接庫的重定向表。比如KERNEL32.LIB、USER32.LIB和GDI32.LIB就是我們常用到的導(dǎo)入庫,通過它們,我們就可以調(diào)用Windows提供的函數(shù)了。如果我們在程序中使用到了Rectangle這個函數(shù),GDI32.LIB就可以告訴鏈接器,這個函數(shù)在GDI32.DLL動態(tài)鏈接庫文件中。這樣,當(dāng)用戶程序運行時,它就知道“動態(tài)鏈接”到GDI32.DLL模塊中以使用這個函數(shù)。其實說白了導(dǎo)入庫就是一個索引,一個dll動態(tài)鏈接庫的索引表,這是我的理解。
動態(tài)鏈接庫(Dynamic-Link Libraries)
“動態(tài)鏈接”是將一些公用的函數(shù)或資源組織成動態(tài)鏈接庫文件(.dll),當(dāng)某個需要使用dll中的函數(shù)或資源的程序啟動時(準(zhǔn)確的說是初始化時),系統(tǒng)將該dll映射到調(diào)用進程的虛擬地址空間、增加該dll的引用計數(shù)值,然后當(dāng)實際使用到該dll時操作系統(tǒng)就將該dll載入內(nèi)存;如果使用該dll的所有程序都已結(jié)束,則系統(tǒng)將該庫從內(nèi)存中移除。使用同一dll的各個進程在運行時共享dll的代碼,但是對于dll中的數(shù)據(jù)則各有一份拷貝(當(dāng)然也有在dll中共享數(shù)據(jù)的方法)。 動態(tài)鏈接庫中可以定義兩種函數(shù):輸出函數(shù)和內(nèi)部函數(shù)。輸出函數(shù)可以被其他模塊調(diào)用,內(nèi)部函數(shù)只能被動態(tài)鏈接庫本身調(diào)用。動態(tài)鏈接庫也可以輸出數(shù)據(jù),但這些數(shù)據(jù)通常只被它自己的函數(shù)所使用。
如我們所知,Windows程序都是一些可執(zhí)行文件,它們可以創(chuàng)建并顯示一個或多個窗體,使用消息循環(huán)來接收用戶的輸入。但是動態(tài)鏈接庫并不能直接被執(zhí)行,它們一般也不會接收消息。它們只是一些包含著函數(shù)的獨立文件,這些函數(shù)可以被Windows程序或者其它DLL調(diào)用以完成某項任務(wù)。
“動態(tài)鏈接”是指Windows程序在運行時才把自己需要存在于某個庫中的函數(shù)鏈接進來。“靜態(tài)鏈接”是指Windows程序在編譯階段就把各種對象模塊(.OBJ)、運行時庫(.LIB)和資源文件(.RES)鏈接到一起以創(chuàng)建一個可執(zhí)行文件(.EXE)。
DERNAL32.DLL,USER32.DLL,GDI32.DLL,各種驅(qū)動程序如KEYBOARD.DRV,SYSTEM.DRV和MOUSE.DRV,顯卡和打印機驅(qū)動程序等都是動態(tài)鏈接庫。這些庫可以被所有的Windows程序共同使用。
有某些動態(tài)鏈接庫(如字體文件)稱為“resource-only”。它們只包括數(shù)據(jù),而不包括代碼。因此,動態(tài)鏈接庫的目的之一就是為許多不同的程序提供函數(shù)和資源。在傳統(tǒng)的操作系統(tǒng)里,用戶程序在運行時只能調(diào)用操作系統(tǒng)自身的某些函數(shù)。而在Windows操作系統(tǒng)下,模塊或程序調(diào)用另一個模塊中的函數(shù)來執(zhí)行是一種非常普遍的操作。因此,從某種角度看,對DLL進行編程,其實是在對Windows操作系統(tǒng)作擴展,也可以看作是在對用戶程序作擴展。
動態(tài)鏈接庫模塊可以有其它的擴展名,但是標(biāo)準(zhǔn)的擴展名是.DLL。只有具有標(biāo)準(zhǔn)擴展句的動態(tài)鏈接庫模塊才可以被Windows自動加載。而如果是其它擴展名的動態(tài)鏈接庫模塊,程序必須使用LoadLibrary或者LoadLibraryEx函數(shù)來顯示加載。
我們可以發(fā)現(xiàn),在大型的應(yīng)用軟件中,會常常使用到動態(tài)鏈接庫技術(shù)。舉個例子,假如我們要寫一個大型的應(yīng)用軟件,其中包括了多個程序。我們可以發(fā)現(xiàn)很多程序可能都會使用到一些同樣的通用的函數(shù)。我們可以把這些通用的函數(shù)放到某個目標(biāo)庫文件中(.LIB),然后在鏈接是把它加到每個程序中進行靜態(tài)鏈接。但是這是一種非常浪費的方法,因為每個程序模塊中都會包括這些通用函數(shù)的獨立拷貝。另外,如果我們要改變庫文件中的某個函數(shù),就必須把所有使用到這個函數(shù)的程序都重新編譯一遍。但是,如果我們使用動態(tài)鏈接庫的技術(shù),把所有這些通用函數(shù)都放到一個動態(tài)鏈接庫文件當(dāng)中,我們就可以解決以上提到的各種問題。首先,動態(tài)鏈接庫在硬盤上只保留一個拷貝,程序只是在運行時才會調(diào)用其中使用到的函數(shù),這樣我們就可以節(jié)省大量的程序存儲和運行空間。其次,如果要修改某個通用函數(shù)時,只要調(diào)用接口沒有改變,只是改變它的實現(xiàn)方法,那么我們就不必對每個用到它的程序都進行重新編譯,而只要把動態(tài)鏈接庫模塊重新編譯一遍就可以了。
動態(tài)鏈接庫模塊也可以作為一個單獨的產(chǎn)品來發(fā)布。這樣程序開發(fā)人員就可以使用第三方的模塊來開發(fā)自己的應(yīng)用程序,提高了程序的復(fù)用程序,也節(jié)省了大量的時間和精力。
目標(biāo)庫和導(dǎo)入庫都是在程序開發(fā)過程中才使用到的,而動態(tài)鏈接庫是在程序運行時才使用的。在程序運行時,相應(yīng)的動態(tài)鏈接庫文件必須已經(jīng)保存在硬盤上了。另外,如果要使用動態(tài)鏈接庫文件,該文件必須要保存在同.EXE文件同一個目錄下,或者保存在當(dāng)前目錄、Windows系統(tǒng)目錄、Windows目錄或環(huán)境變量中PATH參數(shù)指定的目錄下。程序也是按照這個順序來搜尋它需要的動態(tài)鏈接庫文件的。
創(chuàng)建靜態(tài)鏈接庫(Lib)
創(chuàng)建靜態(tài)鏈接庫比較簡單,創(chuàng)建win32控制臺程序,選擇靜態(tài)庫,這里我沒有選擇上預(yù)編譯頭。生成工程以后就像定義一般的函數(shù)般,定義放在頭文件,然后實現(xiàn)放在cpp文件里頭,直接build就出來一個靜態(tài)的lib了,發(fā)布時附上頭文件給使用者就可以。
創(chuàng)建動態(tài)鏈接庫(DLL)
用SDK創(chuàng)建一個簡單的dll文件
在VC++中選擇新建一個Win32 Dynamic-Link Library。需要建立一個c/c++ head file和一個c/c++ source file并加入工程。頭文件中內(nèi)容為輸出函數(shù)的聲明,源文件中內(nèi)容為DllMain函數(shù)和輸出函數(shù)的定義。下面是一個最簡單的例子。
頭文件代碼如下:

#ifdef TEST_CREATE_DLL_EXPORTS
#define TEST_CREATE_DLL_API __declspec(dllexport)
#else
#define TEST_CREATE_DLL_API __declspec(dllimport)
#endif
// This class is exported from the test_create_dll.dll
class TEST_CREATE_DLL_API Ctest_create_dll {
public:
Ctest_create_dll(void);
// TODO: add your methods here.
};
extern TEST_CREATE_DLL_API int ntest_create_dll;
TEST_CREATE_DLL_API int fntest_create_dll(void);
在創(chuàng)建工程的時候????TEST_CREATE_DLL_EXPORTS就已經(jīng)在預(yù)定義處定義過,生成導(dǎo)出dll。
頭文件預(yù)處理中的__declspec是微軟增加的“C擴展類存儲屬性”(C Extended Storage-Class Attributes),它指明一個給出的實例被存儲為一種微軟特定的類存儲屬性,可以為thread,naked,dllimport或dllexport. [MSDN原文:The extended attribute syntax for specifying storage-class information uses the __declspec keyword, which specifies that an instance of a given type is to be stored with a Microsoft-specific storage-class attribute (thread, naked, dllimport, or dllexport).] 輸出函數(shù)必須指明為CALLBACK。 DllMain是dll的入口點函數(shù)。也可以不寫它。DllMain必須返回TRUE,否則系統(tǒng)將終止程序并彈出一個“啟動程序時出錯”對話框。 編譯鏈接后,得到動態(tài)鏈接庫文件dlldemo.dll和輸入庫文件dlldemo.lib。
_declspec(dllexport)
聲明一個導(dǎo)出函數(shù),是說這個函數(shù)要從本DLL導(dǎo)出。我要給別人用。一般用于dll中 。
省掉在DEF文件中手工定義導(dǎo)出哪些函數(shù)的一個方法。當(dāng)然,如果你的DLL里全是C++的類的話,你無法在DEF里指定導(dǎo)出的函數(shù),只能用__declspec(dllexport)導(dǎo)出類。
__declspec(dllimport)
聲明一個導(dǎo)入函數(shù),是說這個函數(shù)是從別的DLL導(dǎo)入。我要用。一般用于使用某個dll的exe中 。
不使用 __declspec(dllimport) 也能正確編譯代碼,但使用 __declspec(dllimport) 使編譯器可以生成更好的代碼。編譯器之所以能夠生成更好的代碼,是因為它可以確定函數(shù)是否存在于 DLL 中,這使得編譯器可以生成跳過間接尋址級別的代碼,而這些代碼通常會出現(xiàn)在跨 DLL 邊界的函數(shù)調(diào)用中。但是,必須使用 __declspec(dllimport) 才能導(dǎo)入 DLL 中使用的變量。
相信寫WIN32程序的人,做過DLL,都會很清楚__declspec(dllexport)的作用,它就是為了省掉在DEF文件中手工定義導(dǎo)出哪些函數(shù)的一個方法。當(dāng)然,如果你的DLL里全是C++的類的話,你無法在DEF里指定導(dǎo)出的函數(shù),只能用__declspec(dllexport)導(dǎo)出類。但是,MSDN文檔里面,對于__declspec(dllimport)的說明讓人感覺有點奇怪,先來看看MSDN里面是怎么說的:
不使用 __declspec(dllimport) 也能正確編譯代碼,但使用 __declspec(dllimport) 使編譯器可以生成更好的代碼。編譯器之所以能夠生成更好的代碼,是因為它可以確定函數(shù)是否存在于 DLL 中,這使得編譯器可以生成跳過間接尋址級別的代碼,而這些代碼通常會出現(xiàn)在跨 DLL 邊界的函數(shù)調(diào)用中。但是,必須使用 __declspec(dllimport) 才能導(dǎo)入 DLL 中使用的變量。
extern "C"
指示編譯器用C語言方法給函數(shù)命名。
在制作DLL導(dǎo)出函數(shù)時由于C++存在函數(shù)重載,因此__declspec(dllexport) function(int,int) 在DLL會被decorate,例如被decorate成為function_int_int,而且不同的編譯器decorate的方法不同,造成了在用GetProcAddress取得function地址時的不便,使用extern "C"時,上述的decorate不會發(fā)生,因為C沒有函數(shù)重載,但如此一來被extern"C"修飾的函數(shù),就不具備重載能力,可以說extern 和extern "C"不是一回事。
C++編譯器在生成DLL時,會對導(dǎo)出的函數(shù)進行名字改編,并且不同的編譯器使用的改變規(guī)則不一樣,因此改編后的名字會不一樣。這樣,如果利用不同的編譯器分別生成DLL和訪問該DLL的客戶端代碼程序的話,后者在訪問該DLL的導(dǎo)出函數(shù)時會出現(xiàn)問題。為了實現(xiàn)通用性,需要加上限定符:extern “C”。
但是利用限定符extern “C”可以解決C++和C之間相互調(diào)用時函數(shù)命名的問題,但是這種方法有一個缺陷,就是不能用于導(dǎo)出一個類的成員函數(shù),只能用于導(dǎo)出全局函數(shù)。LoadLibrary導(dǎo)入的函數(shù)名,對于非改編的函數(shù),可以寫函數(shù)名;對于改編的函數(shù),就必須吧@和號碼都寫上,一樣可以加載成功,可以試試看。
解決警告 inconsistent dll linkage
inconsistent dll linkage警告是寫dll時常遇到的一個問題,解決此警告的方法如下:
一般PREDLL_API工程依賴于是否定義了MYDLL_EXPORTS來決定宏展開為__declspec(dllexport)還是__declspec(dllimport)。展開為__declspec(dllexport)是DLL編譯時的需要,通知編譯器該函數(shù)是需要導(dǎo)出供外部調(diào)用的。展開為__declspec(dllimport)是給調(diào)用者用的,通知編譯器,該函數(shù)是個外部導(dǎo)入函數(shù)。
對于工程設(shè)置里面的預(yù)定義宏,是最早被編譯器看到的。所以當(dāng)編譯器編譯DLL工程中的MYDLL.cpp時,因為看到前面有工程設(shè)置有定義MYDLL_EXPORTS,所以就把PREDLL_API展開為__declspec(dllexport)了。
這樣做的目的是為了讓DLL和調(diào)用者共用同一個h文件,在DLL項目中,定義MYDLL_EXPORTS,PREDLL_API就是導(dǎo)出;在調(diào)用該DLL的項目中,不定義MYDLL_EXPORTS,PREDLL_API就是導(dǎo)入。
使用靜態(tài)鏈接庫(Lib)
使用靜態(tài)鏈接庫需要庫的開發(fā)者提供庫的頭文件以及l(fā)ib文件,一般來說lib文件都比較大(相對導(dǎo)入庫來說),靜態(tài)鏈接庫是將全部指令都包含入調(diào)用程序生成的EXE文件中,并不存在“導(dǎo)出某個函數(shù)提供給用戶使用”的情況,就是要么全要,要么都不要。
使用動態(tài)鏈接庫(DLL)
方法一: load-time dynamic linking (隱式調(diào)用)
在要調(diào)用dll的應(yīng)用程序鏈接時,將dll的輸入庫文件(import library,.lib文件)包含進去。具體的做法是在源文件開頭加一句#include ,然后就可以在源文件中調(diào)用dlldemo.dll中的輸出文件了。
#pragma comment(lib, "***.lib") //通知編譯器DLL的.lib文件所在路徑及文件名,也可以不采用該語句,在屬性欄——輸入——附加依賴項處添加對應(yīng)的lib就可以編譯鏈接應(yīng)用程序了。
extern "C" __declspec(dllimport) foo(); //聲明導(dǎo)入函數(shù)
方法二: run-time dynamic linking (顯式調(diào)用)
不必在鏈接時包含輸入庫文件,而是在源程序中使用LoadLibrary或LoadLibraryEx動態(tài)的載入dll。
主要步驟為(以demodll.dll為例):
1) typedef函數(shù)原型和定義函數(shù)指針。
typedef void (CALLBACK* DllFooType)(void) ;
DllFooType pfnDllFoo = NULL ;
2) 使用LoadLibrary載入dll,并保存dll實例句柄
HINSTANCE dllHandle = NULL ;
...
dllHandle = LoadLibrary(TEXT("dlldemo.dll"));
3) 使用GetProcAddress得到dll中函數(shù)的指針
pfnDllFoo = (DllFooType)GetProcAddress(dllHandle,TEXT("DllFoo")) ;
注意從GetProcAddress返回的指針必須轉(zhuǎn)型為特定類型的函數(shù)指針。
4)檢驗函數(shù)指針,如果不為空則可調(diào)用該函數(shù)
if(pfnDllFoo!=NULL)
DllFoo() ;
5)使用FreeLibrary卸載dll
FreeLibrary(dllHandle) ;
動態(tài)鏈接庫(DLL)的優(yōu)點
→節(jié)約內(nèi)存;
→使應(yīng)用程序“變瘦”;
→可單獨修改動態(tài)鏈接庫而不必與應(yīng)用程序重新鏈接;
→可方便實現(xiàn)多語言聯(lián)合編程(比如用VC++寫個dll,然后在VB中調(diào)用);
→可將資源打包;
→可在應(yīng)用程序間共享內(nèi)存
→......