Windows MFC 工程应用开发与框架原理完全剖析教程(中)之下部



第4章 原理篇三 MFC核心框架完全实现

4.1 CWinApp的深入剖析与实现(1)——从CWinThread到CWinApp:CWinApp从CWinThread派生以后就拥有了推进当前应用程序的能力

通过上一讲CWinThread的封装,我们知道了MFC是如何把我们的一个应用程序封装成一个工作者线程的;但是,还有一个问题我们没有回答的,就是WinMain它是怎么被MFC隐藏的,接下来我们要谈WinMain函数的封装,就一定要谈到CWinApp这么一个实现。
CWinThread帮我们做了一件很重要的事情,就是它把一个线程和一个Windows对象的实例句柄进行了关联,封装到了内部去了,这样的意义,对于WinMain的隐藏究竟是怎么实现的呢?
在这里插入图片描述

既然我们现在的CWinApp已经有了控制代码推进,和改变程序状态的能力,接下来的问题是,为什么WinMain函数可以开始从CWinApp这里分离呢?


4.2 WinMain函数被独立出来的原理剖析

为什么我们这一个CWinApp实现了以后,就能够完成WinMain函数的分离呢?就是我们再也看不到WinMain函数了。

Win32应用程序的特征,它泾渭分明的有两个函数:WinMain和窗口回调函数WndProc。

在这里插入图片描述

原来,我们通过CWinApp持有当前这个应用程序的HINSTANCE来推进这个应用程序,进而我们写一个WINMAIN.cpp文件隐藏掉WinMain函数,接下来用户只要从CWinApp进行派生,那我的这样一个框架就能够自然而然的 推进,我就没有必要再把我的WinMain暴露给用户了,这就是MFC框架设计的一个逻辑。


4.3 CWinApp工程建立与CWinThread从工作者线程向UI线程的扩展

接下来我们将完成Windows GUI途径的MFC的封装,这个也是我们MFC的核心功能,这里我们选择的项目和前面有所区别:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

接下来,我们把前面写的一些项目文件拷贝到当前项目目录里面:
在这里插入图片描述

在这里插入图片描述

还要让我们的程序具有动态识别和动态创建的能力:
在这里插入图片描述

在这里插入图片描述

把拷贝过来的所有内容添加到现有项目中来:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

首先,要对我们的_AFXWIN.h的CWinThread类进行扩容,先把头文件_afx.h加进来,把动态器识别的能力给它加进来:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Run函数:线程开始推进;
PumpMessage函数:我们还要能够把我的消息压到我们的这一个对应的窗口回调函数里面去;
OnIdle函数:如果我们这个线程没有其他消息的时候,我们就让这个线程执行到OnIdle函数里面来;
在这里插入图片描述

我们在任务管理器可以看到上图选中的系统空闲进程,我们MFC的这个OnIdle也是这个意思,当我们MFC在没有消息循环的时候,我们就让当前这个线程推进到OnIdle这个过程,使得我们这个线程不会占用CPU的资源;

IsIdleMessage判断下这个pMsg是不是空闲消息;
在这里插入图片描述


4.4 CWinApp的类结构设计

在这里插入图片描述

这里声明了析构函数~CWinApp,是因为我们以后的应用程序都是会从CWinApp派生的,所以子类要能够做些自己处理的内容。

在这里插入图片描述

这里我们就知道了,原来我们用CWinApp在封装WinMain。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

现在这里就有一个问题了,我们之后会对AfxModuleThreadState再做一个扩充,为什么呢,因为这个是我们CWinApp在所继承的CWinThread当中一个非常重要的内容,因为它告诉了你现在这个窗体应用它是有资源的;
我们先在这里把它的逻辑写完,之后再补充它里面相应的数据结构。

在这里插入图片描述

最后我们还得前向声明一个全局函数AfxGetApp:
在这里插入图片描述

这个大家在以后MFC编程当中非常常用,我现在想拿这个应用程序里面的内容,你就可以通过AfxGetApp()获得,这里在我们实现这个框架的过程中,你就可以体会为什么这样做了,因为它这个里面封装了HINSTANCE。

在这里插入图片描述

这个实际上意味着什么事情呢,AfxWinInit这是一个全局的函数,我们可以通过AfxWinInit在WinMain函数当中被调用,也就是说WinMain函数当中调用AfxWinInit,那么它就可以通过WinMain函数当中的HINSTANCE传递进来,并且把HINSTANCE变成CWinApp当中的一个成员变量,接下来我们就可以用了。


4.5 CWinApp的线程模块状态维护数据结构AfxModuleState、AfxModuleThreadState的实现

接下来我们就开始来补充相应的数据结构。
在这里插入图片描述

这个数据结构里面的内容不足以满足我们的需要,我们进行一些补充和完善:
在这里插入图片描述
在这里插入图片描述

m_hCurrentInstanceHandle,没有它的话我这一个CWinApp是没有办法去获得能够被Windows感知的对象的。

在这里插入图片描述

一个是拿我们App当中的Module State,还有一个需要拿我们这一个模块当中的状态。

在这里插入图片描述

CWnd就是我们的那个窗口,我们要把画出来的窗口资源跟它进行一个比对。

在这里插入图片描述

我们用以前讲TLS的那张图来解释下这几个类要做的事情:
在这里插入图片描述

MFC是一个框架,我们用AFX打头来表明这是一个Application FrameWork的缩写。

我们设计这个AFX_MODULE_STATE是什么意思呢?
就是告诉你,这个是我们MFC为了管理你推起来的这个应用程序而设计的一个数据结构,我们说过当我们MFC把我们一个程序代码变成一个线程进行推进的时候,它可能会对应上图右边这几个槽位中的若干个Windows的对象,因为只有Windows对象才能够被Windows操作系统感知,那么在上图右边2和4这几个槽位中我们拿出来的这一个个东西称之为Module;

那么我们的AFX_MODULE_STATE它指的是什么呢,它指的就是我们想拉起来的这一个个Module当中的状态,换句话说,当我这样一个MFC框架把我们这一个线程拉起来了以后,我如果想在这个程序的运行过程当中获得上图右边这几个槽位中的Windows对象的状态,我只能借助MFC框架,所以就用AfxGetModuleState就把这几个槽位里面对应的Module拿出来了,拿出来的这个Module就拿到了Windows对应的数据结构,那么Windows给的这一个HINSTANCE句柄,我们通过Windows这个句柄对它进行状态的刻画,比如说让它前进啊,Windows当中的一些特征啊,我们想要做的一些响应参数啊,我们都可以通过它来获取,所以我们就要设计AFX_MODULE_STATE这样一个数据结构来获得它的状态,来控制它的状态,进而帮助用户(这个用户指的是使用MFC框架的程序)来体现出我现在这个应用程序究竟是运行的Windows对象到底到了什么状态。

AFX_MODULE_STATE的意义解读:
我们的这一个AFX_MODULE_STATE,它想干的活是什么事情呢,它想干的活就是想把上图槽里面的这个Windows对象封装成一个可以被MFC管理的内容,我们为了让MFC便于管理,我们设计了AFX_MODULE_STATE这样一个数据结构来刻画它。

我们得获得当前这个线程的状态,所以我们用THREAD_LOCAL宏保存这个线程的特定状态:
在这里插入图片描述

在这里插入图片描述

你的这一个钩子句柄是什么意思?
这个CWnd我们看上去是一个C++的Windows对象,但是这个CWnd对象的建立没有我们想象的那么简单,不是简简单单的把一个句柄给它放上去就OK了,我们需要做一个件事情:
首先,m_pWndInit这是一个指针,我要完成指针和指针的映射;
第二个,我的这个CWnd是个C++对象,你要知道C++对象在起来之前,需要用C++运行时的,而很有可能在我这个C++对象构造之前啊,我的Windows已经开始对这个应用程序对象进行构建了,所以我们得加一个控制层,在我的C++对象能够建立之前,我就能够捕获这个Windows对象,这种操作我们就用的Windows另外一个技术,叫钩子技术,也就是说我在这个Windows消息发送之前,我已经截获你了,使得我的MFC框架有控制你的能力。

接下来我们来完善引入消息循环里的AFX_MODULE_THREAD_STATE。
有过MFC使用经验的就会知道,我们在MFC程序里面是可以直接调用AfxGetThreadState函数,就可以拿到这个线程里面的内容;
我们就来模仿实现它:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


4.6 MFC框架是如何接管应用程序的生与死,即WinMain函数的隐藏于CWinApp对象协作关系的实现

在这里插入图片描述

其实AfxGetModuleState就是向操作系统要这一个Windows对象的所有权。

在这里插入图片描述

这个地方就是我们经常讲的,就是说我们的WinMain函数是程序的入口点,换句话说,当我们的MFC没有足够的机会去获得这个Windows对象的时候啊,我们得让我们这样一个MFC推进应用程序的过程稍等一下,所以在我构造函数之前是不能直接去获得你这个应用程序的句柄的;
什么意思啊,就是说我现在把这个句柄m_hInstance置为空,只有当WinMain开始起跳了以后,Windows操作系统才能为当前的应用程序分配HINSTANCE;那这个和你这里置空有什么关联呢?
你不要忘了,我们有一个CWinApp theApp,这个theApp是C++运行时的对象,这个全局的类的变量它是先于WinMain函数被构建的,所以这个时候我们只能把我们想要指向的进程实例句柄空在那里,等WinMain进入了以后再把它设回来,这个很重要。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

因为一般情况下我们退出的时候啊,实际上它是想向Windows的当前消息循环推一条WM_QUIT消息,所以这个时候我们在这里重置了MSG.wParam这个分量,然后我们会往我们的WndProc里面推进。

至此为止,我们的CWinApp的构建就大体结束了,有了这个做基础,我们就开始进行WinMain的隐藏了,我们肯定不能在类方法里面做这个事情,那么我们怎么办呢?
我们还得有一个全局的函数,让我们的WinMain起跳了以后去调这个函数,从而帮助我们完成这个CWinApp里面的相应实现。

新建一个文件APPINIT.CPP:
在这里插入图片描述

首先,我们把它推到我们的线程局部存储管理里面。

在这里插入图片描述

接下来我们就可以正式把我们的WinMain隐藏起来了。
首先要给我们这个MFC框架有机会获得推进这个应用程序的能力:
在这里插入图片描述

在这里插入图片描述

然后初始化MFC类库框架,再进行应用程序的全局初始化:
在这里插入图片描述

在这里插入图片描述

我们做的这一切,都是当我这个WinMain被Windows推进了以后,接下来你这个应用程序的所谓的生和死都由我们的MFC类库来管理。

在这里插入图片描述


4.7 从CWinThread到CWinApp单元测试与CWinApp类设计完善

在这里插入图片描述

在这里插入图片描述

我们现在没有消息循环,所以我们现在就想看如果调了InitInstance是不是就可以进入主线程了。
这个函数最后返回FALSE,因为我们没写消息循环,写了现在也不能进,因为我们还没有构建完毕。

在这里插入图片描述

重新生成,我们发现有很多语法错误,我们逐个进行调试解决。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

接着就是链接错误了,这是非常常见的BUG,说明你这个是有声明没实现,这都是和我们CWinThread实现相关的。

在这里插入图片描述

在这里插入图片描述

可以看到VS的提示,上图这些函数你都没实现,这可能是我们写程序的时候遗漏了,我们把它们补充起来就可以了。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

补充完成后,再让编译器帮我们检查下有没有语法错误:
在这里插入图片描述

这个是说没有CWinThread的无参构造函数的实现,我们给它补充进来:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个就是说AfxGetApp没有实现,我们再去把它补充实现掉:
在这里插入图片描述

生成成功没有语法错误后,我们来看看主线程有没有被推进:
在这里插入图片描述

这说明我们的逻辑有问题,这个时候我们就来看看逻辑哪边有问题,再进行调试。


4.8 从CWinThread到CWinApp 调试技巧演示

我们来看针对上面的问题怎么调试。
在这里插入图片描述

我们在WinMain函数中打个断点后运行一下,发现还是有这个问题:
在这里插入图片描述

还是有这个问题,说明我们压根WinMain都没进那我们就来看了,WinMain都没进根据我们C++的知识,很有可能它在构造这个全局的CMyApp theApp的时候有问题,那我们在CMyApp theApp这里打个断点,我们知道,CMyApp theApp这里肯定是进CWinApp的,CWinApp肯定是进CWinThread里的。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个时候应用程序没有崩,我们F5进到CWinThread无参构造函数中,进到这里也没有崩,我们继续单步进入CWinApp::CWinApp()里面:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

还是崩了,我们去掉前面问题的断点,直接在下图那里打上断点:
在这里插入图片描述

在这里插入图片描述

也就是下断点那里从第4行走到第5行的时候,程序开始崩了,我们继续再走一下,启动一下调试器:
在这里插入图片描述

很有可能是我们AfxGetModuleState这里出的问题,我们转到该函数定义处:
在这里插入图片描述

在这里插入图片描述

我们一步步单步跟进,分配槽号,获得里面的内容,和我们想的一样。
在这里插入图片描述

好像没有问题,目前一下子没有看到里面的缺陷在什么位置,我们把断点改下位置:
在这里插入图片描述

在这里插入图片描述

我们继续往下走,当走到上图这里的时候,我们发现业务逻辑好像有点问题了,我们的pRes是想拿到当前模块的状态,上图执行的位置那里,pState->m_pModuleState这个时候肯定是空的,我们在28行又调了一遍AfxGetModuleState,这说明是不是出现了递归调用啊,递归调用肯定会出现栈溢出,所以这个地方实际上是我们逻辑有问题了;
我们这个地方如果是一开始还没有分配的时候,这个时候28行这里拿当前的Module State,它肯定拿不到,那这个时候它应该怎么办呢?
它应该拿的App的状态:
在这里插入图片描述

在这里插入图片描述

我们把上图的断点先禁用一下,运行一下程序看看有没有问题。
在这里插入图片描述

这个时候跳到WinMain函数中了,说明这个时候它开始推进了:
在这里插入图片描述

在这里插入图片描述

这个再度证明了我们的这个CMyApp theApp全局变量构建在我们的WinMain函数之前,并且我们已经把WinMain获得的HINSTANCE放到了我们的CWinApp里面去,这是一个非常有价值的事情。

基于此,我们的CWinThread和CWinApp的封装也就基本完成了。


4.9 从CWinThread到CWinApp——MFC框架是MFC应用程序的基石

在这里插入图片描述

我们来回顾总结下从CWinThread到CWinApp里面做的事情,这个代码量比较庞大,我们把这个逻辑帮你梳理了以后,帮助大家有利于从宏观上把握这样一个框架。

这个问题的由来,是希望隐藏WinMain,换句话说,当我们把这个MFC给用户了以后,这个用户就不再需要些WinMain了,只要按照我的MFC规约开始写程序就OK了。
我们来看看它是什么意思:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们在程序当中用函数组织业务逻辑,在我们操作系统当中我们就是用线程来组织进程的推进,这个观点非常重要。
线程与线程的交互构成了进程的业务逻辑呈现!

最后我们总结,我们任何的代码都要在线程当中执行!因此,我们在MFC当中才有一个所谓的叫CWinThread类的概念,CWinThread讲白了就是我们要能够用C++的语法呈现出线程对象的概念。
好,在这里我们总结出,我们CWinThread类的目标是什么:
我们CWinThread类的目标,从代码上讲是封装WinMain函数,从我们的面向对象角度来讲,它是要生成线程对象!

在这里插入图片描述

在这里插入图片描述

(1)我们开一个线程,就是推进一个指令,我们推进一个指令,就是要用一个线程块来执行,那么这么多的指令序列它被我们封装成了一个一个的线程对象,那么这一个一个的线程对象怎么串联;
(2)线程当中我们说过它有自己的私有数据,因为每个线程都要维护自己的一个内容,从我们代码层面来讲,就是说我们的线程就是一个函数,函数有自己的局部变量,这些局部变量是要能够自己完成的,同时这些私有变量要能够被统一指挥。

对于第一个问题,我们很自然的想到,这个线程它是有可能增加的,也有可能删除的,我们也没有办法给你预先分配多少空间,那么设计一个链表就解决了,把这些线程对象用线程对象的指针就可以串联起来;
那么第二个问题,我们就必须要讲了,线程是私有数据,私有数据是每个线程特有的,别人看不到,如果你想要看到,那么一定得有一个统一的“大管家”,这个“大管家”肯定不是你进程本身,那这个“大管家”是谁呢?一定是我们的Windows系统,所以我们再回头向Windows系统请教,你能不能给我们一个统一管理的东西呢,Windows系统给我们提供了一个非常好用的机制,叫TLS线程局部存储。

在这里插入图片描述

线程局部存储实际上是Windows操作系统本身提供的一种机制,它能够很方便的管理一个进程空间当中若干个线程当中的私有数据;
TLS是Windows的原生机制,它可以很方便的在一个进程空间之内,管理这些线程的私有数据;这个非常有价值。

我们通过自己去管理线程当中的私有数据,MFC就具有了这样的能力,它能够为每个线程去关联和它相关的Windows对象(如上图右上角那几个槽里面的对象),并且让每个Windows对象为它当前自己的线程服务,那有了这样的能力,MFC就有信心去感知应用程序,去推进应用程序,并且很重要的就是,能够让这个MFC生成的程序顺利的和其他的Windows对象进行交互,否则这些交互还得由这个程序的开发人员来操心。
有了MFC,它直接就帮你做好了,你只要去调它的相应的Afx的函数,获得它的指针,获得它的句柄,就可以完成业务逻辑的开发了,这个就是MFC非常重要的机制。

在这里插入图片描述

上图那个菱形横空出世、贯穿整个Win32程序代码的就是MFC,这个MFC干了一件非常了不起的事情,它说我的这个程序FstWnd.exe,它不再是经由你自己写WinMain和WndProc了,你首先交给我这个MFC框架,我MFC再把它交给OS来推进,这样带来的好处就是,MFC有了这样一个能力去推进我们写的这样一个GUI的用户程序FstWnd.exe,这个就更有意思了,我MFC怎么能够推进你的FstWnd.exe这样一个应用程序呢?
我们从代码上面来看,如果一个程序需要推进,必然就是一行一行代码的执行,以前这个执行是直接由操作系统加载一个程序到内存中成为一个进程,然后开始一条一条的执行,现在我告诉你,首先你的应用程序的代码行(也就是我们讲的指令序列),先交给MFC,由MFC这个框架来推进你的指令序列。
MFC也是一个程序,它怎么能够推进你的指令序列呢,很简单,我们将这些指令序列组合成函数,然后我们将这些函数视为某一个线程,这不就行了么,我们知道线程是能够推进的,线程能推进那么我就能够推进函数了,那同时我们需要知道线程的状态,和线程相关的数据,我们就要设计一系列的数据结构,这一系列结构就是下图中的函数所获得的数据结构:
在这里插入图片描述

AppModuleState是我们APP当中的模块状态;
ModuleState是这一个模块的状态,是和我们当前线程相对应的;
ModuleThreadState,我们的这一个模块和我的线程也有状态关系;
ThreadState,线程本身的状态;

这一系列的内容就是帮我们人为设计出来的,对这种东西的理解就能够知道了,我们MFC是如何控制这个程序运行的。

接下来,我们用一个例子(Win32控制台应用程序+MFC类库)来帮大家理解:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们修改上图项目属性为下图:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

如果没有MFC的话,我们得调用Windows操作系统提供的API就可以获得当前的线程。

在这里插入图片描述

但是我们现在有了MFC,我们这个应用程序它就有了这样一个能力:
FstWnd.exe这个应用程序的推进不再是经由它自己直接向操作系统要求,MFC就类似于中间的连接点,它来控制你的行为,然后它来代你去向Windows交互,并且能够帮你完成一些预定义的东西。
那如何来体现这些问题呢,我们来看一看我们自己封装的这些东西在MFC当中怎么用,做一个简单的例子让大家理解。

在这里插入图片描述

你加了MFC这个框架以后,这个MFC它就自动获取了一种感知你当前应用程序的能力,同时它也能够当你需要向操作系统打交道的时候,你不是直接去要了,而是先向MFC这个框架去要。

在这里插入图片描述

这个错误是告诉你当前这个ThreadID是没有被分配的,我们修改代码:
在这里插入图片描述

在这里插入图片描述

这里只是为了简单说明原理。

那我如果现在想要拿一个线程,那很简单,我们这么做:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个新创建的线程ID的16288也拿到了,这样使得我们MFC具有感知我们这一个应用程序的能力了。

在这里插入图片描述

上图这些MFC的函数都是告诉你我可以获得里面的哪些参数和哪些分量,这样可以使得MFC可以灵活控制我们的程序,在我们的控制台应用程序里面感知的不明显,如果是我们GUI程序,我们感知的会非常非常明显。

比如说我现在想要获得当前应用程序的实例,我们可以这么干:
在这里插入图片描述

我们可以看到它有这一系列的方法和成员数据,它有m_hInstance,也有m_hThread,这些都是当前的应用程序所需要的内容,我如果是一个Win32的应用程序,我现在想要获得应用程序的实例句柄,我就能够去访问它想要的内容,比如说我拿到句柄,我就可以去调Win32的API,然后去访问它想要做的事情,比如说我想画个图,我想画个控件,我想改变它里面的状态,这一系列都是MFC帮你封装好了的,让你不再需要操心它里面的技术细节,你只需要问MFC要就可以了。
有了这样一个MFC核心框架做支撑,我们就能够把我们整个应用程序撑起来,并且能够代替我们的OS,换句话说,我们可以把OS想象成是个空的,我要的东西都可以问MFC框架来要。

在这里插入图片描述

没有上图这些东西,MFC是没有办法帮我们管理应用程序的,所以它就自己发明了这些数据结构,所以我们对这一系列数据结构,都是映射成一条一条可以管理的东西和管理的对象,因此我们的MFC才有了推进的能力。
我们就能够很清楚的知道,我们用我们的CWinApp究竟帮我们做了什么事情,有了这样一个技术之后,它帮我们获得了掌控程序,控制程序,代替我们向操作系统打交道的能力;
这个就是我们对CWinThread到CWinApp的非常重要的总结,也希望能够帮大家掌握MFC这个框架的精髓的基础CWinApp的设计。


4.10 CWnd的深入剖析与实现(1)——从HWND窗口句柄到CWnd的C++对象:C++内存对象的构建与CPlex类设计

在这里插入图片描述

前面我们说过,这个CWinApp相当于我们应用程序本身,操作系统通过CWinApp这个C++对象来感知我们这一个应用程序的存亡。
同样的,我们通过CWnd这个C++对象,来对应我们Windows当中的窗口。
那接下来,我们就要想办法,我们如何通过C++的语法去封装Windows中的句柄。
句柄实际上是内存当中的一种指针,那同样的我们C++的对象,比如说我们产生的一个CWnd对象,这个对象应该也是一个指针,这个时候我们必然希望我们的框架能够完成一种指针到指针的映射关系,我们通过管理我们这个MFC应用程序的内存分布,去管理内存当中的这种映射关系,从而完成应用程序的一种封装,这种封装就是把一个纯粹的Win32的一个指针变成指针和它的数据操作的手段,也就是函数这样一个内容。

接下来,我们为了上图辅助这一系列对象的生成,我们得做一个辅助的工作,就是我们要封装一个对象的映射关系。

在这里插入图片描述

C++封装的工程意义:
在这里插入图片描述

在我们这个应用程序当中啊,这一个个的对象,比如obj、app和window对象,这些对象和对象之间的交互完成了业务逻辑,并不是类的交互来完成逻辑,是对象和对象之间的交互完成的逻辑;
进而我们回头来考察下,上图圆圈中的几个对象是由谁创建出来的,简单地从语法上来讲就是CObject这个类new出来的,CWinApp类new出来的,CWnd类new出来的,这个是在我们C++概念当中有的,C语言是没有这种所谓的对象的概念,那么我们现在要把我们这一个C程序,比如说我们这个句柄,我们窗口回调这个函数,我们想把它变成一个C++的对象来供我们的MFC这一个框架来感知,那么必然的就要做件什么事情呢,必然的就是说我们要刻画并且规划好我们这一个MFC应用程序当中的内存布局!

在这里插入图片描述

这个就是我们讲的MFC当中最精髓的一个地方,我们通过规划内存布局,把C的函数、把C的句柄、把C的指针,拼装成一个C++对象。

在这里插入图片描述

如上图所示,我们把前面一个内存块和后面一个内存块用链表给它串起来,串起来的目的是我们把pHead塞给MFC框架,MFC框架就能够管理这样的内存,从而管理C++对象指针和窗口句柄之间的映射关系。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


4.11 CWnd的深入剖析与实现(2)——从HWND窗口句柄到CWnd的C++对象:CWnd映射辅助工具类CMapPtrToPtr的实现

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

为什么要用哈希呢?
我举个简单的例子,我这个程序当中有很多CAssoc,如果我维护的这个程序当中的窗口句柄和对应的C++对象指针有足够多的情况下,我如果用暴力穷举去做的话,会带来一个非常重要的问题,时间响应很糟糕,我为了找对应的C++对象,封装成C++对象以后使得我整个的应用程序的响应变得很差,因为我不知道要把这样的消息路由给哪个窗口句柄;
我最希望的是,我有了这样的消息以后直接就可以路由,几乎没有性能损耗,那这个时候我找窗口句柄的速度直接反映出了我的这个MFC程序的性能,所以MFC框架的设计者就考虑到这一点就用了哈希。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(上图标有红线的71行代码有错误,应改为return m_nHashTableSize

我们写了这么多代码,我们先不着急去实现它的细节,肯定有很多人在刚刚碰到这种复杂一点的框架的时候,会有一些困惑,我们学框架首先第一个得牢牢去关注的特点,并不是它的语法实现细节,而是在考虑它的框架和脉络,描绘出这个事情究竟在干什么,你才能够比较好的理解它的细节。

我们画张图来看一下:
在这里插入图片描述

我们MFC通过感知CAssoc去感知HWND向CWnd的映射,这就是CMapPtrToPtr类所要做的一个事情。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

接下来讲解下如何使用哈希技术加快查找的原理。

在这里插入图片描述

最好的方法是用空间换时间。
最好的方式是不进行任何查找就能够直接得到用户项。

在这里插入图片描述

在这里插入图片描述

上图有的哈希值可能会有多个连接。

接下来,我们根据上图的思想,利用哈希方法来完成映射排列的功能。

在这里插入图片描述

(上图的初始化少了m_pFreeList = NULL;这句代码)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

如果Lookup函数返回FALSE,说明我们拿不到窗口句柄向C++对象的映射,否则的话,根据这个key就能拿到对应的CAssoc结构,就能拿到CWnd对象指针。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

删除一个节点的话,如果是链表那我得把链表串起来,得有一个结构先把这个节点保存起来,所以我们得有一个指向指针的指针,去存我们链表的前继节点。

在这里插入图片描述

ppAssocPre就是命中节点前面节点的pNext成员的地址,*ppAssocPre就是pNext成员的值。

在这里插入图片描述

在这里插入图片描述


4.12 CWnd的深入剖析与实现(3)——从HWND窗口句柄到CWnd的C++对象:CWnd映射辅助工具类CMapPtrToPtr的单元测试

在这里插入图片描述

接下来我们要对CMapPtrToPtr类做一个单元测试。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

有错误,说明我们的程序有问题,我们来调试找一下。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们发现m_pFreeList的值不是NULL,而是0xcccccccc,接下往下走:

在这里插入图片描述

在这里插入图片描述

走到这里就要崩溃了,因为0xcccccccc这个空间根本是不可达的空间,所以在121行这里引用了m_pFreeList->pNext,程序就会崩溃。

在这里插入图片描述

说明我们访问了一个不可达的指针地址,这是我们编程当中一个非常非常重要的隐患问题,如果不对我们的指针变量做初值准备的话,在我们应用程序当中会出现一个默认的或者任意的值,在Debug中都是0xcccccccc,这个是会造成程序潜在风险的。
修改代码如下:
在这里插入图片描述

在这里插入图片描述

这个就说明了在编程的时候要特别要注意对函数指针初始化的问题,这个东西会引起我们程序潜在崩溃的,我们对任意的指针在使用前都要赋初值。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值