MFC中,Release版出错Debug版不出错的一个最常见原因之深入剖析

来源:互联网 发布:云计算创始人 编辑:程序博客网 时间:2024/04/30 16:52
也不知道网上有没有类似的文章,小弟斗胆在这里献丑一回;  
       最近一段时间,许多人发帖子说自己的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版的时候才发现,程序会非法操作的;  
       以上所述仅代表个人看法,如有不同意的朋友,欢迎参加讨论;  
 
---------------------------------------------------------------  
 
http://expert.csdn.net/Expert/topic/2539/2539864.xml?temp=.9315149  
---------------------------------------------------------------  
 
哈,vc7就会报错了,我还真不知道,看来微软也意识到这是个BUG了;  
---------------------------------------------------------------  
 
这是  MFC  的一个  BUG,看看  MSDN  文档说的:  
ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.2052/vccore/html/vcrefwhatsnewlibrariesvisualc70.htm#vcrefwhatsnewlibrariesmfcvisualc70  
 
Visual  C++  .NET  中包含的下列库可能是新增的,也可能是经过更改的。    
.......  
Microsoft  基础类  (MFC)  库  
*  有关  MFC  的参考主题包含数百个新的代码示例。    
*  静态强制类型转换和  MFC  消息映射      从  Visual  C++  .NET  开始,MFC  对消息处理函数的返回类型和参数类型进行更严格的类型检查。这些新增行为通过使用错误信息标记潜在不安全的消息处理函数,来通知开发人员可能会遇到的问题。MFC  现对  ON_MESSAGE、ON_REGISTERED_MESSAGE、ON_THREAD_MESSAGE  和  ON_REGISTERED_THREAD_MESSAGE  使用静态强制类型转换。    
例如,过去开发人员可以对  ON_MESSAGE  或  ON_REGISTERED_MESSAGE  使用返回  void  而非  LRESULT  的成员函数,并且编译时不报告任何错误。而使用  Visual  C++  .NET,则可以捕获潜在的错误强制类型转换,并将它标记为错误。开发人员可以通过替换返回类型(用  LRESULT  替换)并重新编译来修复这种潜在的问题。    
 
*  DHTML  编辑组件:CHtmlEditCtrl、CHTMLEditView、CHtmlEditDoc。    
.........  
 
VC7  的界面用不惯,而且速度慢,否则值得升级。  
 
---------------------------------------------------------------  
 
对了,建议大家以后做东西的时候都用Release版本做,这样像上面的问题就不会出现,有的人  
可能会说用Release版本怎么调试啊。下面的办法可以解决。调试完了改回原来的设置再发布  
http://expert.csdn.net/Expert/TopicView1.asp?id=2555224  
看看我的回答  
 
Release版本单步跟踪方法:(可能对大家来说早就是小菜一碟了)  
选中Win32  Release然后  
Project-》setting-》C/C++  -》Category-》General  
                                                           -》Optimization-》Disable(Debug)  
                                                           -》Debug  Info-》Program  DataBase  
                                   -》Link---》Generate  Debug  Info打上钩  
 
另外请朋友们去捧捧这两个帖子。这个帖子我的回复花了我长时间。也许对那些  
Debug和release不太懂得人有帮助。虽然不是我的帖子,但是所涉及到的知识  
挺全的。  
 
http://expert.csdn.net/Expert/TopicView1.asp?id=2539864  
http://expert.csdn.net/Expert/topic/2553/2553540.xml?temp=.2079431  
本人关心技术。希望和有志之士(关心技术)成为朋友  
 
很同意woaini5994(孤独的猪)  的话  
这不算是作广告吧。希望版主不要删除  
 
 
---------------------------------------------------------------  
 
遇到并解决的第一个release  mode  bug  
 
       “在  Class  Wizard  添加的响应函数中使用手动添加的参数将导致  Debug  模式运行正常,但Release  模式运行时非法操作。”  
 
       估计原因:MFC默认的  ON_CONTROL  消息响应函数原型为  (void)pfn(void),  因此在未改变MFC函数类型声明时,用额外的参数调用会导致Release  mode下,程序堆栈上的函数返回地址被作为函数参数来错误使用,而函数返回地址也就自然不对了,从而导致Access  Violation。  
 
 
 
       解决办法:  
 
       1.把消息响应函数声明对应的  AfxSig_vv,改为相应函数类型的  AfxSig_xx,然后用新的语句(原来的宏展开后把AfxSig_vv换成AfxSig_XX)替代ON_CONTROL等宏。  
 
       2.把消息响应函数的函数体移到一个自定义的一般类函数中,在类函数中使用参数,消息响应函数只对类函数进行调用。(此方法仅适用于在消息响应函数中添加默认参数的情况)
原创粉丝点击