MFC中,Release版出错Debug版不出错的一个最常见原因之深入剖析
也不知道网上有没有类似的文章,小弟斗胆在这里献丑一回;
最近一段时间,许多人发帖子说自己的MFC程序Release版会出错,而Debug版不会出错,记得在两年前我也曾遇到过类似的问题,但是没有进行深入研究,这两天我对这个问题作了一个深入的探讨发现了一个非常容易犯的错误,这也与VC编译器有关(不知道是微软的BUG还是怎么回事),首先我们看一个事例工程:
用VC新建一个Dialog工程,然后加入一个新的对话窗,并且生成一个对话窗类;然后在主对话窗的OnOK事件中建立那个新对话窗的非模态对话窗,例如下面:
void CADlg::OnOK()
{
m_pDlg = new CDlg1;//m_pDlg是类成员变量,新对话窗的指针
m_pDlg->Create(IDD_DIALOG1);
m_pDlg->ShowWindow(SW_SHOW);
}
然后加一个自定义消息:WM_MYMSG;
在新的对话窗的OnOK事件中向主对话窗发送WM_MYMSG消息;
void CDlg1::OnOK()
{
CWnd *pWnd = GetParent();
pWnd->SendMessage(WM_MYMSG);
}
下面我要说的就是最关键的地方了,我们为了响应WM_MYMSG消息通常有两种做法,一种是重载主对话窗的WindowProc虚函数,然后在函数内部响应这个消息,例如下面:
LRESULT CADlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
Switch(message)
{
case WM_MYMSG:
{
……
break;
}
}
return CDialog::WindowProc(message, wParam, lParam);
}
这种做法通常不会出错;
下面我们看第二种响应这个消息的方法;
首先在主对话窗中加入一个函数,例如下面:
VOID CADlg::OnMyMsg()
{
}
然后在主对话窗的消息映射表中加一项:
BEGIN_MESSAGE_MAP(CADlg, CDialog)
//{{AFX_MSG_MAP(CADlg)
ON_MESSAGE(WM_MYMSG , OnMyMsg)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
这样当我们的子对话窗向主对话窗发送WM_MYMSG消息的时候,MFC就会调用我们的OnMyMsg函数,于是错误出现了,首先我们看看ON_MESSAGE宏的定义;
#define ON_MESSAGE(message, memberFxn) \
{ message, 0, 0, 0, AfxSig_lwl, \
(AFX_PMSG)(AFX_PMSGW)(LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM))&memberFxn }
熟悉宏定义的朋友一看就明白,这个宏在展开的时候实际上是将两个参数压栈(WPARAM和LPARAM),然后调用函数指针;
而我们的函数OnMyMsg确没有参数定义,换句话说,函数返回的时候不会平栈,这就是Release版程序非法操作的原因;
再说具体些,我们把上面的OnMyMsg函数写成这样:
VOID CADlg::OnMyMsg()
{
MessageBox("测试");
}
然后看看它的汇编代码:
push 0
push 0
push 403020h
call 004017CA
ret
前面我们就不看了,看最后一句:
ret
完了,它直接ret了(当然了,直接ret是我们函数定义的结果),而不是比如说什么
ret 8
之类的语句(这是因为我们的函数没有定义参数,因此直接ret)。
这样我们进入函数前压栈的两个参数就没有进行平栈动作了;函数返回,栈不平当然就会非法操作了;
换句话说,如果我们的程序写成这样:
VOID CADlg::OnMyMsg()
{
MessageBox("测试");
__asm ret 8;
}
那么Release版就不会报错了(相反Debug版就会报错),不信请试验一下,但是这样写是不对的,请朋友们在编写程序的时候不要这样写,这只是说明消息映射函数平栈情况的一个证据罢了;
话分两头,为什么Debug版没有问题呢?
先看看下面的汇编:
mov ecx,dword ptr [ebp-0Ch]
mov dword ptr fs:[0],ecx
pop edi
pop esi
pop ebx
add esp,5Ch
cmp ebp,esp
call _chkesp (004022fc)
mov esp,ebp
pop ebp
VC在对MFC的Debug版程序进行编译的时候,会在函数的后面加上一段类似上面的代码,那段代码的功能就是检测esp,看看栈是否是平的,如果不平则强行平栈,因此Debug版程序不会出这种错误,至于微软为什么要这样做,我实在也是想不明白,请各位朋友也一起琢磨一下吧(欢迎跟贴讨论);
综上所述,我们在编写MFC程序,映射自己的消息函数的时候要么采用第一种方法,重载WindowProc虚函数,要么采用第二种方法,但是函数要定义两个参数(WPARAM和LPARAM),即使没有用处也要这样定义;这样就可以避免Release版出错Debug版不出错的绝大部分情况了;
另外,我这里再提一下这个宏:
ON_MESSAGE_VOID
这个宏定义在"afxpriv.h",这个宏与ON_MESSAGE相反,他的消息映射函数不能带参数。即如果用这个宏进行消息映射,那么那个消息映射函数就不能带参数,如果带了参数就会发生Release版出错,Debug版不出错的情况了;
最后,我们不管用上面那个宏映射消息响应函数,而你的消息响应函数不管定义成什么样子,VC在进行编译的时候都不会报错,因此这个错误将隐藏的很深,直到你即将发布Release版的时候才发现,程序会非法操作的;
以上所述仅代表个人看法,如有不同意的朋友,欢迎参加讨论;