?? 還有一種方法可以顯式導出類成員函數(shù),就是采用虛函數(shù)表的方法。 COM 就是這樣做的。當在類中聲明一組虛函數(shù)的時候, 編譯器會創(chuàng)建一個虛函數(shù)表,將各虛函數(shù)的地址按聲明的順序放入其中。當一個類對象被創(chuàng)建時,它的前四個字節(jié)是一個指針,指向這個虛函數(shù)表。所以,修改以上的代碼:
//CTry.h
…
??? virtual void print() ; // 將這個成員函數(shù)聲明為虛函數(shù)
TRY_API CTry* createObject() ; // 聲明一個全局函數(shù),創(chuàng)建一個 Ctry 對象的實例
//CTry.cpp
CTry *createObject()
{
??? return new CTry() ;
}
?
在測試工程中,顯式導出該全局函數(shù),并通過它調(diào)用類成員函數(shù)。
typedef CTry* (*Fn_FunType)() ;
Fn_FunType pFun=(Fn_FunType)::GetProcAddress(hInstance ,"?createObject@@YAPAVCTry@@XZ") ;
CTry * pTry = pFun() ;
pTry ->print() ;
delete pTry ;
?? 這個方法雖然同樣能達到顯式導出類成員函數(shù)的目的。但是上面的方法有個缺點,就是在DLL中new 出來的對象,卻要放到客戶端釋放。因為客戶端并不知道DLL中的實現(xiàn)方法,很可能會不知道釋放這個對象,從而造成內(nèi)存泄露。<<Effiective C++>>中明文規(guī)定禁止這樣做。
? 現(xiàn)在要找一個方法可以把回收內(nèi)存的任務(wù)交 DLL 自己來處理。有沒有這樣一個方法呢?當然有,它就是引用計數(shù)。引用計數(shù)是一個簡單的垃圾回收機制,它的原理很簡單,組件內(nèi)部維護著一個引用計數(shù)的數(shù)值,當用戶從組件取得一個接口時,該數(shù)值 +1 ,當用戶使用完這個接口,并釋放該接口時,該數(shù)值 -1 ,當該數(shù)值為 0 時,組件將自己從內(nèi)存中刪除。下面是采用了引用計數(shù)的 DLL 實現(xiàn)方法。
? 上面的方法中,用戶不能通過構(gòu)造函數(shù)來實例化一個對象,必須通過 createObject 接口才能實例化對象,當用戶使用完這個對象時必須調(diào)用 removeRef() 接口。代碼如下:
? 當然上面只是我為了方便測試而寫的一個簡單的例子,還不完善。引用計數(shù)還有很多細節(jié)和注意事項。具體請看《more effective c++》第29項。
關(guān)于名字修飾約定
?? 前面代碼我都是通過編譯器中的修飾名來調(diào)用相應(yīng)的函數(shù)的,修飾名是編譯器在編譯函數(shù)定義或者原型時生成的字符串。比如 "?createObject@@YAPAVCTry@@XZ" 就是 CTry::createObject 的修飾名。
名字修飾約定隨調(diào)用約定和編譯種類 (C 或 C++) 的不同而變化。函數(shù)名修飾約定隨編譯種類和調(diào)用約定的不同而不同,下面分別說明。
A 、 C 編譯時函數(shù)名修飾約定規(guī)則:
__stdcall 調(diào)用約定在輸出函數(shù)名前加上一個下劃線前綴,后面加上一個 "@" 符號和其參數(shù)的字節(jié)數(shù),格式為 _functionname@number 。
__cdecl 調(diào)用約定僅在輸出函數(shù)名前加上一個下劃線前綴,格式為 _functionname 。
__fastcall 調(diào)用約定在輸出函數(shù)名前加上一個 "@" 符號,后面也是一個 "@" 符號和其參數(shù)的字節(jié)數(shù),格式為 @functionname@number 。
它們均不改變輸出函數(shù)名中的字符大小寫,這和 PASCAL 調(diào)用約定不同, PASCAL 約定輸出的函數(shù)名無任何修飾且全部大寫。
B 、 C++ 編譯時函數(shù)名修飾約定規(guī)則:
__stdcall 調(diào)用約定:
1 、以 "?" 標識函數(shù)名的開始,后跟函數(shù)名;
2 、函數(shù)名后面以 "@@YG" 標識參數(shù)表的開始,后跟參數(shù)表;
3 、參數(shù)表以代號表示:
X--void ,
D--char ,
E--unsigned char ,
F--short ,
H--int ,
I--unsigned int ,
J--long ,
K--unsigned long ,
M--float ,
N--double ,
_N--bool ,
....
PA-- 表示指針,后面的代號表明指針類型,如果相同類型的指針連續(xù)出現(xiàn),以 "0" 代替,一個 "0" 代表一次重復(fù);
4 、參數(shù)表的第一項為該函數(shù)的返回值類型,其后依次為參數(shù)的數(shù)據(jù)類型 , 指針標識在其所指數(shù)據(jù)類型前;
5 、參數(shù)表后以 "@Z" 標識整個名字的結(jié)束,如果該函數(shù)無參數(shù),則以 "Z" 標識結(jié)束。
其格式為 "?functionname@@YG*****@Z" 或 "?functionname@@YG*XZ" ,例如
????????? int Test1 ( char *var1,unsigned long ) ----- “ ?Test1@@YGHPADK@Z ”
????????? void Test2 () ?????????????????????? ----- “ ?Test2@@YGXXZ ”
__cdecl 調(diào)用約定:
規(guī)則同上面的 _stdcall 調(diào)用約定,只是參數(shù)表的開始標識由上面的 "@@YG" 變?yōu)?/span> "@@YA" 。
__fastcall 調(diào)用約定:
規(guī)則同上面的 _stdcall 調(diào)用約定,只是參數(shù)表的開始標識由上面的 "@@YG" 變?yōu)?/span> "@@YI" 。 VC++ 對函數(shù)的省缺聲明是 "__cedcl", 將只能被 C/C++ 調(diào)用 .
?? 通常我們希望我們的 DLL 中的導出函數(shù)名能夠更易被識別(用戶使用才會更方便),也就是說 DLL 應(yīng)該編譯出無修飾的 C 函數(shù)名,而不復(fù)雜的修飾名。所以當使用 C++ 文件來創(chuàng)建 DLL 時應(yīng)該使用 extern “c” 來修飾導出函數(shù)和變量。實際上 VS 編譯器已經(jīng)為我們準備了一個宏 EXTERN_C 用來代替 extern “c” 。修改以上的代碼如下:
EXTERN_C TRY_API int nTry;
EXTERN_C TRY_API int fnTry(void);
生成 DLL ,用 depens 工具查看,他們已經(jīng)變成了如下模樣:
這樣在客戶端就可以通過函數(shù)名去調(diào)用它們。但是 EXTERN_C 宏卻沒辦法修飾類成員函數(shù),
如果像全局函數(shù)那樣修飾類成員函數(shù),編譯無法通過。用 .DEF 文件可以解決這個問題。
關(guān)于 .def 文件
模塊定義 (.def) 文件是包含一個或多個描述 DLL 各種屬性的 Module 語句的文本文件。
def 文件包含下列模塊定義語句:
1. 文件中的第一個語句必須是 LIBRARY 語句。此語句將 .def 文件標識為屬于 DLL 。 LIBRARY 語句的后面是 DLL 的名稱。鏈接器將此名稱放到 DLL 的導入庫中。
2. ?EXPORTS 語句列出被導出函數(shù)的名字;將要輸出的函數(shù)修飾名羅列在 EXPORTS 之下,這個名字必須與定義函數(shù)的名字完全一致,如此就得到一個沒有任何修飾的函數(shù)名了。
3.? 可以使用 DESCRIPTION 語句描述 DLL 的用途 ( 此句可選 ) ;
4.?? ";" 對一行進行注釋 ( 可選 ) 。
創(chuàng)建一 .DEF 文件,命名為 export.def ,用來修飾 CTry::print 函數(shù),如下:
LIBRARY??? "Try"
EXPORTS
print????? =????? ?print@CTry@@UAEXXZ? PRIVATE
在 DLL 工程屬性 -> 鏈接器 -> 輸入 -> 模塊定義文件中將 export.def 添加進去。這樣就可以直接通過函數(shù)名來顯式調(diào)用類成員函數(shù)了。下面是 DLL 在 depens 工具中顯示的情況。
參考資料:
《 windows 核心編程》
《 programming windows 》
《微軟 DLL 專題》