lib和dll文件的區別和聯繫

lib和dll文件的區別和聯繫

什麼是lib文件,lib和dll的關係如何 (2008-04-18 19:44:37)   
(1)lib是編譯時需要的,dll是運行時需要的。
如果要完成源代碼的編譯,有lib就夠了。
如果也使動態連接的程序運行起來,有dll就夠了。
在開發和調試階段,當然最好都有。
(2)一般的動態庫程序有lib文件和dll文件。lib文件是必須在編譯期就連接到應用程序中的,而dll文件是運行期纔會被調用的。如果有dll文件,那麼對應的lib文件一般是一些索引信息,具體的實現在dll文件中。如果只有lib文件,那麼這個lib文件是靜態編譯出來的,索引和實現都在其中。靜態編譯的lib文件有好處:給用戶安裝時就不需要再掛動態庫了。但也有缺點,就是導致應用程序比較大,而且失去了動態庫的靈活性,在版本升級時,同時要發佈新的應用程序才行。
(3)在動態庫的情況下,有兩個文件,一個是引入庫(.LIB)文件,一個是DLL文件,引入庫文件包含被DLL導出的函數的名稱和位置,DLL包含實際的函數和數據,應用程序使用LIB文件鏈接到所需要使用的DLL文件,庫中的函數和數據並不複製到可執行文件中,因此在應用程序的可執行文件中,存放的不是被調用的函數代碼,而是DLL中所要調用的函數的內存地址,這樣當一個或多個應用程序運行是再把程序代碼和被調用的函數代碼鏈接起來,從而節省了內存資源。從上面的說明可以看出,DLL和.LIB文件必須隨應用程序一起發行,否則應用程序將會產生錯誤。
一、開發和使用dll需注意三種文件    
  1、   dll頭文件    
  它是指dll中說明輸出的類或符號原型或數據結構的.h文件。當其它應用程序調用dll時,需要將該文件包含入應用程序的源文件中。    
  2、   dll的引入庫文件    
  它是dll在編譯、鏈接成功後生成的文件。主要作用是當其它應用程序調用dll時,需要將該文件引入應用程序。否則,dll無法引入。    
  3、   dll文件(.dll)    
  它是應用程序調用dll運行時,真正的可執行文件。dll應用在編譯、鏈接成功後,.dll文件即存在。開發成功後的應用程序在發佈時,只需要有.exe文件和.dll文件,不必有.lib文件和dll頭文件。   
動態鏈接庫 (DLL) 是作爲共享函數庫的可執行文件。動態鏈接提供了一種方法,使進程可以調用不屬於其可執行代碼的函數。函數的可執行代碼位於一個 DLL 中,該 DLL 包含一個或多個已被編譯、鏈接並與使用它們的進程分開存儲的函數。DLL 還有助於共享數據和資源。多個應用程序可同時訪問內存中單個 DLL 副本的內容。 
動態鏈接與靜態鏈接的不同之處在於:動態鏈接允許可執行模塊(.dll 文件或 .exe 文件)僅包含在運行時定位 DLL 函數的可執行代碼所需的信息。在靜態鏈接中,鏈接器從靜態鏈接庫獲取所有被引用的函數,並將庫同代碼一起放到可執行文件中。 
使用動態鏈接代替靜態鏈接有若干優點。DLL 節省內存,減少交換操作,節省磁盤空間,更易於升級,提供售後支持,提供擴展 MFC 庫類的機制,支持多語言程序,並使國際版本的創建輕鬆完成。 
lib與dll文件最大區別在調用方面 
dll可以靜態陷入
lib與DLL
從這一章起,我講述的內容將特定於windows平臺。其實這篇文章也可看作是我在windows下的開發經驗總結,因爲以後我決定轉unix了。
前面有一章說編譯與鏈接的,說得很簡略,其實應該放到這一章一塊兒來說的。許多單講C++的書其實都過於學院派,對於真實的工作環境,上百個源文件怎麼結合起來,幾乎沒有提及。我引導讀者一步步看看lib與DLL是怎麼回事。
一個最簡單的C++程序,只需要一個源文件,這個源文件包含了如下語句
int main(){return 0;}
自然,這個程序什麼也不做。
當需程序需要做事情時,我們會把越來越多的語句添加到源文件中,例如,我們會開始在main函數中添加代碼:
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
由於人的智力水平的限制,當一個函數中包含了太多的語句時,便不太容易被理解,這時候開始需要子函數:
#include <stdio.h>
void ShowHello()
{
printf("Hello World!\n");
}
int main()
{
ShowHello();
return 0;
}
同樣的道理,一個源文件中包含了太多的函數,同樣不好理解,人們開始分多個源文件了
// main.cpp
void ShowHello();//[1]
int main()
{
ShowHello();
return 0;
}
// hello.cpp
#include <stdio.h>
void ShowHello()
{
printf("Hello World!\n");
}
將這兩個文件加入到一個VC工程中,它們會被分別編譯,最後鏈接在一起。在VC編譯器的輸出窗口,你可以看到如下信息
--------------------Configuration: hello - Win32 Debug--------------------
Compiling...
main.cpp
hello.cpp
Linking... 
hello.exe - 0 error(s), 0 warning(s)
這展示了它們的編譯鏈接過程。
接下來,大家就算不知道也該猜到,當一個工程中有太多的源文件時,它也不好理解,於是,人們想到了一種手段:將一部分源文件預先編譯成庫文件,也即lib文件,當要使用其中的函數時,只需要鏈接lib文件就可以了,而不用再理會最初的源文件。
在VC中新建一個static library類型的工程,加入hello.cpp文件,然後編譯,就生成了lib文件,假設文件名爲hello.lib。
別的工程要使用這個lib有兩種方式:
1 在工程選項-〉link-〉Object/Library Module中加入hello.lib
2 可以在源代碼中加入一行指令
#pragma comment(lib, "hello.lib")
注意這個不是C++語言的一部分,而是編譯器的預處理指令,用於通知編譯器需要鏈接hello.lib
根據個人愛好任意使用一種方式既可。
這種lib文件的格式可以簡單的介紹一下,它實際上是任意個obj文件的集合。obj文件則是cpp文件編譯生成的,在本例中,lib文件只包含了一個obj文件,如果有多個cpp文件則會編譯生成多個obj文件,從而生成的lib文件中也包含了多個obj,注意,這裏僅僅是集合而已,不涉及到link,所以,在編譯這種靜態庫工程時,你根本不會遇到鏈接錯誤。即使有錯,錯誤也只會在使用這個lib的EXE或者DLL工程中暴露出來。
關於靜態lib,就只有這麼多內容了,真的很簡單,現在我們介紹另外一種類型的lib,它不是obj文件的集合,即裏面不含有實際的實現,它只是提供動態鏈接到DLL所需要的信息。這種lib可以在編譯一個DLL工程時由編譯器生成。涉及到DLL,問題開始複雜起來,我不指望在本文中能把DLL的原理說清楚,這不是本文的目標,我介紹操作層面的東西。
簡單的說,一個DLL工程和一個EXE工程的差別有兩點:
1 EXE的入口函數是main或者WinMain,而DLL的入口函數是DllMain
2 EXE的入口函數標誌着一段處理流程的開始,函數退出後,流程處理就結束了,而DLL的入口函數對系統來說,只是路過,加載DLL的時候路過一次,卸載DLL的時候又路過一次[2],你可以在DLL入口函數中做流程處理,但這通常不是DLL的目的,DLL的目的是要導出函數供其它DLL或EXE使用。你可以把DLL和EXE的關係理解成前面的main.cpp和hello.cpp的關係,有類似,實現手段不同罷了。
先看如何寫一個DLL以及如何導出函數,讀者應該先嚐試用VC創建一個新的動態鏈接庫工程,創建時選項不選空工程就可以了,這樣你能得到一個示例,以便開始在這個例子基礎上工作。
看看你創建的例子中的頭文件有類似這樣的語句:
#ifdef DLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
這就是函數的導出與使用導出函數的全部奧妙了。你的DLL工程已經在工程設置中定義了一個宏DLL_EXPORTS,因此你的函數聲明只要前面加DLL_API就表示把它導出,而DLL的使用者由於沒有定義這個宏,所以它包含這個頭文件時把你的函數看作導入的。通過模仿這個例子,你就可以寫一系列的標記爲導出的函數了。
導出函數還有另一種方法,是使用DEF文件,DEF文件的作用,在現在來說只是起到限定導出函數名字的作用,這裏,我們要引出第二種[4]使用DLL的方法:稱爲顯示加載,通過Windows API的LoadLibrary和GetProcAddress這兩個函數來實現[5],這裏GetProcAddress的參數需要一個字符串形式的函數名稱,如果DLL工程中沒有使用DEF文件,那麼很可能你要使用非常奇怪的函數名稱(形如:?fnDll@@YAHXZ)才能正確調用,這是因爲C++中的函數重載機制把函數名字重新編碼了,如果使用DEF文件,你可以顯式指定沒編碼前的函數名。 
 

有了這些知識,你可以開始寫一些簡單的DLL的應用,但是我可以百分之百的肯定,你會遇到崩潰,而之前的非DLL的版本則沒有問題。假如你通過顯式加載來使用DLL,有可能會是調用約定不一致而引起崩潰,所謂調用約定就是函數聲明前面加上__stdcall __cdecl等等限定詞,注意一些宏如WINAPI會定義成這些限定詞之一,不理解他們沒關係,但是記住一定要保持一致,即聲明和定義時一致,這在用隱式加載時不成問題,但是顯示加載由於沒有利用頭文件,就有可能產生不一致。
調用約定並不是我真正要說的,雖然它是一種可能。我要說的是內存分配與釋放的問題。請看下面代碼:
void foo(string& str)
{
str = "hello";
}
int main()
{
string str;
foo(str);
printf("%s\n", str.c_str());
return 0;
}
當函數foo和main在同一個工程中,或者foo在靜態庫中時,不會有問題,但是如果foo是一個DLL的導出函數時,請不要這麼寫,它有可能會導致崩潰[6]。崩潰的原因在於“一個模塊中分配的內存在另一個模塊中釋放”,DLL與EXE分屬兩個模塊,例子中foo裏面賦值操作導致了內存分配,而main中return語句之後,string對象析構引起內存釋放。
我不想窮舉全部的這類情況,只請大家在設計DLL接口時考慮清楚內存的分配釋放問題,請遵循誰分配,誰釋放的原則來進行。
如果不知道該怎麼設計,請抄襲我們常見的DLL接口--微軟的API的做法,如:
CreateDC
ReleaseDC
的成對調用,一個函數分配了內存,另外一個函數用來釋放內存。
回到我們有可能崩潰的例子中來,怎麼修改才能避免呢?
這可以做爲一個練習讓讀者來做,這個練習用的時間也許會比較長,如果你做好了,那麼你差不多就出師了。一時想不到也不用急,我至少見過兩個有五年以上經驗的程序員依然犯這樣的錯誤。

注[1]:爲了說明的需要,我這裏使用直接聲明的方式,實際工程中是應該使用頭文件的。
注[2]: 還有線程創建與銷燬也會路過DLL的入口,但是這對新手來說意義不大。
注[3]:DEF文件格式很簡單,關於DEF文件的例子,可以通過新建一個ATL COM工程看到。
注[4]:第一種方法和使用靜態庫差不多,包含頭文件,鏈接庫文件,然後就像是使用普通函數一樣,稱爲隱式加載。
注[5]:具體調用方法請參閱MSDN。
注[6]:之所以說有可能是因爲,如果兩個工程的設置都是採用動態連接到運行庫,那麼分配釋放其實都在運行庫的DLL中進行,那麼這種情況便不會發生崩潰

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