【MFC】探索程序启动机制的实现原理
# 全局对象的构造
C++ 的对象在创建时,会调用构造函数。
而全局对象的构造时机,
这里以上节编写的示例代码为例,我们在代码中实例化了一个全局对象 g_theApp
,基于 VS 强大的源码调试能力,我们来对 MFC 程序的启动机制一探究竟。
示例
1 |
|
# g_theApp
构造调试
- 让光标停留在
CMyWinApp g_theApp;
行,按下F9
,设置断点 -
F5
运行,让程序中断到当前行F11
单步步入,进入到CMyWinApp
的构造函数中- 继续单步步入,进入到基类
CWinApp
的构造函数中
到这里,我们就开始调试到 MFC 的源码了。
# g_theApp
构造分析
接下来我们选择部分代码进行讲解
CWinApp::CWinApp 部分代码一
1 | CWinApp::CWinApp(LPCTSTR lpszAppName) |
MFC 类库中有一个描述主模块状态的全局对象, _AFX_CMDTARGET_GETSTATE
宏函数就是用于获取该全局对象的地址
以及描述主模块线程状态的全局对象,其地址保存在主模块状态中的成员中。
MFC 在设计时想必已经安排好了构造顺序已确保程序运行的正确性,此处我们的全局对象 g_theApp
的父类部分 CWinApp
在构造时才能够正确使用这些全局对象,这里不再做深究。
将我们创建的 g_theApp
的地址保存到主模块线程状态的 m_pCurrentWinThread
成员中。
CWinApp::CWinApp 部分代码二
1 | // initialize CWinApp state |
将我们创建的 g_theApp
的地址保存到主模块状态的 m_pCurrentWinApp
成员中。
# WinMain 的启动流程
回忆我们的 CMyWinApp
类,在类中我们重写了虚函数 InitInstance
。
见名知意,我们猜测,这是一个初始化函数,但是我们不清楚函数是何时、如何被调用,因此我们需要继续分析 MFC 的源码。
# InitInstance
回调调试
- 在
CMyFrameWnd* frame = new CMyFrameWnd;
行设置断点 - 运行程序,中断在此行
- 查看
调用堆栈
,我们可以看到InitInstance
的调用函数,以及调用函数的调用函数… 等层级关系。- 在这里我们也看到了我们熟悉的
WinMain
函数,说明InitInstance
是在WinMain
执行过程中被调用的。 - 而我们并没有实现
WinMain
,那么WinMain
自然也是由 MFC 实现的,至此,我们先前的一个疑惑也解决了。
- 在这里我们也看到了我们熟悉的
- 从
调用堆栈
中定位到WinMain
函数,在此处设置断点,重新运行程序。 -
WinMain
函数中仅有一行代码,即调用AfxWinMain
并返回,单步进入
# AfxWinMain
源码分析
我们依旧选择我们感兴趣的源码进行讲解
AfxWinMain
是 MFC 实现的全局函数。
以 Afx
开头的函数,基本上都是 MFC 实现的全局函数。
AfxWinMain 部分代码
1 | int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, |
此函数获取主模块线程状态的 m_pCurrentWinThread
成员。
先前我们在构造 g_theApp
的过程中已经看到,主模块线程状态的 m_pCurrentWinThread
成员保存的是 g_theApp
的地址;
因此,此函数实际上是获取 g_theApp
的地址,也就是说,在 MFC 的启动流程中,可能要使用我们创建的全局对象 g_theApp
。
此函数获取主模块状态的 m_pCurrentWinApp
成员。
先前我们在构造 g_theApp
的过程中已经看到,主模块状态的 m_pCurrentWinApp
成员保存的是 g_theApp
的地址;
与 AfxGetThread 作用类似。
CWinApp*
指向 CMyWinApp
类型的对象,我们是能够理解的;
但是为什么 CWinThread*
的赋值也能被允许呢?因为 CWinApp
类就继承自 CWinThread
类。
当前行实际上调用了 g_theApp
的 InitApplication
函数,我们并没有提供此函数,因此只可能是 g_theApp
的父类部分提供的。
这个函数实际上也是虚函数,我们能够重写它,一般在我们希望做一些应用程序初始化的工作时重写。
终于又到了我们熟悉的部分了, InitInstance
就是我们重写的函数,此时 pThread
指向 g_theApp
,这就是 多态
了。
CMyWinApp::InitInstance
1 | virtual BOOL InitInstance() { |
我们暂时略过具体代码,最终是通过 return TRUE
返回的;
根据 AfxWinMain
中的代码的逻辑,我们会走到 Run
这个函数。
# 不可或缺的消息循环
在直接使用 Win32API
开发界面程序时,我们都会编写消息循环以阻塞主线程,避免 WinMain
返回后终止进程。
MFC 程序自然也不例外,而 MFC 的消息循环究竟编写在哪里呢?
其实读者只要在调试时步过 nReturnCode = pThread->Run();
,就会使得程序直接运行起来,不再处于中断状态,因而得知, Run
成员函数封装了消息循环。
# CWinApp::Run
的源码分析
CWinApp::Run
1 | int CWinApp::Run() |
我们忽略对 AfxOleGetUserCtrl
函数的调用, m_pMainWnd
是不是有些眼熟?
我们在重写 InitInstance
时,使 m_pMainWnd
指向了 new
出来的 CMyFrameWnd
对象。
而我们在调用 Run
成员函数时,就是以 g_theApp
的身份进行调用的。
因此,在 Run
成员函数中访问 m_pMainWnd
,自然得到我们当时 new
出来的 CMyFrameWnd
对象。
接下来我们调用 CWinApp
的父类 CWinThread
的 Run
成员函数
CWinThread::Run 第一部分
1 | int CWinThread::Run() |
代码并不复杂,这里直接在代码中注释,可以自行阅读。
在 MFC 中程序中调用 Win32API
时,通常都会指明调用的是全局作用域下的函数 :: 。
CWinThread::Run 第二部分
1 | ...... |
if (!PumpMessage())
MFC 在此函数中封装了对 GetMessage
、 TranslateMessage
、 DispatchMessage
函数的调用,读者感兴趣可以自行跟进,这里就不再分析了。
一旦 PumpMessage
返回 FALSE
,就会调用虚成员函数 ExitInstance
并退出消息循环。
而 PumpMessage
返回 FALSE
的条件即是 GetMessage
获得 WM_QUIT
消息,程序结束。
我们也可以重写 ExitInstance
,在程序结束前做必要的资源释放。
从 Run
成员函数返回后,也会一路返回到 WinMain
,程序也就退出了。
# 基本流程
-
首先,我们在编写 MFC 应用时,需要实例化一个类型为
CWinApp
的全局对象。- 如果需要重写初始化等成员虚函数,则需要创建继承自
CWinApp
的子类的对象 (此处命名为g_theApp
)。
- 如果需要重写初始化等成员虚函数,则需要创建继承自
-
在
g_theApp
被构造时,会使 MFC 定义的全局变量主模块状态和主模块线程状态的成员指向g_theApp
地址。 -
程序进入
WinMain
函数,会通过全局变量主模块状态和主模块线程状态得到g_theApp
地址,再以g_theApp
的身份调用必要的成员虚函数。- 初始化
- 消息循环
- 退出
至此,我们基本上对 MFC 程序的启动流程有了一个大致的认知。
而窗口的创建、消息的接收处理等部分,我们留到下一篇再叙。