什么是Windows句柄?

176

在讨论Windows中的资源时,“句柄”是什么?它们是如何工作的?

7个回答

211

它是对资源的一个抽象引用值,通常是内存、打开的文件或管道。

准确地说,在Windows中(以及计算机领域一般),句柄是一个抽象层,将真实的内存地址隐藏起来,让API用户无法直接访问,从而允许系统透明地重新组织物理内存以适应程序。将句柄解析为指针会锁定内存,释放句柄会使指针失效。在这种情况下,可以将其视为指向指针表的索引...您使用索引进行系统API调用,系统可以随意更改指针表中的指针。

或者,当API编写者打算让API用户与返回地址指向的具体内容隔离时,也可以将真正的指针作为句柄给出;在这种情况下,必须考虑到句柄指向的内容可能随时更改(从API版本到版本,甚至从返回句柄的API的每次调用),因此句柄应被视为仅对API有意义的不透明值。

我应该补充说明,在任何现代操作系统中,即使是所谓的“真正的指针”,也仍然是进程虚拟内存空间中的不透明句柄,这使得操作系统可以管理和重新排列内存而不会使进程内的指针失效。


一个句柄的值是否有意义?我的意思是,如果一个句柄的值小于另一个句柄的值,这是否意味着该句柄比另一个句柄更早创建? - Vannes Yang
1
不,句柄的值没有实际意义。它应该被视为不透明的值。 - Lawrence Dol

116
一个 HANDLE 是上下文特定的唯一标识符。所谓上下文特定是指,从一个上下文中获得的句柄不能在任何其他与 HANDLE 相关的任意上下文中使用。
例如,GetModuleHandle 返回当前加载模块的唯一标识符。返回的句柄可以用于接受模块句柄的其他函数。它不能被赋予需要其他类型句柄的函数。例如,你不能将从 GetModuleHandle 返回的句柄给 HeapDestroy 并期望它做出合理的反应。
HANDLE 本身只是一个整数类型。通常情况下,但不一定,它是指向某些基础类型或内存位置的指针。例如,GetModuleHandle 返回的 HANDLE 实际上是指向模块的基础虚拟内存地址的指针。但没有规定句柄必须是指针。句柄也可以是一个简单的整数(可能会被某些 Win32 API 用作数组的索引)。
HANDLE 是有意不透明的表示,提供了对内部 Win32 资源的封装和抽象。这样,Win32 API 可能会更改 HANDLE 后面的基础类型,而不会以任何方式影响用户代码(至少这是想法)。
考虑我刚刚编造的三种不同的 Win32 API 的内部实现,并假设 Widget 是一个结构体。
Widget * GetWidget (std::string name)
{
    Widget *w;

    w = findWidget(name);

    return w;
}

void * GetWidget (std::string name)
{
    Widget *w;

    w = findWidget(name);

    return reinterpret_cast<void *>(w);
}

typedef void * HANDLE;

HANDLE GetWidget (std::string name)
{
    Widget *w;

    w = findWidget(name);

    return reinterpret_cast<HANDLE>(w);
}

第一个示例公开了有关API的内部细节:它允许用户代码知道GetWidget返回指向struct Widget的指针。这有一些后果:
  • 用户代码必须能够访问定义Widget结构的头文件
  • 用户代码可能会修改返回的Widget结构的内部部分
这两个后果都可能是不希望出现的。
第二个示例从用户代码中隐藏了这个内部细节,只返回void *。用户代码不需要访问定义Widget结构的头文件。
第三个示例与第二个完全相同,只是我们将void *称为HANDLE。也许这会阻止用户代码试图弄清楚void *指向的确切内容。
为什么要费这个劲?考虑这个API的同一版本的第四个示例:
typedef void * HANDLE;

HANDLE GetWidget (std::string name)
{
    NewImprovedWidget *w;

    w = findImprovedWidget(name);

    return reinterpret_cast<HANDLE>(w);
}

请注意,该函数的接口与上面第三个示例完全相同。这意味着用户代码可以继续使用此新版本的API,而无需进行任何更改,即使“幕后”实现已更改为使用NewImprovedWidget结构体。
这些示例中的句柄实际上只是void *的一个新名称,它是Win32 API中HANDLE的确切定义(在MSDN上查找)。它提供了一个不透明的壁垒,增加了使用Win32 API的代码在Windows版本之间的可移植性,使其与Win32库的内部表示之间的端口性增加。

6
我已经扩展了我的原始答案,并提供了一些具体的例子。希望这能让这个概念更加清晰易懂。 - Dan Moulding
4
这是我最近看到的回答问题最为干净、直接和写作最佳的之一。非常感谢您抽出时间写下这篇回答! - Andrew
@DanMoulding:因此,使用handle而不是void *的主要原因是阻止用户代码尝试确定void *指向的确切内容。我理解得对吗? - Lion Lai

45
在Win32编程中,句柄是一个表示由Windows内核管理的资源的标记。句柄可以是窗口、文件等。
使用Win32 API时,句柄只是一种识别要使用的特定资源的方法。
例如,如果您想创建一个窗口并在屏幕上显示它,可以执行以下操作:
// Create the window
HWND hwnd = CreateWindow(...); 
if (!hwnd)
   return; // hwnd not created

// Show the window.
ShowWindow(hwnd, SW_SHOW);

在上面的例子中,HWND表示“窗口句柄”。
如果您习惯于面向对象的语言,可以将HANDLE视为一个没有方法的类实例,其状态仅可由其他函数修改。在这种情况下,ShowWindow函数修改窗口HANDLE的状态。
有关更多信息,请参见处理和数据类型

通过 HANDLE ADT 引用的对象由内核管理。另一方面,您提到的其他句柄类型 (HWND 等) 是用户对象。这些对象不由 Windows 内核管理。 - IInspectable
1
@IInspectable 猜测这些是由 User32.dll 处理的东西? - the_endian

14
一个“句柄”是Windows管理的对象的唯一标识符。它类似于指针,但不像指针那样是可被用户代码解引用以获取访问某些数据的地址。相反,“句柄”应该传递给一组能够对句柄所标识的对象执行操作的函数。

6

所以从最基本的层面来说,HANDLE是指向指针的指针或

#define HANDLE void **

现在让我们来谈谈为什么你需要使用它。

我们来看一个设置示例:

class Object{
   int Value;
}

class LargeObj{

   char * val;
   LargeObj()
   {
      val = malloc(2048 * 1000);
   }

}

void foo(Object bar){
    LargeObj lo = new LargeObj();
    bar.Value++;
}

void main()
{
   Object obj = new Object();
   obj.val = 1;
   foo(obj);
   printf("%d", obj.val);
}

由于 obj 是按值传递的(复制并将其传递给函数),因此 printf 将打印原始值 1。

现在,如果我们更新 foo 为:

void foo(Object * bar)
{
    LargeObj lo = new LargeObj();
    bar->val++;
}

有可能printf将打印出更新后的2的值,但是foo也有可能导致某种形式的内存损坏或异常。

原因在于,虽然你现在使用指针将obj传递给函数,但你还分配了2兆字节的内存,这可能会导致操作系统移动内存并更新obj的位置。由于你通过值传递了指针,如果obj被移动,那么操作系统会更新指针但不更新函数中的副本,可能导致问题。

最终对foo的更新:

void foo(Object **bar){
    LargeObj lo = LargeObj();
    Object * b = &bar;
    b->val++;
}

这将始终打印更新后的值。

当编译器为指针分配内存时,它将其标记为不可移动的,因此由于分配给函数的大对象引起的任何内存重排都会指向正确的地址,以查找要更新的最终内存位置。

任何特定类型的HANDLE(hWnd,FILE等)都是领域特定的,并指向某种类型的结构以防止内存损坏。


2
这是错误的推理;C内存分配子系统不能随意使指针失效。否则,任何C或C++程序都无法被证明是正确的;更糟糕的是,任何足够复杂的程序都会因定义而被证明是不正确的。此外,如果直接指向的内存在程序下面移动,双重间接寻址也无济于事,除非指针本身实际上是来自真实内存的抽象——这将使它成为一个句柄 - Lawrence Dol
1
Macintosh操作系统(版本为9或8)正是如上所述。如果您分配了一些系统对象,通常会得到一个句柄,使操作系统可以自由移动该对象。对于最初的Mac内存大小有限,这是相当重要的。 - Rhialto supports Monica

6
一个 handle 就像数据库记录中的主键值。
编辑1:好吧,为什么要给我点踩呢?一个主键唯一标识了数据库记录,而在 Windows 系统中,一个 handle 唯一标识了窗口、已打开的文件等等。这就是我的意思。

1
我不认为你可以断言句柄是唯一的。它可能在用户的Windows Station中是唯一的,但如果有多个用户同时访问同一系统,则不能保证其唯一性。也就是说,多个用户可能会得到一个数值上相同但在用户的Windows Station上映射到不同事物的句柄值... - Nick
2
@nick 在给定的上下文中是唯一的。主键在不同的表之间也不会是唯一的... - Benny Mackney

5

把Windows窗口想象成一个描述它的结构体。这个结构体是Windows的内部组成部分,您不需要知道其详细信息。相反,Windows为该结构体提供了指向结构体的指针的typedef。这就是您可以获取窗口的“句柄”的方式。


1
是的,但需要记住的是,句柄通常不是一个内存地址,用户代码不应该对其进行解引用操作。 - sharptooth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接