问题来源:
以前碰到的问题---程序中需要实现一组全局的窗口对象,并且要求是动态生成的,并能根据需要删除、增加、查找、修改。我用到了基于模板的链表CTypedPtrList(不知道怎么回事,我特别喜欢用这个)。我声明如下:
CTypedPtrList theFlagButton;
每当在堆中建立一个CFlagButton对象后,就将这个堆的指针用theFlagButton->AddTail()添加到链表的尾部。当程序运行一段时间后,当我用这个指针去取这个窗口对象时,偶尔会发生错误。当时我的理解是程序上有其它的逻辑上的错误。但当我仔细设置断点调试后发现情况比我想象中要糟糕得多---居然指针所指的对象没有句柄存在,也就说指针错误了。
查阅相关资料后才得知:只有句柄才是窗口对象的唯一标识。
那么究竟什么是句柄?为什么要使用句柄?WINDOWS是如何管理这些句柄的? 这要从WINDOWS的内存管理说起,WINDOWS是多任务多用户的操作系统,操作系统要对进程进行统一的调度,其中最重要的就是内存上的管理,WINDOWS下每个进程运行在自己独立的虚拟地址空间。我们来回忆一下WIN32进程和线程的概念:
一个进程通常定义为程序的一个实例。在Win32中, 进程占据4GB的地址空间。与它们在MS-DOS和16位Windows操作系统中不同, Win32进程是没有活力的。这就是说,一个Win32进程并不执行什么指令,它只是占据着4GB的地址空间,此35空间中有应用程序EXE文件的 代码和数据。EXE需要的任意DLL也将它们的代码和数据装入到进程的地址空间。除了地址空间,进程还占有某些资源,比如文件、动态内存分配和线程。当进程终止时,在它生命期中创建的各种资源将被清除。
但是进程是没有活力的,它只是一个静态的概念。为了让进程完成一些工作,进程必须至少占有一个线程,所以线程是描述进程内的执行,正是线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可以包含几个线程, 它们可以同时执行进程的地址空间中的代码。为了做到这一点,每个线程有自己的一组CPU寄存器和堆栈。
扯远了,呵呵。。。
问题就是我们电脑有很多的进程,但我们的内存却是很有限的,WINDOWS采用了虚拟内存这种管理方式,使得能更有效的使用这些内存,而且,WINDOWS的内存管理模块还经常地偷偷地把某些内存对象从一处移动到另一处。
应用程序启动后,所有的窗口对象加载到地址空间中去,一部分到了占据物理内存,一部分占据虚拟内存(由WINDOWS内存管理自动管理),里面存在两种类型的变量,一种是WINDOWSD对象,一种是普通的对象,对于WINDOWS对象,就存在一个HANDLE(句柄)与之对应。那么什么是句柄呢?
句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。每个对象都有一个内存地址,似乎我们只要获知这个地址,就可以随时用这个地址访问对象。但是,如果您真的这样认为,那么您就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,依此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?
为了解决这个问题,Windows操作系统为各应用程序腾出一些内存储地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的。Windows内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配给的,当系统卸载时(Unload)又释放给系统。
句柄地址(稳定)-→记载着WIN32对象在内存中的地址-→对象在内存中的地址(不稳定)→实际对象
知道了这一点后,实际的软件设计中,我不再保存指针,而是保存句柄,然后通过句柄来操作对象。改进之后,程序一切正常了。
CList theFlagButton;
说明:CList 和 CTypedPtrList在使用上是有区别的,CList成员得到的对象都需要手动转换,而CTypedPtrList是不需要手动转换的。因为内部已经进行了安全类型转换,能保证类型安全。
句柄和指针的相互转换:
CButton button;
HWND hwnd = button.m_hwnd;
CWnd *pWnd = CButton::FromHandle(hWnd);
需要主要的是获得的指针不能长久保存,只能临时用来对对象进行操作。