Windows|说说LocalAlloc和GlobalAlloc的区别

Windows|说说LocalAlloc和GlobalAlloc的区别

他们俩的区别 , 在16位Windows的年代 , 还是挺大的 。
在16位Windows系统中 , 内存是通过段选择子(selectors)来访问的 , 每个段最大可以寻址64KB 。 其中 , 有一个默认的段称之为”数据段” , 一个近指针(near pointers)操作会以距离数据段的偏移来进行操作 。 举个例子 , 如果你有一个近指针p , 它的值是0x1234 , 然后数据段的值为0x012F , 则当你写入数据到*p时 , 你即将访问的地址为012F:1234 。 (当你声明一个指针的时候 , 默认情况下它是近指针 。 如果你想使用一个远指针 , 则需要在声明的时候添加FAR)
重要的是 , 近指针总是相对于段的 , 通常这个段就是上面所说的数据段 。
GlobalAlloc这个函数会分配一个段选择子 , 进而通过它来访问你请求的内存地址 。 (如果你要求访问的地址空间大于64KB , 则将会发生不同的故事 , 当然 , 这不是今天的主题)
你可以使用这个段选择子来声明一个远指针 。 一个远指针包括一个段选择子和一个近指针 。 (请记住 , 一个近指针是相对一个段选择子的偏移 , 当你将一个近指针和一个合适的段选择子组合在一起的时候 , 你就得到了一个远指针了)
每个程序的实例和它依赖的DLL , 都会拥有属于它自己的数据段 , 也就是我们常说的实例句柄 , 关于这个实例句柄 , 你应该记得 , 我在之前的一篇文章中专门讨论过 。 默认的数据段会作为一个应用程序实例的句柄 , 而在一个DLL中的默认数据段会作为这个DLL模块的句柄 。 因此 , 如果你有一个近指针p , 然后在一个应用程序中通过*p访问数据 , 则它会相对程序的实例句柄来作为基础地址 。 如果你从一个DLL中进行地址访问 , 则它会基于DLL模块的句柄作为基础地址 。
通过调用LocalInit函数 , 被默认数据段所引用的内存会转变为一个”本地堆”(Local Heap) 。 初始化这个本地堆通常是一个程序或者DLL在启动加载后需要做的第一件事(对于DLL来说 , 通常这是它需要做的唯一一件事) 。 当完成本地堆的初始化之后 , 你就可以使用LocalAlloc来分配内存了 。 LocalAlloc函数会返回一个相对于默认数据段的近指针 , 所以当你从一个应用程序中调用LocalAlloc时 , 它会从程序的地址空间以程序实例句柄作为基础地址进行分配 。 如果你在一个DLL中调用它 , 则会从DLL中的模块句柄开始进行地址分配 。
聪明的你一定发现了 , 我们可以使用LocalAlloc来分配地址而不是实例句柄 。 我们需要做的 , 只是将默认的段修改为其他通过GlobalAlloc分配的内存地址 , 然后再次使用LocalAlloc , 最后将将段还原为之前的默认段 。 这可以实现以非默认段为基础地址的地址访问 , 这种常见非常少见 , 但是如果你足够小心和谨慎 , 一般不会出现大的问题 。
所以 , 从上面的描述 , 我们可以看到 , 在16位Windows上 , LocalAlloc和GlobalAlloc有着完全不同的行为 。 LocalAlloc会返回一个近指针 , 而GlobalAlloc会返回一个段选择子 。
对于跨模块传输指针的场景 , 这个指针必须是一个远指针 , 因为每个模块都有一个不同的默认段 。 如果你希望将一块内存的访问控制权转移到另一个模块 , 则你需要使用GlobalAlloc , 因为接收者可以使用GlobalFree来释放它(有人可能有些迷糊了 , 接收者并不能使用LocalFree , 因为LocalFree适用于本地堆 , 它释放的将会是接收者自己的堆 , 而不是发送者的) 。
到了Win32的年代 , 本地堆和全局堆之间的区别已经很小了 。 如果你调用一个从16位版本Windows继承而来的函数 , 并使用它来传递内存控制权 , 则它会使用一个HGLOBAL的形式 。 剪贴板相关的API就是这种场景的一个经典例子 。 如果你将一块数据放置到剪贴板 , 它必须使用HGLBAL的形式进行内存分配 , 因为这个时候 , 你是将内存数据传输到剪贴板 , 然后剪贴板会在不需要访问此内存的时候 , 调用GlbalFree来进行内存释放 。 而通过STGMEDIUM来进行数据传输 , 也是基于相同的原因使用HGLOBAL 。