最近接觸到的遊戲會有很多的dll和lib檔案,之前關于動态連結庫和靜态連結庫一直很不了解,最近發現了一篇很好的文章,非常清晰的講解了dll與lib的關系,這裡拿出來給大家分享下。
原文連結:http://blog.163.com/zhengjiu_520/blog/static/3559830620093583438464/
前面有一章說編譯與連結的,說得很簡略,其實應該放到這一章一塊兒來說的。許多單講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,它不是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中進行,那麼這種情況便不會發生崩潰
下面進一步去解釋一下靜态連結與動态連結~
所謂靜态就是link的時候把裡面需要的東西抽取出來安排到你的exe檔案中,以後運作你的exe的時候不再需要lib。
所謂動态就是exe運作的時候依賴于dll裡面提供的功能,沒有這個dll,你的exe無法運作。
lib, dll, exe都算是最終的目标檔案,是最終産物。而c/c++屬于源代碼。源代碼和最終目标檔案中過渡的就是中間代碼obj,實際上之是以需要中間代碼,是你不可能一次得到目标檔案。比如說一個exe需要很多的cpp檔案生成。而編譯器一次隻能編譯一個cpp檔案。這
樣編譯器編譯好一個cpp以後會将其編譯成obj,當所有必須要的cpp都編譯成obj以後,再統一link成所需要的exe,應該說缺少任意一個obj都會導緻exe的連結失敗。
1.obj裡存的是編譯後的代碼跟資料,并且有名稱,是以在連接配接時會出現未解決的外部符号一說。當連成exe後便不存在名稱的概念了,隻有位址。靜态lib就是一堆obj的組合。
2.編譯器會預設連結一些常用的庫,其它的需要你自己指定。
在程式的編譯過程中,如果出現如下錯誤“unresolved symbol [email protected]”,通常是因為找不到引用過的外部函數對應的.lib檔案,或者是.c、.cpp源檔案。
如果在c++工程中使用用c編寫的.lib檔案,需要做如下引用:
extern “C”
{
#include “headfile.h”
}
(1)如果要完成源代碼的編譯,有lib就夠了。
如果要使動态連接配接的程式運作起來,有dll就夠了。
在開發調試階段,當然最好都有。
(2)lib檔案是必須在編譯期就連接配接到應用程式中的,而dll檔案是運作期才會被調用的。有dll,則一定有對應的lib;有lib,不一定要有dll。
有兩種lib檔案:
靜态庫lib,它包含函數的二進制代碼.程式link時,被複制到output檔案。這個lib檔案是靜态編譯出來的,索引和實作都在其中。這時不需要dll。靜态編譯的lib檔案有好處:給使用者安裝時就不需要再挂動态庫了。但也有缺點,就是導緻應用程式比較大,而且失去了動态庫的靈活性,在版本更新時,同時要釋出新的應用程式才行。
動态庫lib,它包含函數的描述和在DLL中的位置,也就是說,它為存放函數實作的dll提供索引功能,為了找到dll中的函數實作的入口點,程式link時,根據函數的位置生成函數調用的jump指令。庫中的函數和資料并不複制到可執行檔案中,是以在應用程式的可執行檔案中,存放的不是被調用的函數代碼,而是DLL中所要調用的函數的記憶體位址,這樣當一個或多個應用程式運作是再把程式代碼和被調用的函數代碼連結起來,進而節省了記憶體資源。
DLL(dynamic link library)其實也是一種可執行檔案格式。跟 .exe 檔案不同的是,.dll 檔案不能直接執行,他們通常由 .exe 在執行時裝入,内含有一些資源以及可執行代碼等。其實 Windows 的三大子產品就是以 DLL 的形式提供的(Kernel32.dll,User32.dll,GDI32.dll),裡面就含有了 API 函數的執行代碼。為了使用 DLL 中的 API 函數,我們必須要有 API 函數的聲明(.H)和其導入庫(.LIB),函數的原型聲明不難了解,那麼導入庫又是做什麼用的呢?我們暫時先這樣了解:導入庫是為了在 DLL 中找到 API 的入口點而使用的。
一、開發和使用dll需注意三種檔案
1、 dll頭檔案 (.h)
它是指dll中說明輸出的類或符号原型或資料結構的.h檔案。當其它應用程式調用dll時,需要将該檔案包含入應用程式的源檔案中。
2、 dll的導入庫檔案(.lib)
它是dll在編譯、連結成功後生成的檔案。主要作用是當其它應用程式調用dll時,需要将該檔案引入應用程式。否則,dll無法引入。
3、 dll檔案(.dll)
它是應用程式調用dll運作時,真正的可執行檔案。dll應用在編譯、連結成功後,.dll檔案即存在。開發成功後的應用程式在釋出時,隻需要有.exe檔案和.dll檔案,不必有.lib檔案和dll頭檔案。
程式庫/模版庫(LIB)
對于一些常用的函數,如printf、strcpy等,把他們編成庫函數,由使用者調用,減少重複勞動和出錯的可能,但編譯後代碼長度并沒有變小。
動态連結庫(DLL)
當多個程序都需要調用某個函數時,為了節省記憶體空間把這些函數編成動态連接配接庫,由多個程序動态共享。
選擇使用LIB還是DLL
要考慮應用中具體情況,比如說多少程序共享一個DLL合适,效率如何等等,更具實際做出權衡。另外,DLL也有其缺點,例如不同版本DLL的相容性不可能做到完美。
如果同時有lib檔案和dll檔案生成的話,lib檔案應該是dll檔案的一個索引一樣的東西。dll檔案就是動态庫功能的具體實作了。
如果你想使用lib檔案,就必須:
1 包含一個對應的頭檔案告知編譯器lib檔案裡面的具體内容
2 設定lib檔案允許編譯器去查找已經編譯好的二進制代碼