句柄和指针的区别
這是初學者最常問及的問題,一些面試官也很喜歡問這個問題 。
當把硬盤上的資源調入內存以后,將有一個句柄指向它,但是句柄只能指向一個資源。而且句柄知道所指的內存有多大。還有指針,指針指向地址,它不知道分配的內存有多大。??
但是如果你定義一個句柄,然后在VC里面右擊鼠標,選擇"go to definition of handle”,你會發現它的本質就是一個指針,但是它的作用不同于指針。它和通常意義上的指針是有區別的。句柄借用了指針的思想,有它的邏輯特點,但沒有它的物理功能。句柄是WINDOWS分配給窗口等資源的唯一標識,是一個整數。
一、書上定義:
<<Microsoft Windows 3 Developer''s Workshop>>(Microsoft Press,by Richard Wilton)
在Windows環境中,句柄是用來標識項目的,這些項目包括:模塊(module)、任務(task)、實例 (instance)、文件(file)、內存塊(block of memory)、菜單(menu)、控制(control)、字體(font)、資源(resource),包括圖標(icon),光標 (cursor),字符串(string)等、GDI對象(GDI object),包括位圖(bitmap),畫刷(brush),元文件(metafile),調色板(palette),畫筆(pen),區域 (region),以及設備描述表(device context)。
<<WINDOWS編程短平快>>(南京大學出版社):
句柄是WONDOWS用來標識被應用程序所建立或使用的對象的唯一整數,WINDOWS使用各種各樣的句柄標識諸如應用程序實例,窗口,控制,位圖,GDI對象等等。WINDOWS句柄有點象C語言中的文件句柄。
二、MFC源代碼:
#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
DECLARE_HANDLE(HMODULE);
DECLARE_HANDLE(HINSTANCE);
DECLARE_HANDLE(HLOCAL);
DECLARE_HANDLE(HGLOBAL);
DECLARE_HANDLE(HDC);
DECLARE_HANDLE(HRGN);
DECLARE_HANDLE(HWND);
DECLARE_HANDLE(HMENU);
DECLARE_HANDLE(HACCEL);
DECLARE_HANDLE(HTASK);
三、理解:
句柄是一個32位的整數,實際上是windows在內存中維護的一個對象(窗口等)內存物理地址列表的整數索引。因為windows的內存管理經常會將當前空閑對象的內存釋放掉,當需要時訪問再重新提交到物理存儲,所以對象的物理地址是變化的,不允許程序直接通過物理地址來訪問對象。程序將想訪問的對象的句柄傳遞給系統,系統根據句柄檢索自己維護的對象列表就能知道程序想訪問的對象及其物理地址了。
句柄是一種指向指針的指針。我們知道,所謂指針是一種內存地址。應用程序啟動后,組成這個程序的各個對象是駐留在內存的。如果簡單地理解,似乎我們只要獲知這個內存的首地址,那么就可以隨時用這個地址訪問對象了。但是,如果真這么認為,那么就大錯特錯了。我們知道windows是一個虛擬內存為基礎的操作系統。在這種情況下,windows內存管理器經常在內存中來回移動對象,以此來滿足各種應用程序的內存需要,對象被移動意味著它的地址變化了。如果地址總是如此的變化,我們應該去那里找對象呢?為了解決這個問題,windows操作系統為各個應用程序騰出一些內存地址,用來專門登記各個應用對象在內存中的地址變化,而這個地址(存儲單元的位置)本身是不變的。windows內存管理器移動對象在內存中的位置后,把對象新的地址告知這個句柄地址來保存。這樣我們只需要記住這個句柄地址就可以間接地知道對象具體在內存中哪個位置了。這個地址是在對象裝載(load)時由系統分配的,當系統卸載時又釋放給系統。句柄地址(穩定)---->記載著對象在內存中的地址---->對象在內存中的地址(不穩定)---->實際對象。但是必須注意,程序每次重新啟動,系統不保證分配跟這個程序的句柄還是原來哪個句柄,而絕大多數情況下的確不一樣。假如我們把進入電影院看電影看成是一個應用程序的啟動運行,那么系統給應用程序分配的句柄總是不一樣,這和每次電影院給我們的門票總是不同的座位是一個道理。
?? 對于Wind32 API,盡管為每個對象分配了數據塊,但是微軟不想向用戶程序返回指針。對于一個聰明的程序員來說,指針包含了太多的信息。它給出了對象存儲的確切位置。指針一般允許對對象的內部表示進行讀寫操作,而這些內部表示也許正是操作系統想隱瞞的。指針還使越過進程地址空間共享對象變得困難。為了對程序員進一步隱藏信息,Win32對象創建程序實例一般會返回對象句柄。對象可以映射到唯一句柄,句柄也可由映射到唯一的對象。為了保證句柄能夠完成信息隱藏的的任務,對象和句柄之間的映射沒有任何文檔記載,不保證固定不變,而已僅有微軟知道這種映射,或者還有少數系統級工具開放商知道。
??? 對象指針和句柄之間的映射可以由函數Encode和Decode來實現,原型如下:
??? HANDLE Encode(void* pObject);
??? Void* Decode(HANDLE hObject);
??? 在極端情況下,句柄可以和對象指針相同,Encode和Decode只需做類型轉換,對象和句柄之間的映射主要是全等映射。
??? 在Win32 API中,實例句柄(HINSTANCE)或者模塊句柄(HMODULE)是指向映射到內存的PE文件映像的指針。LockResource用來鎖住全局資源句柄得到指針,但實際上它們的值相同。LockResource返回的資源句柄只是偽裝后的內存映射資源的指針。
??? 通常情況下,對象指針和句柄之間的映射是基于表格的映射。操作系統創建表格或者是一級表示保存所有要考慮的對象。需要創建新對象時,首先要在表格中找到空入口。然后就把表示對象的數據添入其中。當對象被刪除時,它的數據成員和它在中的入口被釋放,以便再利用入口。用這種基于表的對象管理方法,表中的索引可以很好的組成對象的句柄,編碼和解碼也很簡單。
??? (在Win32 API中,內核對象是用進程表實現的。為了容納大量內核對象,每個進程都有自己的內核對象表。NT/2000內核執行體中一部分是對象管理器,它只管理內核對象。對象管理器提供函數ObReferenceObjectByHandle。根據DDK(Driver Develepment Kits)文檔,它提供對象指針的解碼全過程,如果存取操作被批準,則會返回對象體相應的指針。因此對于一個把對象句柄翻譯稱為對象指針的解碼全程來說,額外的安全檢查很重要。www.internals.com上面有個非常好的工具HandleEx,它能夠列出Windows NT/2000的內核對象。
??? 只有句柄是不夠的,盡管句柄提供了近乎完美的抽象,信息隱藏和保護,但是它也是程序員遭受挫折的地方。在像Win32 API這樣以句柄為中心的API中,微軟沒有任何文檔記載對象的內部表示以及對象是如何管理的,也沒有提供參考實現,程序員只有函數原型,微軟文檔和或多或少基于微軟文檔的書籍。程序員面臨的首要問題包括系統資源。當對象被創建,對象的句柄被返回時,誰都不知道對象用了什么資源,因為對象的內部表示是不知道的。程序員是應該保護該對象還是應該在對象沒有用時盡快把它刪除呢?GDI支持的三種位圖,為了減少系統資源消耗,應該使用哪一種呢?CPU時間時計算機的主要資源。當內部表示對程序員隱藏時,程序員就很難在復雜的算法設計中判斷這種操作的復雜性如果你用GDI組成復雜區域,算法的復雜度是O(n)(問題規模n),O( )(問題規模)還是O()。隨著程序的隱藏,調試也成問題。程序運行5分鐘顯示了一些垃圾數據,猜測由資源泄漏,但是泄漏在哪兒?怎么解決?如果是處理系統中幾百個應用程序的管理員,當系統資源很少時,如果找出問題?唯一可以用的資源泄漏工具是BoundsChecker,它依賴API窺視技術查出對象創建和刪除之間的不匹配之處。最讓人受挫的地方可能是程序的兼容性。程序為什么能在Windows95下把GDI對象從一個進程傳遞到另外一個進程,而Windows NT/2000不行?為什么Windows95不能處理大的設備無關圖?
??? 以GDI對象為例子,創建了GDI對象,就會得到該對象的句柄。句柄的類型有可能是HPEN,HBRUSH,HFONT或者是HDC中的一種。但最普通的 GDI對象類型是HGDIOBJ,它被定義成為空指針。HPEN的實際編譯類型是隨著時間宏STRICT的不同而不同。不同GDI句柄的定義模仿了GDI 對象不同類的類層次結構,但是沒有真正的類層次結構。GDI對象一般有多個創建函數和一個接受HGDIOBJ的析構函數——DeleteObject。也可以用GetStockObject取得預先創建好的GDI對象句柄,無論GetStockObject調用順序是如何,它返回的句柄看起來總是常數。甚至當運行一個程序的兩個實例時,它在每個進程中返回相同的,唯一解釋是對象句柄堆是不變的,系統初始化,堆對象被創建并被所有進程重復使用。盡管 Windows頭文件把GDI句柄定義成為指針,但是檢查這些句柄的值時,它們根本不像指針。生成幾個GDI對象句柄并看一下返回句柄的十六進制顯示,就會發現結果從0x01900011變化到0xba040389。如果HGDIOBJ像在Windows頭文件里面定義的那樣是指針,則前者是指向用戶地址空間中未分配的無效指針,而后者是執行內核地址空間。這就暗示GDI句柄不是指針。另一個發現是GetStockObject(BLACK_PEN)和 GetStockObject(NULL_PEN)返回值只相差一,如果句柄真的是指針的話,這不可能是存儲內部GDI對象的空間,因此可以肯定的說 GDI對象句柄不是指針。系統GDI句柄數限制為16384個,進程GDI句柄數限制為12000個。這樣單獨的進程不會搞亂整個GDI系統。但是 Windows 2000第一版沒有對每個進程加以限制。現在在同一個系統下運行兩個GDIHandles,在每一個進程中調用8192次CreatePen。第一個很好的創建了對象,第二個在7200左右會停止。第二個進程失敗后,整個屏幕一團糟,這個試驗表示GDI對象同樣是從同一個資源池分配的。系統中的進程使用 GDI資源時會互相影響。把8192和7200相加。考慮到GDIHandle屬性頁面和其它進程的頁面使用的GDI對象句柄,可以猜測,GDI句柄數目有系統范圍限制:16384。GDI對象存儲于系統范圍內的固定大小的對象表中,稱之為對象句柄表。它是一個大小固定的表,而不是一個會動態增長的數據結構。這就意味著簡明和效率。但是???? 缺點就是前面說的,限制了GDI句柄數:16384個。下面看看HGDIOBJ的構成,Windows NT/2000下,GDI返回的對象句柄是32位值,組成8位十六進制數。如果用GDIHandles創建很多GDI對象,注意到其中顯示的雙字句柄的低位字,會發現它們都在0x000到0x3FFF之間。低位字在進程中總是唯一的,出了堆對象外,低位字甚至在進程中也是唯一的。句柄的低位有時候按照遞增的順序排列,有時候又遞減。在進程間也是這樣。例如,某些情況下,CreatePen在低位返回0x03C1,另一個進程中的下一個CreatePen在低位返回0x03C3。對這些現象的解釋是HGDIOBJ的低位字是對系統范圍的16384個GDI對象所組成的表的索引。再來關注高4位的十六進制數。創建幾個畫刷,幾個畫筆,幾個字體,DC等。不難看出相同類型的GDI對象句柄有個共同特點:相同類型的對象句柄的第三位和第四位十六進制數幾乎都是相同的。畫刷句柄的第三位和第四位總是0x90和0x10,畫筆總是0x30和0xb0等等。最高位是1(二進制)的對象句柄都是堆對象。因此可以有足夠的證據說對象句柄的第三位和第四位十六進制數是對象類型的編碼和堆對象標記。在32位GDI句柄值中余下的兩個十六進制位暫時還沒找到有意義的模式。總結一下,GDI對象句柄由8位位置高位,一位堆對象標記,7位對象類型信息和高四位為0的16位索引組成。因為GDI對象表是由系統中所有過程和系統DLL所共享的,桌面,瀏覽器,字處理器以及DirectX游戲都在為同一個GDI句柄的儲存區而競爭。而在Windows 2000中,DirectX則使用一個獨立的對象句柄表。GDI句柄表一般存儲在內核模式的地址空間里以使圖形引擎能很容易訪問它,通過一定技巧,將為每個使用GDI的進程在用戶模式存儲空間里面建立表的只讀視圖。在Windows 2000終端服務中,每個對話都有它自己的Windows圖形引擎和視窗管理器(WIN32K.SYS)的拷貝,以至于系統中有多個GDI對象表。
??? GDI句柄表的每一個入口都是一個16字節的結構體,如下面代碼所示:
??? Typedef struct
??????? {
??????? void*??????? pKernel;
??????? unsigned short nPaid;
??????? unsigned short nCount;
??????? unsigned short nUnique;
??????? unsigned short nType;
??????? void*??????? pUser;
??? } GdiTableEntry;
??? 可見:GDI對象有一個指向它的內核模式對象的指針,一個指向其用戶模式的指針,一個過程ID,一個種類的計數,一個唯一性的標準值以及一個類型標識符。多么完美,優雅的設計!
??? 盡管Win32 API不是面相對象的API,但它也面臨著和面相對象語言一樣要解決的問題,即信息的隱藏和抽象數據類型,而且Win32 API比面相對象語言更想隱藏對象。用于創建Win32對象的Win32函數隱藏了該對象的大小和儲存位置,而且沒有返回指向對象的指針,而是僅僅返回該對象的句柄。Win32 API句柄是Win32對象一一對應的變量,它僅能被操作系統識別。分析一下,你會發現Win32使用了相當多的抽象數據類型,如文件對象,它包括了許多具體的對象類型,我們可以用CreateFile來創建文件,管道,通訊端口,控制臺,目錄以及設備等,但是這些操作都返回一種類型的句柄。跟蹤到 WriterFile中,會發現最后的操作其實是操作系統中不同例程甚至是不同產商提供的設備驅動程序來處理的,這就像是C++的虛函數和多態機制。同樣,在GDI域中,設備上下文被當作一個抽象對象類型看待。創建或者檢索打印機設備上下文,顯示設備上下文,內存設備上下文和圖元文件上下文的程序都將返回同類型的設備上下文句柄。顯而易見的是,同年國國句柄的類屬繪圖調用是通過不同的例程來處理的,而這些例程但是由GDI或者圖形設備驅動程序通過物理設備結構中的函數指針表來實現的。因此實現這樣的機制也會像C++一樣有一個類似于虛函數表的函數指針表,一個變量和函數指針通過這個表形成映射,方便的實現這種虛函數和多態機制,這個變量就是句柄....?
因此,句柄和指針其實是兩個截然不同的概念。windows系統用句并標記系統資源,用句并隱藏系統信息。你只需要知道有這個東西,然后去調用它就行了,它是32bit的uint。指針則標記某個物理內存的地址,是不同的概念。
指針對應著一個數據在內存中的地址,得到了指針就可以自由地修改該數據。Windows并不希望一般程序修改其內部數據結構,因為這樣太不安全。所以Windows給每個使用GlobalAlloc等函數聲明的內存區域指定一個句柄(本質上仍是一個指針,但不要直接操作它),平時你只是在調用API函數時利用這個句柄來說明要操作哪段內存。當你需要對某個內存進行直接操作時,可以使用GlobalLock鎖住這段內存并獲得指針來直接進行操作。
句柄是指針的“指針”,使用句柄主要是為了利于windows在進程內存地址空間移動分配的內存塊,以防止進程的內存空間被撕的四分五裂而存在過多的碎片。
句柄是一些表的索引也就是指向指針的指針。間接的引用對象,windows可以修改對象的"物理"地址和 描述器的值,但是句柄的值是不變的。
句柄可以在獲得窗口的時候使用,指針可以進行調用窗口,兩個使用的地方不一樣.一個括號外,一個括號內.
?
從窗口指針獲取窗口句柄:GetSafeHwnd();
從窗口句柄獲取臨時窗口指針:FromHandle();
從窗口句柄獲取永久窗口指針: FromHandlePermanent();
其實兩者被沒有關系,實際上是MFC在創建窗口的時候用鉤子函數溝住HCBT_CREATEWND消息,
然后通過CWnd::Attach()函數把二者捆綁在一起。
以后就可以用GetSafeHwnd(),FromHandle(),FromHandlePermanent()這三個函數可以互相得到了。
MFC之所以要這樣做,主要是為了使原來的SDK面向過程的編程遍成面向對象的編程,所有的MFC的窗口都共用一窗口過程函數,在窗口過程函數里,通過窗口句柄(HWND)找到窗口對象指針(CWnd *)從而把消息分發到窗口對象中,這樣以后就可以在窗口類中實行消息響應編程處理了。
附注一:獲得窗口句柄三種方法
1.HWND FindWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName)
HWND FindWindowEx(HWND hwndParent, HWND hwndChildAfter,LPCTSTR lpClassName, LPCTSTR lpWindowName)
2.HWND WindowFromPoint(POINT& Point)//獲得當前鼠標光標位置的窗口HWND
3.BOOL CALLBACK EnumChildProc(HWND hwnd,LPARAM lParam)
BOOL CALLBACK EnumChildWindows(HWND hWndParent, WNDENUMPROC lpEnumFunc,LPARAM lParam)
BOOL CALLBACK EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam)
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
?
附注二:指針 句柄之間的轉換
a.由指針獲得句柄?
CWnd * pWnd;?
CWnd HWnd;?
HWnd = pWnd->GetSafeHWnd();
b.由句柄得到指針:
CWnd* pWnd=FromeHandle(hMyHandle);?
pWnd->SetWindowText("Hello World!");?
or CWnd* pWnd; pWnd->Attach(hMyHandle);
MFC類中有的還提供了標準方法,比如Window 句柄 :?
static CWnd* PASCAL FromHandle( HWND hWnd );?
HWND GetSafeHwnd( ) const;
對于位圖:?
static CBitmap* PASCAL FromHandle( HBITMAP hBitmap );?
static CGdiObject* PASCAL FromHandle( HGDIOBJ hObject );?
HGDIOBJ GetSafeHandle( ) const;
總結
- 上一篇: 小白教程:Visual Studio20
- 下一篇: 数据结构 排序 java_Java数据结