?? 還有一種方法可以顯式導(dǎo)出類成員函數(shù),就是采用虛函數(shù)表的方法。 COM 就是這樣做的。當(dāng)在類中聲明一組虛函數(shù)的時候, 編譯器會創(chuàng)建一個虛函數(shù)表,將各虛函數(shù)的地址按聲明的順序放入其中。當(dāng)一個類對象被創(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() ;
}
…
在測試工程中,顯式導(dǎo)出該全局函數(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 ;
…
?? 這個方法雖然同樣能達(dá)到顯式導(dǎo)出類成員函數(shù)的目的。但是上面的方法有個缺點,就是在DLL中new 出來的對象,卻要放到客戶端釋放。因為客戶端并不知道DLL中的實現(xiàn)方法,很可能會不知道釋放這個對象,從而造成內(nèi)存泄露。<<Effiective C++>>中明文規(guī)定禁止這樣做。
? 現(xiàn)在要找一個方法可以把回收內(nèi)存的任務(wù)交
DLL
自己來處理。有沒有這樣一個方法呢?當(dāng)然有,它就是引用計數(shù)。引用計數(shù)是一個簡單的垃圾回收機(jī)制,它的原理很簡單,組件內(nèi)部維護(hù)著一個引用計數(shù)的數(shù)值,當(dāng)用戶從組件取得一個接口時,該數(shù)值
+1
,當(dāng)用戶使用完這個接口,并釋放該接口時,該數(shù)值
-1
,當(dāng)該數(shù)值為
0
時,組件將自己從內(nèi)存中刪除。下面是采用了引用計數(shù)的
DLL
實現(xiàn)方法。
#ifdef?TRY_EXPORTS
#define
?TRY_API?__declspec(dllexport)
#else
#define
?TRY_API?__declspec(dllimport)
#endif
#include?
<
iostream
>
using
?
namespace
?std;
//
?此類是從?Try.dll?導(dǎo)出的
class
??CTry?
{
public
:
????friend?TRY_API?CTry
*
?createObject()?;
????
virtual
??
void
?print()?;
????
virtual
?
void
?addRef()?;
????
virtual
?
void
?removeRef()?;
protected
:
????
int
?refCount?;
????
//
不允許通過構(gòu)造函數(shù)初始化對象
????CTry(
void
)?;
????
????
virtual
?
~
CTry()?;

????
//
不允許相互賦值,這里面細(xì)節(jié)太多,懶得去分析:D
????CTry(CTry?
&
ths)?;
????CTry
*
?
operator
=
?(
const
?CTry
&
?ths?)?;
????
}
;
extern
?TRY_API?
int
?nTry;
TRY_API?
int
?fnTry(
void
);
?
TRY_API?CTry
*
?createObject()?;
//
?Try.cpp?:?定義?DLL?應(yīng)用程序的入口點。
//
#include?
"
stdafx.h
"
#include?
"
Try.h
"
#ifdef?_MANAGED
#pragma?managed(push,?off)
#endif
BOOL?APIENTRY?DllMain(?HMODULE?hModule,
???????????????????????DWORD??ul_reason_for_call,
???????????????????????LPVOID?lpReserved
?????????????????????)
{
????
switch
?(ul_reason_for_call)
????
{
????
case
?DLL_PROCESS_ATTACH:
????
case
?DLL_THREAD_ATTACH:
????
case
?DLL_THREAD_DETACH:
????
case
?DLL_PROCESS_DETACH:
????????
break
;
????}
????
return
?TRUE;
}
#ifdef?_MANAGED
#pragma?managed(pop)
#endif
//
?這是導(dǎo)出變量的一個示例
TRY_API?
int
?nTry?
=
?
0
;
//
?這是導(dǎo)出函數(shù)的一個示例。
TRY_API?
int
?fnTry(
void
)
{
????
return
?
42
;
}
//
?這是已導(dǎo)出類的構(gòu)造函數(shù)。
//
?有關(guān)類定義的信息,請參閱?Try.h
void
?CTry::print()
{
????cout
<<
"
CTry
"
<<
endl;
}
?CTry?
*
createObject()
?
{
?????CTry?
*
pTry?
=
?
new
?CTry()?;
?????pTry
->
addRef()?;
?????
return
?pTry?;
?}
?
void
?CTry::removeRef()
?
{
?????
if
?(
--
refCount?
==
?
0
)
?????
{
?????????delete?
this
?;
//
刪除自己
?????}
?}
?
void
?CTry::addRef()
?
{
?????
++
refCount?;
?}
?CTry::CTry()
?
{
?????refCount?
=
?
0
?;
?}
?CTry::
~
CTry()
?
{
?}
?CTry::CTry(CTry?
&
ths)
?
{
?}
?CTry?
*
CTry::
operator
?
=
(
const
?CTry?
&
ths)
?
{
?????
return
?NULL;
?}
?
? 上面的方法中,用戶不能通過構(gòu)造函數(shù)來實例化一個對象,必須通過
createObject
接口才能實例化對象,當(dāng)用戶使用完這個對象時必須調(diào)用
removeRef()
接口。代碼如下:
#include?
"
stdafx.h
"
#include?
<
iostream
>
#include?
"
Try.h
"
using
?
namespace
?std;
typedef?CTry
*
?(
*
Fn_FunType)()?;
int
?main()
{
????HINSTANCE?hInstance?
=
?
0
;
????hInstance?
=
?LoadLibrary(
"
Try.dll
"
)?;
????
if
?(hInstance)
????
{
????????Fn_FunType?pFun?
=
?(Fn_FunType)::GetProcAddress(hInstance?,
"
?createObject@@YAPAVCTry@@XZ
"
)?;
????????
if
?(pFun)
????????
{
????????????
//
(pTry->*pFun)()?;
????????????CTry
*
?pTry?
=
?pFun()?;
????????????pTry
->
print()?;
????????????pTry
->
removeRef();
????????}
????}
????system(
"
pause
"
)?;
????
return
?
0
?;
}
? 當(dāng)然上面只是我為了方便測試而寫的一個簡單的例子,還不完善。引用計數(shù)還有很多細(xì)節(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
、以
"?"
標(biāo)識函數(shù)名的開始,后跟函數(shù)名;
2
、函數(shù)名后面以
"@@YG"
標(biāo)識參數(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ù)類型
,
指針標(biāo)識在其所指數(shù)據(jù)類型前;
5
、參數(shù)表后以
"@Z"
標(biāo)識整個名字的結(jié)束,如果該函數(shù)無參數(shù),則以
"Z"
標(biāo)識結(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ù)表的開始標(biāo)識由上面的
"@@YG"
變?yōu)?/span>
"@@YA"
。
__fastcall
調(diào)用約定:
規(guī)則同上面的
_stdcall
調(diào)用約定,只是參數(shù)表的開始標(biāo)識由上面的
"@@YG"
變?yōu)?/span>
"@@YI"
。
VC++
對函數(shù)的省缺聲明是
"__cedcl",
將只能被
C/C++
調(diào)用
.
?? 通常我們希望我們的
DLL
中的導(dǎo)出函數(shù)名能夠更易被識別(用戶使用才會更方便),也就是說
DLL
應(yīng)該編譯出無修飾的
C
函數(shù)名,而不復(fù)雜的修飾名。所以當(dāng)使用
C++
文件來創(chuàng)建
DLL
時應(yīng)該使用
extern “c”
來修飾導(dǎo)出函數(shù)和變量。實際上
VS
編譯器已經(jīng)為我們準(zhǔn)備了一個宏
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 文件標(biāo)識為屬于 DLL 。 LIBRARY 語句的后面是 DLL 的名稱。鏈接器將此名稱放到 DLL 的導(dǎo)入庫中。
2. ?EXPORTS 語句列出被導(dǎo)出函數(shù)的名字;將要輸出的函數(shù)修飾名羅列在 EXPORTS 之下,這個名字必須與定義函數(shù)的名字完全一致,如此就得到一個沒有任何修飾的函數(shù)名了。
3.? 可以使用 DESCRIPTION 語句描述 DLL 的用途 ( 此句可選 ) ;
4.?? ";" 對一行進(jìn)行注釋 ( 可選 ) 。
創(chuàng)建一 .DEF 文件,命名為 export.def ,用來修飾 CTry::print 函數(shù),如下:
LIBRARY??? "Try"
EXPORTS
print????? =????? ?print@CTry@@UAEXXZ? PRIVATE
在
DLL
工程屬性
->
鏈接器
->
輸入
->
模塊定義文件中將
export.def
添加進(jìn)去。這樣就可以直接通過函數(shù)名來顯式調(diào)用類成員函數(shù)了。下面是
DLL
在
depens
工具中顯示的情況。
?
參考資料:
《
windows
核心編程》
《
programming windows
》
《微軟
DLL
專題》


