在C實(shí)現(xiàn)COM接口系列1中實(shí)現(xiàn)的com接口IFoo與使用它的客戶耦合在一起,沒(méi)有實(shí)現(xiàn)在各自分離的模塊,因此不符合模塊化編程思想。本期添加類(lèi)廠支持,以使接口的實(shí)現(xiàn)與接口的使用相分離。
---------------------------------------------------
類(lèi)廠的作用到底是什么?
將接口的實(shí)現(xiàn)與客戶使用分離開(kāi)來(lái)嗎?
不盡然。使用CoCreateInstance,客戶可以完全不必知道類(lèi)廠的存在,而創(chuàng)建組件,獲取組件實(shí)現(xiàn)的接口并使用。
即COM庫(kù)可以完全拋開(kāi)類(lèi)廠的概念,而是提供一個(gè)這樣的函數(shù)原型:
CoCreateObject(REFID rclsid,...,REFID riid,void **ppItf);
用戶在調(diào)用的時(shí)候可以對(duì)riid提供IID_Unknown或者特定于該對(duì)象的一個(gè)接口,直接獲取該對(duì)象的IUnknown或特定的接口指針。
可以看到,這正是CoCreateInstance所作的事情。
1 類(lèi)廠提供了間接創(chuàng)建類(lèi)對(duì)象的方式:用戶可以先獲取并持有類(lèi)廠接口指針,通過(guò)該指針?biāo)赶虻念?lèi)廠接口創(chuàng)建類(lèi)對(duì)象。適用于需要?jiǎng)?chuàng)建多個(gè)(或重復(fù)創(chuàng)建)類(lèi)對(duì)象的地方,減少了每次都要定位對(duì)象庫(kù)并把對(duì)象庫(kù)裝入內(nèi)存的開(kāi)銷(xiāo)。
2 類(lèi)廠提供了保證組件庫(kù)留在內(nèi)存不被卸載出去的另一種方法:類(lèi)廠接口函數(shù)LockServer。組件庫(kù)維護(hù)一個(gè)庫(kù)范圍計(jì)數(shù)器,只有該計(jì)數(shù)器為0時(shí),組件庫(kù)才允許自己被卸載出內(nèi)存。(與此相對(duì),引用計(jì)數(shù)是類(lèi)對(duì)象范圍的,通過(guò)該類(lèi)實(shí)現(xiàn)的各個(gè)接口來(lái)維護(hù)。如果一個(gè)類(lèi)對(duì)象的引用計(jì)數(shù)達(dá)到0,那么該對(duì)象占有的內(nèi)存就被釋放,該對(duì)象上的接口指針也不再有效。)
除了調(diào)用LockServer鎖定組件庫(kù)以外,當(dāng)創(chuàng)建的組件個(gè)數(shù)大于0時(shí),組件庫(kù)也不能被卸載。也可以說(shuō),調(diào)用一次LockServer()的作用相當(dāng)于創(chuàng)建了一個(gè)組件。
-----------------------------------------------------------------------
客戶一側(cè):
1 使用一個(gè)接口需要知道哪些信息?
備選:
接口IID
類(lèi)對(duì)象(類(lèi)廠)CLSID(或ProgID)
接口函數(shù)原型(參數(shù)個(gè)數(shù),類(lèi)型,返回值)
實(shí)現(xiàn)接口組件的線程模型(進(jìn)程內(nèi)、進(jìn)程外、遠(yuǎn)程)?
類(lèi)型庫(kù)typelib信息?
服務(wù)一側(cè):
2 實(shí)現(xiàn)一個(gè)組件和接口以供客戶調(diào)用,需要提供哪些東西?
備選:
所有客戶使用組件和接口所需的內(nèi)容
額外的還有:
--------------------------------------------------------------------
為dll添加.def文件與直接在需要導(dǎo)出的函數(shù)定義處指定_declspec( dllexport )有區(qū)別嗎?如果有是什么區(qū)別?
我發(fā)現(xiàn)在outdll.c中這樣指定:
__declspec( dllexport ) HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)
會(huì)產(chǎn)生編譯錯(cuò)誤:
1>------ Build started: Project: outside, Configuration: Debug Win32 ------
1>Compiling...
1>outdll.c
1>d:\outside-cf\outside\outdll.c(19) : error C2375: 'DllGetClassObject' : redefinition; different linkage
1> c:\program files\microsoft visual studio 8\vc\platformsdk\include\objbase.h(833) : see declaration of 'DllGetClassObject'
1>Build log was saved at "file://d:\outside-cf\outside\Debug\BuildLog.htm"
1>outside - 1 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 1 up-to-date, 0 skipped ==========
c2375的解釋意思是出錯(cuò)的函數(shù)使用的鏈接指示符與之前聲明的不同。
Compiler Error C2375
'function' : redefinition; different linkage
The function is already declared with a different linkage specifier.
objbase.h中聲明了DllGetClassObject()函數(shù):
STDAPI DllGetClassObject(IN REFCLSID rclsid, IN REFIID riid, OUT LPVOID FAR* ppv);
而使用.def文件就沒(méi)有問(wèn)題。
-----------------------------------------------------------------------------
初次執(zhí)行結(jié)果:
問(wèn)題就是總有一個(gè)分配的內(nèi)存沒(méi)有釋放:

根據(jù)打印出來(lái)的內(nèi)存地址可以判斷,應(yīng)該是先創(chuàng)建的類(lèi)廠對(duì)象的內(nèi)存沒(méi)有釋放。
檢查代碼,main()中并沒(méi)有忘記調(diào)用Release(pCF)釋放類(lèi)廠對(duì)象。打印Release(pCF)的返回值,發(fā)現(xiàn)是1,即在類(lèi)廠接口指針上少調(diào)用了一次Release,那么,究竟是哪里少的呢?
main()函數(shù)中有關(guān)類(lèi)廠對(duì)象引用計(jì)數(shù)的地方就是CoGetClassObject和Release(CreateInstance跟類(lèi)廠自己的引用計(jì)數(shù)無(wú)關(guān)),這是一對(duì)增加引用計(jì)數(shù)和減少引用計(jì)數(shù)的對(duì)應(yīng)操作,所以,main()中應(yīng)該沒(méi)有問(wèn)題。
那么,就只有創(chuàng)建類(lèi)廠對(duì)象的時(shí)候了。下面看一下類(lèi)廠對(duì)象是如何創(chuàng)建的。
首先,main調(diào)用CoGetClassObject,該函數(shù)就調(diào)用dll中的DllGetClassObject。由于是第一次調(diào)用(不考慮其他客戶使用該dll的情況),程序執(zhí)行到CreateClassFactory(...),該函數(shù)執(zhí)行完后,類(lèi)廠對(duì)象的引用計(jì)數(shù)是1。
由于創(chuàng)建成功,因此繼續(xù)向下執(zhí)行到QueryInterface,此時(shí),類(lèi)廠對(duì)象的引用計(jì)數(shù)變成了2。然后,DllGetClassObject返回,com庫(kù)函數(shù)CoGetClassObject也應(yīng)該返回。注意,此時(shí)的類(lèi)廠對(duì)象引用計(jì)數(shù)已經(jīng)是2了!
因此,問(wèn)題就出在這里。main調(diào)用一次CoGetClassObject后,類(lèi)廠對(duì)象的引用計(jì)數(shù)是2,而不是我想向中的1。于是,后面調(diào)用一次Release也就當(dāng)然無(wú)法釋放掉類(lèi)場(chǎng)對(duì)象了。
1 HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)
2 {
3 *ppv = 0;
4 if (IsEqualCLSID (rclsid, &CLSID_Outside))
5 {
6
7 if (!vpcfOutside)
8
9 {
10
11 HRESULT hr = CreateClassFactory (&CLSID_Outside, CreateOutside,
12 &IID_IClassFactory, &vpcfOutside);
13
14 if (hr != NOERROR)
15
16 return hr;
17 }
18
19 return QueryInterface (vpcfOutside, riid, ppv);
20
21 }
22
23 return E_FAIL;
24 }
找到了原因,改正就很容易了。這里我覺(jué)得需要把DllGetClassObject作如下修改:
1 HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)
2 {
3 *ppv = 0;
4 if (IsEqualCLSID (rclsid, &CLSID_Outside))
5 {
6
7 if (!vpcfOutside)
8
9 {
10
11 HRESULT hr = CreateClassFactory (&CLSID_Outside, CreateOutside,
12 &IID_IClassFactory, &vpcfOutside);
13
14 if (hr != NOERROR)
15
16 return hr;
17
18 if(IsEqualIID(riid,&IID_IClassFactory))
19 {
20 *ppv = vpcfOutside;// Set *ppv to vpcfOutside directly instead of QueryInterface if first time creation
21 return NOERROR;
22 }
23 else
24 {
25 Release(vpcfOutside);// Any interface requested (riid) other than IID_ClassFactory and IID_Unknown not support by class factory,
26 // call Release to free the memory.
27 return E_FAIL;
28 }
29
30 }
31
32 return QueryInterface (vpcfOutside, riid, ppv);
33
34 }
35
36 return E_FAIL;
37 }
修改后在執(zhí)行,內(nèi)存都正常釋放了。

-------------------------------------------------------------------------------------------
CreateClassFactory代碼說(shuō)明
1 HRESULT CreateClassFactory (REFCLSID rclsid,
2 HRESULT (*pfnCreate)(IUnknown *, REFIID, void **),
3 REFIID riid, void **ppv)
4 {
5 ClassFactory *this;
6 HRESULT hr;
7
8 *ppv = 0;
9 if (hr = Alloc (sizeof (ClassFactory), &this))
10 return hr;
11
12 this->icf.lpVtbl = &vtblClassFactory;
13 this->cRef = 1; // After this call, cRef==1
14
15 this->pfnCreate = pfnCreate;
16
17 hr = QueryInterface (&this->icf, riid, ppv); // After this call, cRef==2
18 Release (&this->icf); // Corresponds to "this->cRef = 1", ater this call, cRef==1
19
20 return hr;
21 }
可以看到,兩行代碼的效果是對(duì)引用計(jì)數(shù)增1及減1,這兩行代碼執(zhí)行后,對(duì)引用計(jì)數(shù)的影響互相抵消,等于沒(méi)有改變引用計(jì)數(shù)。那么,把這兩行同時(shí)注釋掉,是不是可以呢?
我的回答是:在本例中可以。因?yàn)檫@兩行代碼之間的QueryInterface總是可以執(zhí)行成功的(因?yàn)槭怯肐DD_ClassFactory來(lái)調(diào)用該函數(shù)的)。所以,即便把這兩行代碼同時(shí)注釋掉,CreateClassFactory執(zhí)行結(jié)束后,類(lèi)廠對(duì)象的引用計(jì)數(shù)也增了1,以后調(diào)用Release就可以釋放掉類(lèi)廠對(duì)象占用的內(nèi)存。
但是,如果CFQueryInterface的代碼編寫(xiě)中除了錯(cuò)誤,比如,像這樣寫(xiě):
1 static HRESULT CFQueryInterface (IClassFactory *pcf, REFIID riid, void **ppv)
2 {
3 ClassFactory *this = IMPL (ClassFactory, icf, pcf);
4
5 if (IsEqualIID (riid, &IID_IUnknown) ||
6 // IsEqualIID (riid, &IID_IClassFactory)) // Comment out this condition to create an error
7 *ppv = &this->icf;
8 else
9 {
10 *ppv = 0;
11 return E_NOINTERFACE;
12 }
13
14 AddRef ((IClassFactory *)*ppv);
15
16 return NOERROR;
17 }
那么,這兩行代碼之間的QueryInterface就會(huì)執(zhí)行出錯(cuò),那么類(lèi)廠對(duì)象占用的內(nèi)存就永遠(yuǎn)沒(méi)有機(jī)會(huì)釋放了。
也就是說(shuō),AddRef和Release雖然在作用上對(duì)引用計(jì)數(shù)來(lái)說(shuō)相互抵消,但Release函數(shù)提供了釋放對(duì)象內(nèi)存的機(jī)會(huì)(當(dāng)引用計(jì)數(shù)為0時(shí)),如果不成對(duì)的調(diào)用他們,也就失去了管理對(duì)象內(nèi)存(釋放對(duì)象占用的內(nèi)存)的機(jī)會(huì)。
---------------------------------------------------------------------------
組件庫(kù)outside文件說(shuō)明:
IFoo.h IFoo接口聲明
outside.c 組件對(duì)象、IFoo接口實(shí)現(xiàn)
cf.c 類(lèi)廠對(duì)象、IClassFactory接口實(shí)現(xiàn)
outdll.c 組件庫(kù)導(dǎo)出函數(shù)實(shí)現(xiàn)
outside.def 組件庫(kù)模塊定義文件,導(dǎo)出函數(shù)聲明
outside.reg 組件庫(kù)注冊(cè)文件
----------------------------------------------------------------------------
源碼:
outside-cf