原文地址:http://hi.baidu.com/freedomknightduzhi/blog/item/a0504560d1277555ebf8f8ff.html
1:神馬是Dll和Lib,神馬是靜態(tài)鏈接和動態(tài)鏈接
大家都懂的,DLL就是動態(tài)鏈接庫,LIB是靜態(tài)鏈接庫。DLL其實就是EXE,只不過沒main。
動態(tài)鏈接是相對于靜態(tài)鏈接而言的。所謂靜態(tài)鏈接就是把函數(shù)或過程直接鏈接到可執(zhí)行文件中,成為可執(zhí)行程序中的一部分,當(dāng)多個程序調(diào)用同樣的函數(shù)時,內(nèi)存里就會有這個函數(shù)的多個拷貝,浪費內(nèi)存資源。而動態(tài)鏈接則是提供了一個函數(shù)的描述信息給可執(zhí)行文件(并沒有內(nèi)存拷貝),當(dāng)程序被夾在到內(nèi)存里開始運行的時候,系統(tǒng)會在底層創(chuàng)建DLL和應(yīng)用程序之間的連接關(guān)系,當(dāng)執(zhí)行期間需要調(diào)用DLL函數(shù)時,系統(tǒng)才會真正根據(jù)鏈接的定位信息去執(zhí)行DLL中的函數(shù)代碼。
在WINDOWS32系統(tǒng)底下,每個進程有自己的32位的線性地址空間,若一個DLL被進程使用,則該DLL首先會被調(diào)入WIN32系統(tǒng)的全局堆棧,然后通過內(nèi)存映射文件方式映射到這個DLL的進程地址空間。若一個DLL被多個進程調(diào)用,則每個進程都會接收到該DLL的一個映像,而非多份的拷貝。但,在WIN16系統(tǒng)下,每個進程需要擁有自己的一份DLL空間,可以理解為何靜態(tài)鏈接沒啥區(qū)別。
2:DLL和LIB區(qū)別和聯(lián)系。
DLL是程序在運行階段才需要的文件。
LIB是程序編譯時需要鏈接的文件。
DLL只有一種,其中一定是函數(shù)和過程的實現(xiàn)。
LIB是有兩種。若只生成LIB的話,則這個LIB是靜態(tài)編譯出來的,它內(nèi)部包含了函數(shù)索引以及實現(xiàn),這個LIB會比較大。若生成DLL的話,則也會生成一個LIB,這個LIB和剛才那個LIB不同,它是只有函數(shù)索引,沒有實現(xiàn)的,它很小。但是這倆LIB依然遵循上個原則,是在編譯時候是需要被鏈接的。若不鏈接第一個LIB的話,在程序運行時會無法找到函數(shù)實現(xiàn),當(dāng)?shù)簟H舨绘溄拥诙€LIB的話,在程序運行時依然會無法找到函數(shù)實現(xiàn)。但第二種LIB有一種替代方式,就是在程序里,使用LoadLibrary,GetProcAddress替代第二個LIB的功能。第一種LIB生成的EXE文件會很大,因為LIB所有信息被靜態(tài)鏈接進EXE里了。第二種LIB生成的EXE文件會比較小,因為函數(shù)過程實現(xiàn)依舊在DLL內(nèi)。
(啰嗦了一堆,某志希望大家能夠明白兩個LIB的區(qū)別。要再不行的話,我們可以將靜態(tài)編譯的LIB稱為 靜態(tài)鏈接庫。但動態(tài)編譯的LIB稱為
引入庫。可能會比較好一些。)
靜態(tài)鏈接LIB的優(yōu)點是免除掛接動態(tài)鏈接庫,缺點是EXE大,版本控制麻煩些。
動態(tài)鏈接DLL的優(yōu)點是文件小,版本更換時換DLL就好了,缺點是多了點文件。動態(tài)鏈接若是被多個進程使用,會更加方便和節(jié)省內(nèi)存。
3:為什么編譯DLL時總會同時生成一個LIB?這個LIB有用嗎?
若我們不是用靜態(tài)鏈接,而使用DLL,那么我們也需要一個LIB,這個LIB的作用是被鏈接到程序里,在程序運行時告訴系統(tǒng)你需要什么DLL文件。這個LIB里保存的是DLL的名字和輸出函數(shù)入口的順序表。它是有意義的。
當(dāng)然,若我們的應(yīng)用程序里不鏈接這個LIB,則可以使用LoadLibrary,GetProcAddress來告訴系統(tǒng)我們在運行時需要怎么著DLL以及其內(nèi)的函數(shù)。
4:DLL意義。
1:DLL真正實現(xiàn)了跨語言。各種語言都可以生成DLL,而對系統(tǒng)以及應(yīng)用程序來說,哪種語言生成的DLL是沒有區(qū)別的。
2:DLL有足夠的封裝性,對于版本更新有很大好處。因為DLL是運行期間才會使用,所以,即使DLL內(nèi)函數(shù)實現(xiàn)有變化(只要參數(shù)和返回值不發(fā)生變化),程序是不需要進行編譯的。大大提高了軟件開發(fā)和維護的效率。
3:DLL被多個進程使用,因為有內(nèi)存映射機制,無需占用更多內(nèi)存。
5:創(chuàng)建DLL。(注意:某志就不再講解使用MFC AppWizard[dll] 方式創(chuàng)建DLL了。有興趣的自己去百度。這里創(chuàng)建DLL只指使用Win32
Dynamic-link Library創(chuàng)建Non-MFC DLL。呃,DLL的三種類型就不解釋了,依舊那句話:百度一下你就知道。)
每個應(yīng)用程序必須有一個main或者winmain函數(shù)作為入口,DLL一樣,有自己的缺省的入口函數(shù),就是DllMain。函數(shù)如下
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID
lpReserved)
{
switch (ul_reason_for_call)
{
case
DLL_PROCESS_ATTACH: // 進程被調(diào)用
case DLL_THREAD_ATTACH: // 線程被調(diào)用
case
DLL_THREAD_DETACH: // 線程被停止
case DLL_PROCESS_DETACH:
// 進程被停止
break;
}
return TRUE;
}
一般情況下,我們不需要對這個缺省的入口函數(shù)進行什么修改,它就會使動態(tài)鏈接庫得到正確的初始化。但是,當(dāng)我們的DLL需要額外分配內(nèi)存或者資源的時候,或者,DLL希望對調(diào)用自己的進程或線程進行初始化或清除的額外操作時,可以在上述代碼case中加一些自己感冒的東東。(懶……不想細寫了-
-Orz,現(xiàn)在是晚上2點了,明天還一堆的事情)
DLL對于導(dǎo)出類和導(dǎo)出函數(shù)沒啥不同。只要加上 __declspec( dllexport ) 修飾函數(shù)或者類就好了。
但是有查看過DLL代碼的人員都會經(jīng)常見到這么一段代碼
#ifdef FK_DLL_EXPORTS
#define FK_DLL __declspec( dllexport )
#else
#define FK_DLL __declspec( dllimport )
#endif
意義很明顯,但是,問題是 FK_DLL_EXPORTS 這個宏是應(yīng)該在哪兒定義呢?在DLL項目內(nèi),還是在使用DLL的應(yīng)用程序內(nèi)?
這點某志曾迷糊很久,呵呵~其實后來想想,還是蠻明確的。export是導(dǎo)出。import是導(dǎo)入。對于DLL來說,是要導(dǎo)出這些函數(shù)給其他應(yīng)用程序使用的,所以應(yīng)當(dāng)定義
FK_DLL_EXPORTS 宏。對于使用DLL的應(yīng)用程序來說,是導(dǎo)入,是無需定義的。
使用時候也很簡單。
class FK_DLL CMyDllClass{} ;
則整個類被導(dǎo)出。
FK_DLL void MyTestFun( int a );
則該函數(shù)被導(dǎo)出。
但是有時我們可以見到這樣的代碼
extern "C" FK_DLL void MyTestFun2( float b );
其中extern "C"的原理就是標(biāo)示該函數(shù)要求以C形式去進行編譯,不要以C++形式去編譯。具體的編譯原理就不羅嗦了,簡而言之,被extern
"C"定義函數(shù),可以被C以及其他語言進行DLL調(diào)用,而未被extern "C"定義的函數(shù),C是無法訪問DLL中這個函數(shù)的。
在VS中開發(fā)DLL還有一種方式,使用.def文件。
新建個文本文檔,改后綴為FKDll.def,加入到工程里。
FKDll.def里加入以下代碼
LIBRARY FKDll
EXPORTS
MyTestFun@1
MyTestFun2@2
就可以了。其中,LIBRARY語句是說明.def文件是屬于FKDll這個Dll的。EXPORTS下面是我們需要導(dǎo)出的函數(shù)名。后面加的@+數(shù)字,是表示導(dǎo)出函數(shù)的順序編號。這樣就足夠了。(詳細的自己百度,好困,zzzZZZ)
6:使用DLL
使用DLL有兩種方式。顯式鏈接和隱式鏈接。
隱式鏈接很容易。直接#progam comment(lib, "FKDll.lib")
就可以。當(dāng)然,也可以在項目工程->屬性->鏈接庫里加上庫和路徑(相對路徑和絕對路徑都可以)。
顯式鏈接則麻煩些。在程序中使用LoadLibrary加載DLL,再GetProcAddress獲取函數(shù)實現(xiàn),在程序退出之前,調(diào)用FreeLibrary來動態(tài)釋放掉鏈接庫。
例如:
void Main()
{
typedef void (*FKDllFun1)(int a);
FKDllFun1 pFun1;
HINSTANCE hDLL = LoadLibrary("FKDll.dll"); // 若hDll為空則讀取Dll失敗。
pFun1 = (pFun1)GetProcAddress(hDll, "MyTestFun1" ); //
從應(yīng)用程序中的DLL鏡像中獲取名為 MyTestFun1 的函數(shù)指針
pFun1( 100 );
FreeLibrary(hDll);
}
當(dāng)然,我們剛才.def里面還指定了導(dǎo)出函數(shù)的導(dǎo)出順序,那么我們可以修改里面獲取函數(shù)指針那一段為
pFun1 = (pFun1)GetProcAddress(hDll, MAKEINTERSOURCE(1) ); // 1
是剛才指定的MyTestFun1函數(shù)導(dǎo)出順序編號。
這樣可以更快,但是別將編號記混了,會導(dǎo)致詭異的錯誤。
7:比較顯式鏈接和隱式鏈接。
可能的話,盡量使用顯式鏈接。
顯式鏈接可以在程序執(zhí)行時動態(tài)的加載DLL和卸載DLL文件,隱式鏈接是做不到的。
顯式鏈接LoadLibrary,GetProcAddress時能獲知是否加載失敗,我們可以對其進行檢查錯誤處理。而顯式鏈接可能是一個很惡劣的提示或是程序崩潰的結(jié)果。
對于有些Ex類型的加強函數(shù),顯式鏈接可以允許我們找到替代方案。也包括選擇D3d9.dll和OpenGL.dll時也可采用同樣處理。
例如:
if( GetProcAddress( hDll, "FKDllFunEx") == NULL )
{
pFun = GetProcAddress( hDll, "FKDllFun"); // 然后使用pFun進行處理
}
8:導(dǎo)出類和導(dǎo)出函數(shù)
類和函數(shù)的導(dǎo)出方式上面給出了說明,原本極其類似的。
我們說下使用導(dǎo)出類。
若我們隱式的使用了一個導(dǎo)出類,則我們在應(yīng)用程序里繼承它的時候,就如同該類就在應(yīng)用程序代碼里一樣,無需任何處理。
例如:
class FK_DLL CMyDllClass{} ; // Dll文件內(nèi)的代碼
-----------------------
class CAppClass : public CMyDllClass // 應(yīng)用程序內(nèi)代碼,無需做任何處理。
{
....
}
也可以直接使用DLL導(dǎo)出類
void main
{
CMyDllClass* pClass = new CMyDllClass ();
}
但是,若應(yīng)用程序聲明或者分類一個DLL中導(dǎo)出類的對象時會存在一個很討厭的問題:這個操作會使內(nèi)存跟蹤系統(tǒng)失效,使其錯誤的報告內(nèi)存分配和釋放情況。
為解決這個問題,我們可以給出兩個接口函數(shù)對DLL導(dǎo)出類進行創(chuàng)建銷毀支持,就可以使內(nèi)存跟蹤系統(tǒng)正常了。例如
class FK_DLL CMyDllClass{} ;
額外增加倆函數(shù)
FK_DLL CMyDllClass* CreateMyDllClass(){ return new CMyDllClass(); }
FK_DLL void DestoryMyDllClass( CMyDllClass* p_pClass ){ delete p_pClass;
}
-----------------------------------------------
上面的方法可以正確進行內(nèi)存跟蹤了,但是,因為DLL導(dǎo)出類CMyDllClass依舊是導(dǎo)出的狀態(tài),用戶同樣可以跳過我們提供的接口直接使用。那么怎么辦呢。方法是不再對類進行DLL導(dǎo)出,而對類內(nèi)的函數(shù)全部進行DLL導(dǎo)出即可,
-----------------------------------------------
但是若僅僅提供上面兩個接口函數(shù)以及類內(nèi)全部函數(shù),的確功能可以實現(xiàn),卻無法進行類繼承了。若這個類繼承很重要,必須開放,那么就需要使用新的內(nèi)存跟蹤程序替換應(yīng)用程序內(nèi)的原有內(nèi)存跟蹤程序。或者使用下面的一個方法。(見模塊9:復(fù)雜問題)
-----------------------------------------------
同樣,我們也可以發(fā)現(xiàn),在不導(dǎo)出DLL類本身,而只導(dǎo)出DLL類內(nèi)函數(shù)也有一些好處,一些我們不希望外界知道的函數(shù)可以不設(shè)置導(dǎo)出標(biāo)記,這進一步保護了DLL內(nèi)函數(shù)的安全性。
9:復(fù)雜問題。
若我們使用LoadLibrary顯式加載一個DLL,并嘗試在應(yīng)用程序中調(diào)用一個類內(nèi)成員函數(shù)的話,無論該函數(shù)是否在頭文件中有聲明,VS會給出一個"unresolved
external symbol(未解析的外部符號)"的錯誤。我們此時可以將項目屬性中的內(nèi)聯(lián)函數(shù)擴展選項修改為"Only __inline"或"Any
Suitable"即可。但,我們可能在調(diào)試連編的時候期望關(guān)閉內(nèi)聯(lián)函數(shù)擴展,那么另一種解決方案是,將希望導(dǎo)出的函數(shù)聲明為虛函數(shù),例如
class CMyDllClass
{
FK_DLL virtual void MyTestFun( int a ){ dosth(); }
// 用上面代碼替換 FK_DLL void MyTestFun( int a ){ dosth(); }
}
這樣做還有一個額外的好處。將導(dǎo)出的類成員函數(shù)設(shè)置為虛函數(shù)之后,該虛函數(shù)所在的類在應(yīng)用程序中也如同被聲明一樣,可以接受繼承。
例如若是上面的做法,應(yīng)用程序就可以進行順利繼承,而不必要求CMyDllClass 被標(biāo)示為導(dǎo)出。(原理不知,希望精通底層的高手協(xié)助解釋。)
class CAppClass : public CMyDllClass // 應(yīng)用程序內(nèi)代碼,無需做任何處理。
{
....
}