DLL 與Lib

許多單講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中進行,那麼這種情況便不會發生崩潰  

發佈了11 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章