參考資料:
1. http://www.codeforge.cn/read/146318/WinDef.h__html
windef.h頭文件
2. http://www.codeforge.cn/read/146318/WinNT.h__html
winnt.h頭文件
3. https://msdn.microsoft.com/en-us/library/windows/desktop/aa383681%28v=vs.85%29.aspx
微軟官網中關於STRICT的內容
C語言宏中"#"和"##"的用法
5. http://www.cnblogs.com/kerwinshaw/archive/2009/02/02/1382428.html
typedef和#define的用法與區別
6. http://blog.csdn.net/geekcome/article/details/6249151
void及void指針含義的深刻解析
寫在前面:
本文是對在下上一篇文章《圖解說明——究竟什么是Windows句柄》的擴充。同樣地,本文依然是面向初學者的。讓編程語言變得平易,讓初學者學起來你更加舒服,在交流中與廣大讀者同勉共進,是在下的一貫宗旨和追求。所以,在對源碼的解釋中,在下會盡可能做到詳細,具體到句,對於與句柄不是特別相關的內容,也會加以解釋說明。和上一篇一樣,我們仍然把窗口、位圖、畫筆等統稱為對象。
還有一點必須要交代,在下對Windows句柄這一細節的研究,還存在一些疑問,這將在文末進一步說明。
先看winnt.h中關於HANDLE(句柄)的定義:
typedef void *PVOID;
#ifdef STRICT
typedef void *HANDLE;
#define DECLARE_HANDLE(name) struct name##__ {
int unused;
};
typedef struct name##__ *name
#else
typedef PVOID HANDLE;
#define DECLARE_HANDLE(name) typedef HANDLE name
#endif
以上代碼typedef void *PVOID;來自winnt.h(參考資料2)中的第178行,其余代碼來自winnt.h中的第285~293行,考慮到易讀性,在下對代碼格式稍稍做了調整。
在分析源代碼之前,再說一點,那就是typedef和#define的區別的問題。typedef用來定義一個標識符及關鍵字的別名,而#define是宏定義,簡單說,就是字符串替換。如果有讀者還不是很明白,可以參閱參考資料5。在下面的敘述中,我們將兩者都譯為“定義”。因為在下覺得這樣可以帶來敘述上的方便,並且如果大家理解了typedef和#define的區別,這樣做並不會造成理解上的誤會。
下面我們開始逐句分析代碼。
首先,typedef void *PVOID;,這里將PVOID定義為void*型,以后,PVOID a,b;就相當於void *a,*b;(注意不是void *a,b;)。這里再簡單說一下void*,簡單說,void *就是“無類型指針”,可以指向任何數據類型,詳情可參閱參考資料6。
下面的一段總體上是if——else結構。我們先看if部分。
#ifdef STRICT
如果定義了STRICT,就執行后面的代碼。關於STRICT,在后面我們還會進行詳細的講解,這里我們暫時將其跳過,先看條件成立時的代碼。
#define DECLARE_HANDLE(name) struct name##__ {
int unused;
};
這里是一個帶參數的宏定義,name是參數,##為粘貼符號,表示把左右兩邊的內容連接起來。關於帶參數的宏定義和##,讀者可以參閱參考資料4。
這里將結構體
struct name##__
{
int unused;
};
定義為
DECLARE_HANDLE(name)。
接下來,
typedef struct name##__ *name
定義一個指針name,指向上面的結構體name##__。
下面我們以窗口句柄HWND為例,進一步說明。
在windef.h頭文件(見參考資料1)的第196行有代碼
DECLARE_HANDLE (HWND);
我們將宏展開,就是
struct HWND__
{
int unused;
};
同樣,根據typedef struct name##__ *name
有typedef struct HWND__ *HWND。
即句柄HWND是一個指針,指向結構體struct HWND__。
其它句柄的定義與HWND類似,這里不再贅述,讀者可以參閱參考資料1中從195行往后的代碼。
注意這里我們忽略了一個細節,那就是結構體中的int unused。關於這一點,我們先暫時忽略,在后面的“尚未解決”板塊,在下將對這一問題作出交代。
有了前邊的經驗,分析else部分的代碼就變得容易了,讓我們一起來看。
typedef PVOID HANDLE;
由於前邊有typedef void *PVOID;,所以這里HANDLE被定義為void*型。
接着,
#define DECLARE_HANDLE(name) typedef HANDLE name
這里將
typedef HANDLE name
定義為
DECLARE_HANDLE(name)。
還以HWND為例,在這種情況下,
DECLARE_HANDLE (HWND);
宏展開為
typedef HANDLE HWND,
即此時HWND為void*型。
好了,說完這些,我們着重說一下STRICT。相關內容請參閱參考資料3。
在windef.h頭文件的第13~17行定義了STRICT,源代碼如下:
#ifndef NO_STRICT
#ifndef STRICT
#define STRICT 1
#endif
#endif /* NO_STRICT */
這里僅僅是將STRICT定義為數值1,看不出什么名堂。關鍵在於編譯器(注意不是系統)對STRICT的“解釋”。
顧名思義,STRICT是“嚴格”、“嚴厲”的意思。當編譯器“看到”定義了STRICT后,就會對Windows 應用程序中使用的句柄進行嚴格的類型檢查。Windows官網中的原文為Enabling STRICT redefines certain data types so that the compiler does not permit assignment from one type to another without an explicit cast.
也就是說,如果定義了STRICT,除非顯式強制類型轉換,否則不允許將數據從一種類型轉化到另一種類型。換句話說,定義STRICT可以禁止隱式類型轉換。
那么,這是怎么實現的,又有什么用處呢? 以窗口句柄HWND和鈎子句柄HHOOk為例。在windef.h頭文件的第196、197行定義了HWND和HHOOK:
DECLARE_HANDLE (HWND);
DECLARE_HANDLE (HHOOK);
通過前面的分析,我們知道,如果定義了STRICT,那么自然執行#ifdef STRICT后的代碼,這樣,HWND就是HWND__*型的指針,而HHOOK就是HHOOK__*型的指針,兩者類型不同。如果沒有定義STRICT,那么將執行#else后的代碼,可以發現,這段代碼直接將所有句柄都定義為HANDLE,即PVOID,也就是void*型。在這種情況下,上面的HWND和HHOOK都是void*型,類型相同。那么,兩種情況下有什么差別呢?我們舉例說明。
現在一個函數要求一個HHOOK類型的參數,而我們傳給它一個HWND類型的參數。在沒有define STRICT的情況下,這將是合法的,因為HHOOK和HWND都是void*類型。而如果我們定義了STRICT,HHOOK和HWND就是兩個不同類型的指針,上面的參數傳遞將變為不合法,並且在編譯階段就會報錯,這就避免了直到程序出現了運行時錯誤,程序員才知道代碼有錯的情況。
順便說一句,現在VC、VS都define了STRICT,即都默認進行嚴格的類型檢查。
至此,相信大家已經明白了STRICT的作用以及為什么不直接用int unused而要用結構體將其封裝起來。
最后,讓我們一言以蔽之,來總結一下Windows句柄的本質:
Windows句柄本質上就是一個指向結構體的指針(define STRICT的情況下)。
而所謂“指針的指針”的說法並不正確,這只是一個邏輯上的理解。
尚未解決:
現在,讓我們回到前面忽略掉的關於int unused的問題上來。
如果有讀者看過了在下的上一篇文章《圖解說明——究竟什么是Windows句柄》,那么相信有人會和在下最初的想法一樣,認為這里的unused就是我們說的區域A。句柄指向一個結構體,而這個結構體中唯一的數據unused中存放着對象的地址(雖然unused不是指針類型,但int和指針同為4個字節,將對象的地址存到unused里,將來再用某種方式通過unused找到該對象,這也是可以實現的),這與我們先前的圖示恰好吻合。但稍加琢磨,我們發現這樣解釋在某些地方還是有些說不過去的。理由起碼有2:
①首先是名字問題,相信稍微細心的讀者就會發現這一問題。通過前邊的源代碼看,每一個名字都恰如其分地反映了它應有的意義,照這么看,結構體中的int變量存放了一個有用的地址,那它就不應該叫unused。
②如果unused相當於區域A的話,在define STRICT的情況下,句柄指向了區域A,而在沒有define STRICT的情況下,並沒有定義結構體,句柄被定義為void*型,那么,這種情況下的區域A又在哪里,句柄又如何指向它?
所以,綜合前面的分析,在下認為,unused並不是區域A。關於int unused,在下的一個猜想是:
進程創建時,系統在內存的一個地方存放各個對象的地址,同時系統為各個對象指定句柄,存放在內存中另一個地方,並使各個句柄指向相應對象的地址(即區域A)。至於如何指向,很可能是這樣:
對於未define STRICT的情況,直接指向就可以,因為此時句柄是void*型。而對於define了STRICT的情況,可能采用強制類型轉換或是相似的手段來使原先指向結構體的句柄指向一個32位的地址。注意到,原先句柄指向一個結構體,而這個結構體中只有一個int型數據,從內存的角度看,句柄其實指向了一段4字節的內存,而后來,句柄指向32位的地址,同樣是指向一段4字節的內存。而如果我們去掉intunused,只保留一個空結構體,我們知道,空結構體占1個字節(對這一點有疑問的讀者,可以寫一個空結構體,用sizeof()函數實測一下),此時句柄指向1個字節的內存,而現在要讓它指向4個字節的內存(32位地址),很可能會無法“轉化”。然而,如果轉化前后,句柄都指向4個字節的內存,那很可能就能夠轉化。所以,int unused的作用就是使句柄指向一個4字節的內存,以便將來句柄指向對象的地址時能夠順利“轉化”。而從始至終,unused從來沒有被顯式地使用過,所以取名為unused。顯然,這里unused的意思是“未被使用的”,而非“沒用的”。
至此,所有關於windows句柄這一細節的內容都講解完了。
寫在后面:
1.在下知識淺薄、能力有限,講解過程中難免有錯誤疏漏之處,這里懇請大家務必批評指正,在下先行謝過。
2.遺憾的是,到最后,還是有一些疑問沒有解決。這里在下請求路過的大神不吝賜教,也誠望廣大讀者各抒己見,讓我們一起思考,共同進步。