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版的时候才发现,程序会非法操作的;  
       以上所述仅代表个人看法,如有不同意的朋友,欢迎参加讨论;  
 
---------------------------------------------------------------  
 
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.把消息响应函数的函数体移到一个自定义的一般类函数中,在类函数中使用参数,消息响应函数只对类函数进行调用。(此方法仅适用于在消息响应函数中添加默认参数的情况)
 
 
今天在写开启MySQL和Tomcat服务的一个小程序, 有段时间没有用VC了, 于是就用VC中的托盘实现了相应操作, 其中用到了托盘右键弹出菜单消息映射和消息响应, 如:

        消息申明:

        ON_COMMAND_RANGE(ID_CLOUD, ID_CLOUD + 4, OnCommandMy)   //托盘右键菜单响应消息

  消息响应函数申明:

       afx_msg void OnCommandMy(WPARAMwParam, LPARAM lParam );

       通过wParam参数的低字节判断菜单的ID, 进而来实现不同菜单的消息响应.

       因为以前写过类似的程序, 就直接Copy了过来, 在Debug下调试好了, 想应该OK了, 结果在Release版本

下只要对托盘右键菜单进行操作就发生内存错误, 搞得自己很是郁闷, 弄了很久, 没有办法, 就在Release版本下进行了调试, 结果发现是消息响应中出现了问题, 网上也找了很久, 没有找到解决的办法, 最后也只好老实看MSDN, 结果发现我的消息响应函数中的申明可能存在问题:

 

afx_msg void OnCommandMy(WPARAMwParam, LPARAM lParam );申明只适用于ON_COMMAND消息的函数申明, 而ON_COMMAND_RANGE的函数申明在MSDN中建议写成这样:

OnCommandMy(UINT nID);

通过switch(nID) case **:进行针对不同菜单进行消息响应.

nID就是菜单传入消息的ID号, 奇怪的是, 在Debug版本下, 先前的申明方式运行完全正常, 查阅了MSDN, 找出了可能的原因:

Handler functions for single commands normally take no parameters. With the exception of update handler functions, handler functions for message-map ranges require an extra parameter,nID, of type UINT. This parameter is the first parameter. The extra parameter accommodates the extra command ID needed to specify which command the user actually chose.

针对单个Command消息响应函数可以不带参数, 但是对于多个Command消息如ON_COMMAND_RANGE申明的消息响应需要将函数参数列表中的第一个参数定义为UINT nID, 指明command 的ID号, 按照MSDN的理解, ON_COMMAND_RANGE也可以像ON_COMMAND那样在消息响应函数中定义两个参数, 如afx_msg void OnCommandMy(WPARAM wParam, LPARAM lParam );在Debug和Release下, 编译不会出现问题, 在Debug下运行也不会出现问题, 但是在Release下面却出现内存错误, 所以可以带多个参数感觉只能在Debug下可以行的能, 在Release下就没失效了.

今天又花了整整一天上网查阅相关的资料并利用VC查看相应的汇编代码发现, 应该是函数调用和返回时栈操作不平衡导致Release版本下出现了内存错误的问题, ON_COMMAND_RANGE在MFC默认的消息响应函数中, 参数只有一个, 如:
#define ON_COMMAND_RANGE(id, idLast, memberFxn) \
 { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)idLast, AfxSig_vw, \
  (AFX_PMSG)(void (AFX_MSG_CALL CCmdTarget::*)(UINT))&memberFxn },
  // ON_COMMAND_RANGE(id, idLast, OnFoo) is the same as
  //   ON_CONTROL_RANGE(0, id, idLast, OnFoo)

函数调用过程中, 会将传入的参数进行压栈操作, 因为MFC默认的传入参数只有一个, 因此调用OnCommandMy时会有系统传入的一个消息参数进行压栈操作. 在函数返回时, 应该进行出栈操作, 并且保证调用完成后栈维持平衡, 否则会出现可能的内存错误.

在DEBUG上没有出现内存错误在于在调用OnCommandMy函数返回时编译器在返回代码处添加了如下的汇编代码:

pop edi
       pop esi
       pop  ebx
       add esp, 48h
       cmp ebp, esp
       call __chkesp (0041e680)
       mov esp, ebp
       pop ebp
       ret 8(两个参数出栈)

此汇编代码的作用就是在函数返回时检查调用中和调用返回时的栈是否一致, 如果不一致, 就强制平栈操作, 因为在这个调用过程中, 传入OnCommandMy的消息参数只有一个(只是申明成两个, 实际只有一个参数传入), 所以存在栈不一致的情况, 但是强制平栈可以避免由此引起的错误.

在Release版本下, 就没有了检测栈的操作,

只是简单的下面几句汇编代码完成出栈操作:

mov esp, ebp
       pop ebp
       ret
 8两个参数出栈)

可以明显看到, Release下出现了栈操作不平衡的情况, 即入栈数小于出栈数, 从而导致栈区地址错误, 当其它函数两次对栈区进行地址访问时就极有可能出现内存错误的现象了.

所以, 平时写程序时在Debug下高度完成之后, 最好还在Release下看一下, 因为有些时候, Debug下对函数参数的检查不是那么严格, 并且在栈的操作上, Debug可以帮助我们解决很多隐藏的问题, 但是Release下就不会了. 另外在自定义的消息响应函数中, Debug和Release都不会对响应函数的参数列表与MFC默认参数列表进行一致性检测, 从而可能隐藏重大的内存出错的可能性, 导致最终软件在Release下运行可能发生崩溃.

所以上面MSDN的东西说得模棱两可的, 明明ON_COMMAND_RANGE消息响应函数就只能那样定义, 他却说成那样, ......................

自定义的消息响应函数可以在AFXMSG.H下看到MFC默认的定义, 为了不引起上面谈到的问题, 最好遵照这个头文件里面说明的进行定义, 它可以在VC安装目录下的VC98\MFC\SRC查找到.

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值