BSTR CComBSTR及ATL字符串轉換宏
摘要: 總結了BSTR,CComBSTR及ATL字符串轉換宏。 閱讀全文
摘要: 總結了BSTR,CComBSTR及ATL字符串轉換宏。 閱讀全文
在C實現COM接口系列1中實現的com接口IFoo與使用它的客戶耦合在一起,沒有實現在各自分離的模塊,因此不符合模塊化編程思想。本期添加類廠支持,以使接口的實現與接口的使用相分離。
---------------------------------------------------
類廠的作用到底是什么?
將接口的實現與客戶使用分離開來嗎?
不盡然。使用CoCreateInstance,客戶可以完全不必知道類廠的存在,而創建組件,獲取組件實現的接口并使用。
即COM庫可以完全拋開類廠的概念,而是提供一個這樣的函數原型:
CoCreateObject(REFID rclsid,...,REFID riid,void **ppItf);
用戶在調用的時候可以對riid提供IID_Unknown或者特定于該對象的一個接口,直接獲取該對象的IUnknown或特定的接口指針。
可以看到,這正是CoCreateInstance所作的事情。
1 類廠提供了間接創建類對象的方式:用戶可以先獲取并持有類廠接口指針,通過該指針所指向的類廠接口創建類對象。適用于需要創建多個(或重復創建)類對象的地方,減少了每次都要定位對象庫并把對象庫裝入內存的開銷。
2 類廠提供了保證組件庫留在內存不被卸載出去的另一種方法:類廠接口函數LockServer。組件庫維護一個庫范圍計數器,只有該計數器為0時,組件庫才允許自己被卸載出內存。(與此相對,引用計數是類對象范圍的,通過該類實現的各個接口來維護。如果一個類對象的引用計數達到0,那么該對象占有的內存就被釋放,該對象上的接口指針也不再有效。)
除了調用LockServer鎖定組件庫以外,當創建的組件個數大于0時,組件庫也不能被卸載。也可以說,調用一次LockServer()的作用相當于創建了一個組件。
-----------------------------------------------------------------------
客戶一側:
1 使用一個接口需要知道哪些信息?
備選:
接口IID
類對象(類廠)CLSID(或ProgID)
接口函數原型(參數個數,類型,返回值)
實現接口組件的線程模型(進程內、進程外、遠程)?
類型庫typelib信息?
服務一側:
2 實現一個組件和接口以供客戶調用,需要提供哪些東西?
備選:
所有客戶使用組件和接口所需的內容
額外的還有:
--------------------------------------------------------------------
為dll添加.def文件與直接在需要導出的函數定義處指定_declspec( dllexport )有區別嗎?如果有是什么區別?
我發現在outdll.c中這樣指定:
__declspec( dllexport ) HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)
會產生編譯錯誤:
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的解釋意思是出錯的函數使用的鏈接指示符與之前聲明的不同。
Compiler Error C2375
'function' : redefinition; different linkage
The function is already declared with a different linkage specifier.
objbase.h中聲明了DllGetClassObject()函數:
STDAPI DllGetClassObject(IN REFCLSID rclsid, IN REFIID riid, OUT LPVOID FAR* ppv);
而使用.def文件就沒有問題。
-----------------------------------------------------------------------------
初次執行結果:
問題就是總有一個分配的內存沒有釋放:
根據打印出來的內存地址可以判斷,應該是先創建的類廠對象的內存沒有釋放。
檢查代碼,main()中并沒有忘記調用Release(pCF)釋放類廠對象。打印Release(pCF)的返回值,發現是1,即在類廠接口指針上少調用了一次Release,那么,究竟是哪里少的呢?
main()函數中有關類廠對象引用計數的地方就是CoGetClassObject和Release(CreateInstance跟類廠自己的引用計數無關),這是一對增加引用計數和減少引用計數的對應操作,所以,main()中應該沒有問題。
那么,就只有創建類廠對象的時候了。下面看一下類廠對象是如何創建的。
首先,main調用CoGetClassObject,該函數就調用dll中的DllGetClassObject。由于是第一次調用(不考慮其他客戶使用該dll的情況),程序執行到CreateClassFactory(...),該函數執行完后,類廠對象的引用計數是1。
由于創建成功,因此繼續向下執行到QueryInterface,此時,類廠對象的引用計數變成了2。然后,DllGetClassObject返回,com庫函數CoGetClassObject也應該返回。注意,此時的類廠對象引用計數已經是2了!
因此,問題就出在這里。main調用一次CoGetClassObject后,類廠對象的引用計數是2,而不是我想向中的1。于是,后面調用一次Release也就當然無法釋放掉類場對象了。
找到了原因,改正就很容易了。這里我覺得需要把DllGetClassObject作如下修改:
修改后在執行,內存都正常釋放了。
-------------------------------------------------------------------------------------------
CreateClassFactory代碼說明
可以看到,兩行代碼的效果是對引用計數增1及減1,這兩行代碼執行后,對引用計數的影響互相抵消,等于沒有改變引用計數。那么,把這兩行同時注釋掉,是不是可以呢?
我的回答是:在本例中可以。因為這兩行代碼之間的QueryInterface總是可以執行成功的(因為是用IDD_ClassFactory來調用該函數的)。所以,即便把這兩行代碼同時注釋掉,CreateClassFactory執行結束后,類廠對象的引用計數也增了1,以后調用Release就可以釋放掉類廠對象占用的內存。
但是,如果CFQueryInterface的代碼編寫中除了錯誤,比如,像這樣寫:
那么,這兩行代碼之間的QueryInterface就會執行出錯,那么類廠對象占用的內存就永遠沒有機會釋放了。
也就是說,AddRef和Release雖然在作用上對引用計數來說相互抵消,但Release函數提供了釋放對象內存的機會(當引用計數為0時),如果不成對的調用他們,也就失去了管理對象內存(釋放對象占用的內存)的機會。
---------------------------------------------------------------------------
組件庫outside文件說明:
IFoo.h IFoo接口聲明
outside.c 組件對象、IFoo接口實現
cf.c 類廠對象、IClassFactory接口實現
outdll.c 組件庫導出函數實現
outside.def 組件庫模塊定義文件,導出函數聲明
outside.reg 組件庫注冊文件
用C實現的一個基本COM接口IFoo(來自COM Programmer's Cookbook)
把該文中實現的代碼整理匯總到一個項目中。目前只是實現到一個中間階段,重點在說明COM接口的實現原理,還沒有包含類廠的部分。以后還需陸續添加類廠等高級功能。
文件組成:
ifoo.h COM接口IFoo,接口ID IID_IFoo 聲明文件。
outside.c COM接口實現。這里實現IFoo的是一個結構體COutside.
util.h 一些宏定義、全局函數、變量聲明文件。
main.c 筆者為實現項目添加的文件。提供main函數、內存管理函數Alloc,Free的實現(封裝C運行庫函數malloc和free.)、接口ID定義。
COM接口到底是什么?
COM接口是一個指向虛函數表的指針。通過這個指針可以訪問內存中某處的各個功能塊,執行預定義的功能,完成用戶的任務。這些功能塊以函數的形式存在(想不出還有其他形式:))并被調用。它們有一個共同點:都包含一個指針參數,指向這些功能要操作的數據地址。在C++中,這個地址就是對象的首地址,也就是類成員函數中隱含的this指針。在C函數中并沒有這種現成的便利,因此代碼實現中在接口定義時仍使用了接口指針(HRESULT (__stdcall * QueryInterface) (IFoo * This, const IID * const, void **)),而在接口函數實現時根據結構體布局結構,從這個接口指針推算得到對象實例指針。
typedef struct IFoo
{
struct IFooVtbl * lpVtbl;
} IFoo;
typedef struct IFooVtbl IFooVtbl;
struct IFooVtbl
{
HRESULT (__stdcall * QueryInterface) (IFoo * This, const IID * const, void **) ;
ULONG (__stdcall * AddRef) (IFoo * This) ;
ULONG (__stdcall * Release) (IFoo * This) ;
HRESULT (__stdcall * SetValue) (IFoo * This, int) ;
HRESULT (__stdcall * GetValue) (IFoo * This, int *) ;
};
COM接口的要求:
每一個COM接口(指向的虛函數表)的頭三個函數必須是IUnknown接口的函數:QueryInterface,AddRef和Release。在C++中,稱為從IUnknown接口繼承。
對于調用QueryInterface響應查詢IID_IUnknwon得到的接口指針值,同一個對象實現的所有接口必須相同。這是判斷兩個COM對象是否是同一個對象的標準。
宏定義“#define IUNK_VTABLE_OF(x) ((IUnknownVtbl *)((x)->lpVtbl))“說明
在預處理輸出文件main.i中可以找到IUnknownVtbl和IFooVtbl的聲明:
typedef struct IUnknownVtbl
{
HRESULT ( __stdcall *QueryInterface )(
IUnknown * This,
const IID * const riid,
void **ppvObject);
ULONG ( __stdcall *AddRef )(
IUnknown * This);
ULONG ( __stdcall *Release )(
IUnknown * This);
} IUnknownVtbl;
struct IUnknown
{
struct IUnknownVtbl *lpVtbl;
};
struct IFooVtbl
{
HRESULT (__stdcall * QueryInterface) (IFoo * This, const IID * const, void **) ;
ULONG (__stdcall * AddRef) (IFoo * This) ;
ULONG (__stdcall * Release) (IFoo * This) ;
HRESULT (__stdcall * SetValue) (IFoo * This, int) ;
HRESULT (__stdcall * GetValue) (IFoo * This, int *) ;
};
該宏定義的作用就是把IFoo接口中的IFooVtbl類型的指針拿出來((x)->lpVtbl)),并強制轉換((IUnknownVtbl *))成IUnknownVtbl。
“強制轉換”的結果是什么呢?是怎么做到的呢?
很明顯,結果就是得到的指針不再是IFooVtbl *類型,而是變成了IUnknownVtbl *類型。至于做法,系統應該記錄每一個變量、表達式的類型。當進行強制類型轉換時,就(臨時地)修改其類型為轉換到的類型。
同理,QueryInterface, AddRef, Release宏定義中的(IUnknown *)也是這種用法。
可以看到,宏“IUNK_VTABLE_OF“的作用是供宏QueryInterface,宏AddRef,宏Release引用,把IFooVtbl *類型轉換為IUnknownVtbl *類型,最終達到調用IUnknownVtbl中定義的三個QueryInterface,AddRef,Release函數。
那么,這種大費周章的目的是什么呢?為什么不以IFooVtbl中三個函數的定義形式(不通過強制轉換來轉換成必須的類型),直接調用IFooVtbl中定義的函數呢?雖然強制轉換在參數值上并不會造成改變,最終調用的也是IFooVtbl定義的函數(FooQueryInterface,FooAddRef,FooRelease)。
為什么一定要通過IUnknown接口指針調用這三個函數呢?修改QueryInterface宏定義如下:
#define QueryInterface(pif, iid, pintf) \
(((pif)->lpVtbl)->QueryInterface(pif, iid, (void **)(pintf)))
即通過IFoo接口指針來調用由IUnknown引入的函數,有什么不對的地方嗎?
試驗表明,將QueryInterface宏定義如下也可以編譯通過,執行起來也沒有出現任何異常。
#define QueryInterface(pif, iid, pintf) \
(((pif)->lpVtbl)->QueryInterface(pif, iid, (void **)(pintf)))
對于IUnknown接口的三個函數,調用時傳遞的參數是IUnknown *類型(見QueryInterface, AddRef, Release宏定義),而函數定義中(FooQueryInterface, FooAddRef, FooRelease)聲明的參數是IFoo *類型,這種不一致的情況是怎么出現的?這種不一致不會有問題嗎?
這種不一致的產生是由于從不同的角度看待引起的。如果從IUnknown接口來看,那么接口函數中的第一個參數類型就是IUnknown *;如果從IFoo來看,那么第一個參數的類型就是IFoo *。
這種不一致性只是針對于編譯器對于類型的編譯要求有意義的,在接口實現及使用時,傳遞給lpVtbl->QueryInterface, lpVtbl->AddRef,lpVtbl->Release的第一個參數在值上都是相同的,都是實現該接口的內存地址(在本例中是COutside對象的首地址)。
一些語法現象回顧
函數指針變量定義、賦值及調用。
HRESULT (__stdcall * pQI) (IFoo * This, const IID * const, void **) ;
定義一個函數指針變量pQI,該變量指向“返回HRESULT,取3個參數分別為類型IFoo *,const IID * const, void **”的函數。
typedef HRESULT (__stdcall * QIType) (IFoo * This, const IID * const, void **) ;
定義一個函數指針類型,該類型的指針指向“返回HRESULT,取3個參數分別為類型IFoo *,const IID * const, void **”的函數。
HRESULT __stdcall QueryInterface(IFoo * This, const IID * const, void **) ;//函數聲明示例
pQI = 0; // 函數指針賦值,0表示不指向任何函數。
pQI = QueryInterface; // 函數指針賦值,pQI指向QueryInterface。
pQI = &QueryInterface; // 與上面等價。
QueryInterface(&this->ifoo, riid, ppv); // 使用函數名直接調用
pQI(&this->ifoo, riid, ppv); // 函數指針調用
(*pQI)(&this->ifoo, riid, ppv); // 第二種函數指針調用方式
宏定義、展開規則
對于宏,一直有一種霧里看花的感覺,似乎很隨意,怎么來都行,比如:
#define AddRef(pif) \
(IUNK_VTABLE_OF(pif)->AddRef((IUnknown *)(pif)))
宏定義應該是可以嵌套的,即宏定義的“內容“中還可以包含(嵌套)宏,如本例,“IUNK_VTABLE_OF”就是嵌套宏。在展開的時候,將嵌套的宏也一并展開(替換成定義的內容),直到不再有宏為止。
那么就有兩個疑問:
1。如果被嵌套的宏包含(直接或間接)定義的宏,那么展開就沒完沒了,死循環了。
2。如果定義的內容中有跟定義的宏同名的字符串(比如上面的例子IUNK_VTABLE_OF),那么怎么區分這同名的東東是嵌套的宏(需要展開),還是一般的字符串(不需要展開)?
函數調用規范約定、main函數調用規范。
一開始把幾個文件匯總到項目里時,編譯通不過,錯誤提示大致意思是,不能把一種調用規范的函數指針轉換成另一種調用規范的函數指針。后來把調用規范改為 /Gz(__stdcall),編譯為(Compile As)改為/TC(Compile As C Code)就好了。
想來是對于.c文件,編譯器缺省使用的是__cdecl,而IFoo中的接口宏定義在win32下展開成了__stdcall,所以出現了矛盾。而使用/Gz強制未聲明調用規范的函數使用__stdcall,實現就與聲明一致了。
(size_t)&(((s *)0)->m)
c++程序員也許都知道,訪問地址“0”處的成員是一大忌,會造成GP。然而,取地址“0”處的成員的地址,卻是個合法的操作。雖然地址“0”處并沒有什么內容,但是,如果在地址0處存放一個內容,那么該內容中的成員也是有地址的。本例中正是巧妙地利用這種方法,從接口地址計算得出實現該接口的實例地址,進而訪問實例的內部變量。
------------------------------------------------------------------------------------
2009年5月6日
附上源碼:/Files/gracelee/outside.zip
代碼執行結果:
終于弄清楚了原來說的同一個頭文件不能被兩次或兩次以上包含是針對同一個源文件而言的。借用80后的流行語,真是漢哪!
原貼地址:http://www.keil.com/forum/docs/thread10237.asp
作者 Per Westermark
The
#ifndef xx #define xx ... #endif
method is to make sure that a header file isn't included more than once from the same c file.
You can not - and normally don't want to - stop multiple c files from including the same header file.
A header file is included because:
1) You have specifically added a line #include "xx" or #include <xx> in the source file. Don't do that unless you want the file to be included :)
2) You are including one header file, that it it's turn (one or more steps away) includes another header file. But a header file should only contain a recursive #include if it really needs that other file for some declarations. Hence, you need to include it.
What does this mean?
If the header file must be seen by multiple source files, you can't use it to allocate global variables, since the linker would then complain about multiple sets of global variables with the same name. This can be solved with the following:
//globals.h #ifndef _GLOBALS_H #define _GLOBALS_H #if defined MAIN #define EXTERN #else #define EXTERN extern #endif ... EXTERN int my_global_variable; #endif // _GLOBALS_H
// main.c
#define MAIN
#include "globals.h"
...
// misc.c #include "globals.h" ...
In this case, only the inclusion in main.c will result in an "allocation" of global variables, because the #define EXTERN will be empty. All other source files that includes "globals.h" will just see the type information for the global variables.
用戶自定義類(class)類型可以當作系統內建類型(build-in type)來處理,對這一點我一直很驚奇,也很迷惑,特別是在類定義、繼承關系很復雜的時候,要找到來龍去脈真的很抓頭。最近工作中碰到這一塊的東西,順便借這個機會澄清一些概念。
看下面代碼:
由于定義了操作符重載CDerived::operator long() 和CBase::operator long(),49行得以編譯通過。同理,定義了CBase::operator char(),51行可以編譯。
執行結果為:
CBase constructor()
CDerived constructor()
CDerived::operator long()
CBase::operator long()
lTmp=0
CBase::operator char()
cTmp=a
CDerived destructor()
CBase destructor()
這里涉及到的概念主要有:
1 類成員操作符重載(使得用戶定義類型轉換為內建類型成為可能。對于用戶定義類型之間的轉換,還可以通過構造函數的方式進行)
2 自動類型轉換。自動類型轉換發生的情況有以下幾種:
函數調用時傳遞的實參類型與函數聲明中指定的參數類型不匹配
函數返回的對象類型與函數聲明中指定的返回類型不匹配
表達式中操作數的類型不一致(這正是上面例子中的情況)
看看最新的隨筆,竟然有一年多沒來了,自己設的博客標題,真是對自己的諷刺。
很久以前寫的帖子,現在看到還有人在回復,有點感動,也受到一點鼓舞:如果有什么是永恒的話,那么文字當屬一種吧,雖然我蒼白的語言會在網絡浩瀚而五光十色的信息中被深埋而無人注意,畢竟也是一種曾經存在過,發生過的見證。
記錄自己的心情,讓生命在這種書寫中找到歸屬和安慰,這就是博客于我的魅力吧。
工作中經常會需要計算一大串字節的異或,用計算器手工一個一個的輸入太累人了,碰巧這兩天在關注VBScript,于是就想到寫了這個工具。這是我第一次用VB,代碼很簡單,希望能對你有用。
寫代碼時經常會用到用sprintf格式化某個字符串,比如:
sprintf(mess,"This is field 1,Field 2,Field 3\n");
當這樣的域很多的時候,一行放不下,為了便于閱讀,需要把它們分成幾行:
sprintf(mess,"This is filed 1,\
Field 2,\
Field 3,\
...
Field n\n");
但是,這樣帶來個問題,就是格式化后的mess的各個域之間就產生了不想要的字符(如空格等,使用UE可以清楚的看到),原因是由于使用了續行符"\"(line-continuation character),而不用續行符又無法通過編譯(C2001)。
這時可以用雙引號來把各個域分隔開,這樣就既解決了可讀性的問題,又解決了編譯問題:
sprintf(mess,"This is filed 1,"
"Field 2,"
"Field 3,"
...
"Field n\n");
1.noun 生活,生計
What Londoners do to make a living have changed considerably since the 19th century.
19世紀以來,倫敦人謀生的方式發生了很大改變。
幾個常見的詞組:
living room
living condition
2.adj 活的
London is itself a living museum with thousand years of history and culture.
倫敦以其幾千年的歷史和文化,其本身就是一座博物院。
“living”作為形容詞用就有必要提一下“live”。“live”作形容詞也有“活的”意思,但感覺上這里似乎不能替換“living”,不知道對不對。