# 全局对象的构造

C++ 的对象在创建时,会调用构造函数。
而全局对象的构造时机,自然应当在入口函数`main/WinMain`被调用之前,否则我们也就无法在入口函数中使用全局对象了。

这里以上节编写的示例代码为例,我们在代码中实例化了一个全局对象 g_theApp ,基于 VS 强大的源码调试能力,我们来对 MFC 程序的启动机制一探究竟。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <afxwin.h>
class CMyFrameWnd :public CFrameWnd {

};


class CMyWinApp :public CWinApp {
public:
CMyWinApp() {

}

virtual BOOL InitInstance() {
CMyFrameWnd* frame = new CMyFrameWnd;
frame->Create(nullptr, L"MFCBase");
m_pMainWnd = frame;
frame->ShowWindow(SW_SHOW);
frame->UpdateWindow();
return TRUE;
}
};

CMyWinApp g_theApp;

# g_theApp 构造调试

  1. 让光标停留在 CMyWinApp g_theApp; 行,按下 F9 ,设置断点
  2. F5 运行,让程序中断到当前行
  3. F11 单步步入,进入到 CMyWinApp 的构造函数中
  4. 继续单步步入,进入到基类 CWinApp 的构造函数中

到这里,我们就开始调试到 MFC 的源码了。

# g_theApp 构造分析

接下来我们选择部分代码进行讲解

CWinApp::CWinApp 部分代码一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CWinApp::CWinApp(LPCTSTR lpszAppName)
{
if (lpszAppName != NULL)
m_pszAppName = _tcsdup(lpszAppName);
else
m_pszAppName = NULL;

// initialize CWinThread state
AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();
ENSURE(pModuleState);
AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;
ENSURE(pThreadState);
ASSERT(AfxGetThread() == NULL);
pThreadState->m_pCurrentWinThread = this;
ASSERT(AfxGetThread() == this);
m_hThread = ::GetCurrentThread();
m_nThreadID = ::GetCurrentThreadId();
......

AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();

MFC 类库中有一个描述主模块状态的全局对象, _AFX_CMDTARGET_GETSTATE 宏函数就是用于获取该全局对象的地址

AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;

以及描述主模块线程状态的全局对象,其地址保存在主模块状态中的成员中。

MFC 在设计时想必已经安排好了构造顺序已确保程序运行的正确性,此处我们的全局对象 g_theApp 的父类部分 CWinApp 在构造时才能够正确使用这些全局对象,这里不再做深究。

pThreadState->m_pCurrentWinThread = this;

将我们创建的 g_theApp 的地址保存到主模块线程状态m_pCurrentWinThread 成员中。

CWinApp::CWinApp 部分代码二
1
2
3
4
5
// initialize CWinApp state
ASSERT(afxCurrentWinApp == NULL); // only one CWinApp object please
pModuleState->m_pCurrentWinApp = this;
ASSERT(AfxGetApp() == this);
......

pModuleState->m_pCurrentWinApp = this;

将我们创建的 g_theApp 的地址保存到主模块状态m_pCurrentWinApp 成员中。


# WinMain 的启动流程

回忆我们的 CMyWinApp 类,在类中我们重写了虚函数 InitInstance
见名知意,我们猜测,这是一个初始化函数,但是我们不清楚函数是何时、如何被调用,因此我们需要继续分析 MFC 的源码。

# InitInstance 回调调试

  1. CMyFrameWnd* frame = new CMyFrameWnd; 行设置断点
  2. 运行程序,中断在此行
  3. 查看 调用堆栈 ,我们可以看到 InitInstance 的调用函数,以及调用函数的调用函数… 等层级关系。
    • 在这里我们也看到了我们熟悉的 WinMain 函数,说明 InitInstance 是在 WinMain 执行过程中被调用的。
    • 而我们并没有实现 WinMain ,那么 WinMain 自然也是由 MFC 实现的,至此,我们先前的一个疑惑也解决了。
  4. 调用堆栈 中定位到 WinMain 函数,在此处设置断点,重新运行程序。
  5. WinMain 函数中仅有一行代码,即调用 AfxWinMain 并返回,单步进入

# AfxWinMain 源码分析

我们依旧选择我们感兴趣的源码进行讲解

AfxWinMain 是 MFC 实现的全局函数。

Afx 开头的函数,基本上都是 MFC 实现的全局函数。

AfxWinMain 部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);

int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();

// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;

// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;

// Perform specific initializations
if (!pThread->InitInstance())
{
if (pThread->m_pMainWnd != NULL)
{
TRACE(traceAppMsg, 0, "Warning: Destroying non-NULL m_pMainWnd\n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();
......

CWinThread* pThread = AfxGetThread();

此函数获取主模块线程状态m_pCurrentWinThread 成员。
先前我们在构造 g_theApp 的过程中已经看到,主模块线程状态m_pCurrentWinThread 成员保存的是 g_theApp 的地址;
因此,此函数实际上是获取 g_theApp 的地址,也就是说,在 MFC 的启动流程中,可能要使用我们创建的全局对象 g_theApp

CWinApp* pApp = AfxGetApp();

此函数获取主模块状态m_pCurrentWinApp 成员。
先前我们在构造 g_theApp 的过程中已经看到,主模块状态m_pCurrentWinApp 成员保存的是 g_theApp 的地址;
与 AfxGetThread 作用类似。

CWinApp* 指向 CMyWinApp 类型的对象,我们是能够理解的;
但是为什么 CWinThread* 的赋值也能被允许呢?因为 CWinApp 类就继承自 CWinThread 类。

if (pApp != NULL && !pApp->InitApplication())

当前行实际上调用了 g_theAppInitApplication 函数,我们并没有提供此函数,因此只可能是 g_theApp 的父类部分提供的。
这个函数实际上也是虚函数,我们能够重写它,一般在我们希望做一些应用程序初始化的工作时重写。

if (!pThread->InitInstance())

终于又到了我们熟悉的部分了, InitInstance 就是我们重写的函数,此时 pThread 指向 g_theApp ,这就是 多态 了。

CMyWinApp::InitInstance
1
2
3
4
5
6
7
8
virtual BOOL InitInstance() {
CMyFrameWnd* frame = new CMyFrameWnd;
frame->Create(nullptr, L"MFCBase");
m_pMainWnd = frame;
frame->ShowWindow(SW_SHOW);
frame->UpdateWindow();
return TRUE;
}

我们暂时略过具体代码,最终是通过 return TRUE 返回的;

nReturnCode = pThread->Run();

根据 AfxWinMain 中的代码的逻辑,我们会走到 Run 这个函数。


# 不可或缺的消息循环

在直接使用 Win32API 开发界面程序时,我们都会编写消息循环以阻塞主线程,避免 WinMain 返回后终止进程。
MFC 程序自然也不例外,而 MFC 的消息循环究竟编写在哪里呢?
其实读者只要在调试时步过 nReturnCode = pThread->Run(); ,就会使得程序直接运行起来,不再处于中断状态,因而得知, Run 成员函数封装了消息循环。

# CWinApp::Run 的源码分析

CWinApp::Run
1
2
3
4
5
6
7
8
9
10
int CWinApp::Run()
{
if (m_pMainWnd == NULL && AfxOleGetUserCtrl())
{
// Not launched /Embedding or /Automation, but has no main window!
TRACE(traceAppMsg, 0, "Warning: m_pMainWnd is NULL in CWinApp::Run - quitting application.\n");
AfxPostQuitMessage(0);
}
return CWinThread::Run();
}

if (m_pMainWnd == NULL && AfxOleGetUserCtrl())

我们忽略对 AfxOleGetUserCtrl 函数的调用, m_pMainWnd 是不是有些眼熟?
我们在重写 InitInstance 时,使 m_pMainWnd 指向了 new 出来的 CMyFrameWnd 对象。
而我们在调用 Run 成员函数时,就是以 g_theApp 的身份进行调用的。
因此,在 Run 成员函数中访问 m_pMainWnd ,自然得到我们当时 new 出来的 CMyFrameWnd 对象。

这也就是 g_theApp 全局对象的主窗口。

return CWinThread::Run();

接下来我们调用 CWinApp 的父类 CWinThreadRun 成员函数

CWinThread::Run 第一部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int CWinThread::Run()
{
ASSERT_VALID(this);
_AFX_THREAD_STATE* pState = AfxGetThreadState();

// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;

// acquire and dispatch messages until a WM_QUIT message is received.
for (;;) // 开始消息循环
{

// 如果没有消息(PeekMessage返回0)
// phase1: check to see if we can do idle work
while (bIdle &&
!::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE))
{
// call OnIdle while in bIdle state
if (!OnIdle(lIdleCount++)) // 调用成员虚函数OnIdle,进入空闲处理
bIdle = FALSE; // assume "no idle" state
}
......

代码并不复杂,这里直接在代码中注释,可以自行阅读。

在 MFC 中程序中调用 Win32API 时,通常都会指明调用的是全局作用域下的函数 :: 。

CWinThread::Run 第二部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        ......
// phase2: pump messages while available
do
{
// pump message, but quit on WM_QUIT
if (!PumpMessage())
return ExitInstance();

// reset "no idle" state after pumping "normal" message
//if (IsIdleMessage(&m_msgCur))
if (IsIdleMessage(&(pState->m_msgCur)))
{
bIdle = TRUE;
lIdleCount = 0;
}

} while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)); // 如果连续有消息就继续处理
}
}

if (!PumpMessage())
MFC 在此函数中封装了对 GetMessageTranslateMessageDispatchMessage 函数的调用,读者感兴趣可以自行跟进,这里就不再分析了。

一旦 PumpMessage 返回 FALSE ,就会调用虚成员函数 ExitInstance 并退出消息循环。
PumpMessage 返回 FALSE 的条件即是 GetMessage 获得 WM_QUIT 消息,程序结束。

我们也可以重写 ExitInstance ,在程序结束前做必要的资源释放。

Run 成员函数返回后,也会一路返回到 WinMain ,程序也就退出了。

# 基本流程

  1. 首先,我们在编写 MFC 应用时,需要实例化一个类型为 CWinApp 的全局对象。

    • 如果需要重写初始化等成员虚函数,则需要创建继承自 CWinApp 的子类的对象 (此处命名为 g_theApp )。
  2. g_theApp 被构造时,会使 MFC 定义的全局变量主模块状态主模块线程状态的成员指向 g_theApp 地址。

  3. 程序进入 WinMain 函数,会通过全局变量主模块状态主模块线程状态得到 g_theApp 地址,再以 g_theApp 的身份调用必要的成员虚函数。

    • 初始化
    • 消息循环
    • 退出

至此,我们基本上对 MFC 程序的启动流程有了一个大致的认知。

而窗口的创建、消息的接收处理等部分,我们留到下一篇再叙。