1. 程式人生 > >C++編寫DLL

C++編寫DLL

雖然能用DLL實現的功能都可以用COM來替代,但DLL的優點確實不少,它更容易建立。本文將討論如何利用VC MFC來建立不同型別的DLL,以及如何使用他們。一、DLL的不同型別     使用 V C++可以生成兩種型別的DLL:MFC擴充套件DLL和常規DLL。常規DLL有可以分為動態連線和靜態連線。Visual C++還可以生成WIN32 DLL,但不是這裡討論的主要物件。

1、MFC擴充套件DLL 每個DLL都有某種型別的介面:變數、指標、函式、客戶程式訪問的類。它們的作用是讓客戶程式使用DLL,MFC擴充套件DLL可以有C++的介面。也就是它可以匯出C++類給客戶端。匯出的函式可以使用C++/MFC資料型別做引數或返回值,匯出一個類時客戶端能建立類物件或者派生這個類。同時,在DLL中也可以使用DLL和MFC。 Visual C++使用的MFC類庫也是儲存在一個DLL中,MFC擴充套件DLL動態連線到MFC程式碼庫的DLL,客戶程式也必須要動態連線到MFC程式碼庫的DLL。(這裡談到的兩個DLL,一個是我們自己編寫的DLL,一個裝MFC類庫的DLL)現在MFC程式碼庫的DLL也存在多個版本,客戶程式和擴充套件DLL都必須使用相同版本的MFC程式碼DLL。所以為了讓MFC擴充套件DLL能很好的工作,擴充套件DLL和客戶程式都必須動態連線到MFC程式碼庫DLL。而這個DLL必須在客戶程式執行的計算機上。 2、常規DLL 使用MFC擴充套件DLL的一個問題就是DLL僅能和MFC客戶程式一起工作,如果需要一個使用更廣泛的DLL,最好採用常規DLL,因為它不受MFC的某些限制。常規DLL也有缺點:它不能和客戶程式傳送指標或MFC派生類和物件的引用。一句話就是常規DLL和客戶程式的介面不能使用MFC,但在DLL和客戶程式的內部還是可以使用MFC。 當在常規DLL的內部使用MFC程式碼庫的DLL時,可以是動態連線/靜態連線。如果是動態連線,也就是常規DLL需要的MFC程式碼沒有構建到DLL中,這種情況有點和擴充套件DLL類似,在DLL執行的計算機上必須要MFC程式碼庫的DLL。如果是靜態連線,常規DLL裡面已經包含了需要的MFC程式碼,這樣DLL的體積將比較大,但它可以在沒有MFC程式碼庫DLL的計算機上正常執行。

二、建立DLL     利用Visual C++提供的嚮導功能可以很容易建立一個不完成任何實質任務的DLL,這裡就不多講了,主要的任務是如何給DLL新增功能,以及在客戶程式中利用這個DLL 1、匯出類    用嚮導建立好框架後,就可以新增需要匯出類的.cpp .h檔案到DLL中來,或者用嚮導建立C++ Herder File/C++ Source File。為了能匯出這個類,在類宣告的時候要加“_declspec(dllexport)”,如:  class _declspec(dllexport) CMyClass {      ...//宣告  } 如果建立的MFC擴充套件DLL,可以使用巨集:AFX_EXT_CLASS: class AFX_EXT_CLASS CMyClass {      ...//宣告  } 這樣匯出類的方法是最簡單的,也可以採用.def檔案匯出,這裡暫不詳談。 2、匯出變數、常量、物件  

很多時候不需要匯出一個類,可以讓DLL匯出一個變數、常量、物件,匯出它們只需要進行簡單的宣告:_declspec(dllexport) int MyInt;  _declspec(dllexport) extern const COLORREF MyColor=RGB(0,0,0);  _declspec(dllexport) CRect rect(10,10,20,20);  要匯出一個常量時必須使用關鍵字extern,否則會發生連線錯誤。 注意:如果客戶程式識別這個類而且有自己的標頭檔案,則只能匯出一個類物件。如果在DLL中建立一個類,客戶程式不使用標頭檔案就無法識別這個類。 當匯出一個物件或者變數時,載入DLL的每個客戶程式都有一個自己的拷貝。也就是如果兩個程式使用的是同一個DLL,一個應用程式所做的修改不會影響另一個應用程式。  我們在匯出的時候只能匯出DLL中的全域性變數或物件,而不能匯出區域性的變數和物件,因為它們過了作用域也就不存在了,那樣DLL就不能正常工作。如:

MyFunction() {       _declspec(dllexport) int MyInt;        _declspec(dllexport) CMyClass object;  } 3、匯出函式 匯出函式和匯出變數/物件類似,只要把_declspec(dllexport)加到函式原型開始的位置: _declspec(dllexport) int MyFunction(int); 如果是常規DLL,它將和C寫的程式使用,宣告方式如下: extern "c" _declspec(dllexport) int MyFunction(int); 實現: extern "c" _declspec(dllexport) int MyFunction(int x) {      ...//操作  } 如果建立的是動態連線到MFC程式碼庫DLL的常規DLL,則必須插入AFX_MANAGE_STATE作為匯出函式的首行,因此定義如下: extern "c" _declspec(dllexport) int MyFunction(int x) {      AFX_MANAGE_STATE(AfxGetStaticModuleState());       ...//操作  } 有時候為了安全起見,在每個常規DLL裡都加上,也不會有任何問題,只是在靜態連線的時候這個巨集無效而已。這是匯出函式的方法,記住只有MFC擴充套件DLL才能讓引數和返回值使用MFC的資料型別。 4、匯出指標 匯出指標的方式如下: _declspec(dllexport) int *pint; _declspec(dllexport) CMyClass object = new CMyClass; 如果宣告的時候同時初始化了指標,就需要找到合適的地方類釋放指標。在擴充套件DLL中有個函式DllMain()。(注意函式名中的兩個l要是小寫字母),可以在這個函式中處理指標: # include "MyClass.h" _declspec(dllexport) CMyClass *pobject = new CMyClass; DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved) {     if(dwReason == DLL_PROCESS_ATTACH)      {          .....//      }      else if(dwReason == DLL_PROCESS_DETACH)      {          delete pobject;      }  } 常規DLL有一個從CWinApp派生的類物件處理DLL的開和關,可以使用類嚮導新增InitInstance/ExitInstance函式。 int CMyDllApp::ExitInstance() {     delete pobject;      return CWinApp::ExitInstance();  }三、在客戶程式中使用DLL     編譯一個DLL時將建立兩個檔案.dll檔案和.lib檔案。首先將這兩個檔案複製到客戶程式專案的資料夾裡,這裡需要注意DLL和客戶程式的版本問題,儘量使用相同的版本,都使用RELEASE或者都是DEBUG版本。     接著就需要在客戶程式中設定LIB檔案,開啟Project Settings--- >Link--->Object/library Modules中輸入LIB的檔名和路徑。如:Debug/SampleDll.lib。除了DLL和LIB檔案外,客戶程式需要針對匯出類、函式、物件和變數的標頭檔案,現在進行匯入新增的關鍵字就是:_declspec(dllimport),如: _declspec(dllimport) int MyFunction(int); _declspec(dllimport) int MyInt; _declspec(dllimport) CMyClass object; extern "C" _declspec(dllimport) int MyFunction(int); 在有的時候為了匯入類,要把相應類的標頭檔案新增到客戶程式中,不同的是要修改類宣告的標誌: class _declspec(dllimport) CMyClass,如果建立的是擴充套件DLL,兩個位置都是: class AFX_EXT_CLASS CMyClass。

        使用DLL的一個比較嚴重的問題就是編譯器之間的相容性問題。不同的編譯器對c++函式在二進位制級別的實現方式是不同的。所以對基於C++的DLL,如果編譯器不同就有很麻煩的。如果建立的是MFC擴充套件DLL,就不會存在問題,因為它只能被動態連線到MFC的客戶應用程式。這裡不是本文討論的重點。一、重新編譯問題 我們先來看一個在實際中可能遇到的問題:     

比如現在建立好了一個DLL匯出了CMyClass類,客戶也能正常使用這個DLL,假設CMyClass物件的大小為30位元組。如果我們需要修改DLL中的CMyClass類,讓它有相同的函式和成員變數,但是給增加了一個私有的成員變數int型別,現在CMyClass物件的大小就是34位元組了。當直接把這個新的DLL給客戶使用替換掉原來30位元組大小的DLL,客戶應用程式期望的是30位元組大小的物件,而現在卻變成了一個34位元組大小的物件,糟糕,客戶程式出錯了。 類似的問題,如果不是匯出CMyClass類,而在匯出的函式中使用了CMyClass,改變物件的大小仍然會有問題的。這個時候修改這個問題的唯一辦法就是替換客戶程式中的CMyClass的標頭檔案,全部重新編譯整個應用程式,讓客戶程式使用大小為34位元組的物件。 這就是一個嚴重的問題,有的時候如果沒有客戶程式的原始碼,那麼我們就不能使用這個新的DLL了。

二、解決方法    為了能避免重新編譯客戶程式,這裡介紹兩個方法:(1)使用介面類。(2)使用建立和銷燬類的靜態函式。 1、使用介面類    介面類的也就是建立第二個類,它作為要匯出類的介面,所以在匯出類改變時,也不需要重新編譯客戶程式,因為介面類沒有發生變化。    假設匯出的CMyClass類有兩個函式FunctionA FunctionB。現在建立一個介面類CMyInterface,下面就是在DLL中的CMyInterface類的標頭檔案的程式碼: # include "MyClass.h" class _declspec(dllexport) CMyInterface {      CMyClass *pmyclass;      CMyInterface();      ~CMyInterface(); public:      int FunctionA(int);      int FunctionB(int); }; 而在客戶程式中的標頭檔案稍不同,不需要INCLUDE語句,因為客戶程式沒有它的拷貝。相反,使用一個CMyClass的向前宣告,即使沒有標頭檔案也能編譯: class _declspec(dllexport) CMyInterface {      class CMyClass;//向前宣告      CMyClass *pmyclass;      CMyInterface();      ~CMyInterface(); public:      int FunctionA(int);      int FunctionB(int); }; 在DLL中的CMyInterface的實現如下: CMyInterface::CMyInterface() {       pmyclass = new CMyClass(); } CMyInterface::~CMyInterface() {      delete pmyclass; } int CMyInterface::FunctionA() {      return pmyclass->FunctionA(); } int CMyInterface::FunctionB() {      return pmyclass->FunctionB();     } ..... 對匯出類CMyClass的每個成員函式,CMyInterface類都提供自己的對應的函式。客戶程式與CMyClass沒有聯絡,這樣任意改CMyClass也不會有問題,因為CMyInterface類的大小沒有發生變化。即使為了能訪問CMyClass中的新增變數而給CMyInterface類加了函式也不會有問題的。    但是這種方法也存在明顯的問題,對匯出類的每個函式和成員變數都要對應實現,有的時候這個介面類會很龐大。同時增加了客戶程式呼叫所需要的時間。增加了程式的開銷。

2、使用靜態函式    還可以使用靜態函式來建立和銷燬類物件。建立一個匯出類的時候,增加兩個靜態的公有函式CreateMe()/DestroyMe(),標頭檔案如下: class _declspec(dllexport) CMyClass {      CMyClass();       ~CMyClass();  public:       static CMyClass *CreateMe();       static void DestroyMe(CMyClass *ptr);  }; 實現函式就是: CMyClass * CMyClass::CMyClass() {       return new CMyClass;  } void CMyClass::DestroyMe(CMyClass *ptr) {       delete ptr;  } 然後象其他類一樣匯出CMyClass類,這個時候在客戶程式中使用這個類的方法稍有不同了。如若想建立一個CMyClass物件,就應該是: CMyClass x; CMyClass *ptr = CMyClass::CreateMe(); 在使用完後刪除: CMyClass::DestroyMe(ptr);