CD CD FD FD FD FD FD FD
00 00 00 00 00 00 00 00
......
根据经验,p实际被分配了16个字节,后6个字节用于保护。我们按F5全速执行程序,会发现如下的错误信息被弹出:
Debug Error!
Program: c:\temp\test1\Debug\test1.exe
DAMAGE: after normal block (#55) at 0x00421AB0
Press Retry to debug the application
该信息提示,在正常内存块0x00421AB0后的内存被破坏(内存访问越界),我们点击Retry进入调试状态,发现调用堆栈是:
_free_dbg_lk(void *0x00421ab0, int 1) line 1033 + 60 bytes
_free_dbg(void *0x00421ab0, int 1) line 970 + 13 bytes
operator delete(void *0x00421ab0) line 351 + 12 bytes
CTest1App::InitInstance()line 54 + 15 bytes
很显然,这个错误是在调用delete时遇到的,出现在CTest1App::InitInstance()line 54 + 15 bytes之处。我们很容易根据这个信息找到,是在释放哪块内存时出现问题,之后,我们只需要根据这个内存的访问过程确定哪儿出错,这将大大降低调试的难度。
实例四:子类化
子类化是我们修改一个现有控件实现新功能的常用方法,我们借用实例一中的Debug对话框工程来演示我过去学习子类化的一个故事。我们创建一个缺省的名为Debug的对话框工程,并按照下列步骤进行实例化:
在对话框资源中增加一个Edit控件
用class wizard为CEdit派生一个类CMyEdit(由于今天不关心子类化的具体细节,因此这个类不作任何修改)
为Edit控件,增加一个控件类型变量m_edit,其类型为CMyEdit
在OnInitDialog中增加如下语句:
m_edit.SubclassDlgItem(IDC_EDIT1,this);
我们运行这个程序,会遇到这样的错误:
Debug AssertionFailed!
Application:C:\temp\Debug\Debug\Debug.exe
File:Wincore.cpp
Line:311
For information on howyour program can cause an assertion failure, see Visual C++ documentation onasserts.
(Press Retry to debugthe application)
点击Retry进入调试状态,我们可以看到调用堆栈为:
CWnd::Attach(HWND__ *0x000205a8) line 311 + 28 bytes
CWnd::SubclassWindow(HWND__* 0x000205a8) line 3845 + 12 bytes
CWnd::SubclassDlgItem(unsignedint 1000, CWnd * 0x0012fe34 {CDebugDlg hWnd=0x001d058a}) line 3883 + 12 bytes
CDebugDlg::OnInitDialog()line 120
可以看出在Attach句柄时出现问题,出问题行的代码为:
ASSERT(m_hWnd == NULL);
这说明我们在子类化时不应该绑定控件,我们删除CDebugDialog::DoDataExchange中的下面一行:
DDX_Control(pDX, IDC_EDIT1, m_edit);
问题就得到解决
总结
简而言之,call stack是调试中必须掌握的一个技术,但是程序员需要丰富的经验才能很好的掌握和使用它。你不仅仅需要熟知C++语法,还需要对相关的平台、软件设计思路有一定的了解。我的文章只能算一个粗浅的介绍,毕竟我在这方面也不算高手。希望对新进有一定的帮助。
调试之编程准备
对于一个程序员而言,学习一种语言和一种算法是非常容易的(不包括那些上学花很多时间玩,上班说学习没时间的人)。但是,任何程序都可能是有瑕疵的,尤其有过团队协作编程经验的人,对这个感触尤为深刻。
在我前面的述及调试的文章里,我侧重于VC集成环境中的一些设置信息和调试所需要的一些基本技巧。但是,仅仅知道这些是不够的。一个成功的调试的开端是编程中的准备。
分离错误
很多程序员喜欢写下面这样的式子:
CLeftView* pView =
((CFrameWnd*)AfxGetApp()->m_pMainWnd)->m_wndSplitterWnd.GetPane(0,0);
如果一切顺利,这样的式子当然是没什么问题。但是作为一个程序员,你应该时刻记得任何一个调用在某些特殊的情况下都可能失败,一旦上面某个式子失败,那么整个级联式就会出问题,而你很难弄清楚到底哪儿出错了。这样的式子的结果往往是:省了2分钟编码的时间,多了几星期的调试时间。
对于上面的式子,应该尽可能的把式子分解成独立的函数调用,这样我们可以随时确定是哪个函数调用出问题,进口缩小需要检查的范围。
检查返回值
检查返回值对于许多编程者来说似乎是一个很麻烦的事情。但是如果你能在每个可能出错的函数调用处都检查返回值,就可以立刻知道出错的函数。
有些人已经意识到检查返回值的重要性,但是要记住,只检查函数是否失败是不够的,我们需要知道函数失败的确切原因。例如下面的代码:
if(connect(sock,(const sockaddr*)&addr,sizeof(addr)) == SOCKET_ERROR)
{
AfxMessageBox("connect failed");
}
尽管这里已经检查了返回值,实际上没有多少帮助。正如很多在vckbase上提问的人一样,大概这时候只能喊“为什么连接失败啊?”。这种情况下,其实只能猜测失败的原因,即使高手,也无法准确说出失败的原因。
增加诊断信息
在知道错误的情况下,应该尽可能的告诉测试、使用者更多的信息,这样才能了解导致失败的原因。如果程序员能提供如下错误信息,对于诊断错误是非常有帮助的:
出错的文件:我们可以借助宏THIS_FILE和__FILE__。注意THIS_FILE是在cpp文件手工定义的,而__FILE__是编译器定义的。当记录错