调试技术与异常处理Word格式文档下载.docx
《调试技术与异常处理Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《调试技术与异常处理Word格式文档下载.docx(22页珍藏版)》请在冰豆网上搜索。
,i,j,szDeb);
i+=1;
j+=5;
}
在以调试方式运行程序是,当你点击TRACE按钮时会看到在调试窗口中输出了调试信息。
当程序在调试过程中执行到此处时会在输出窗口输入"
tracei=5j=50\n,string=debugstring\n"
。
使用TRACE宏可以让我们随时掌握程序运行过程中变量的变化情况,因为大多数情况下我们都不希望使用断点进入到程序内部,而只是注意运行中数据的值。
注意:
不要采用TRACE宏一次性输出大批量数据或不间断输出数据,因为这样有可能会时程序运行变得非常缓慢,如:
voidtest_trace_e(void)
char*pszDeb=newchar[1024*1024];
%s\n"
pszDeb);
//或者
for(inti=0;
i<
sizeof(pszDeb);
i++)
{
TRACE("
%c\n"
pszDeb[i]);
}
有一点需要注意的是,TRACE宏在只在调试(DEBUG)版本中起作用,而在发行(RELEASE)版本无效,所以不要在TRACE宏中进行对程序状态进行改变的计算或是调用对状态有改变的函数,例如:
voidyourClass:
fun1()
%d"
++m_iTick);
//m_iTick状态改变
returnvalue=%d"
DoSomething());
DoSomething()
if(m_szOut=="
No"
)
returnFALSE;
else
m_szOut="
Yes"
//状态改变
reutrnTRUE;
在调试中还有一种方法可以将对象内部内容输出到调试窗口中,这就是使用转储(Dump)。
转储的实现要通过对象自身实现,在通过对象自身实现时有一个好处就在于可以输入内部受保护层成员。
首先CObject类定义了虚函数:
virtualvoidDump(CDumpContext&
dc)const;
当你从CObject中派生新类时你需要重载该函数,例如下面是个很简单的例子:
classCMyButton:
publicCButton
public:
CMyButton();
~CMyButton(){};
#ifdef_DEBUG
//由于转储只在调试版本中实现,所以使用条件编译
virtualvoidDump(CDumpContext&
#endif
protected:
CStringm_szHotText;
//当鼠标移动过显示的文字
};
CMyButton:
CMyButton():
CButton()
#ifdef_DEBUG
voidCMyButton:
Dump(CDumpContext&
dc)const
dc<
<
"
\n"
CButton:
Dump(dc);
\ndumpofCMyButton\ntextis"
m_szHotText;
我们看到Dump函数接受一个参数为CDumpContext,通过该类可以将数据输出到调试窗口或是文件。
CDumpContext重载了<
操作符,利用<
可以输出各种类型的数据。
下面的代码示范了调用方法:
OnDump()
CButtonbu1;
CMyButtonbu2;
bu1.Dump(afxDump);
//afxDump是一个CDumpContext类型的全局变量。
bu2.Dump(afxDump);
//输出的调试信息为:
aCButtonat$64F538
m_hWnd=0x0
aCButtonat$64F4F4
dumpofCMyButton
textisnotinit
//上面两行输出了CButton的转储信息,后面四行输出了CMyButton的转储信息。
设置断点进行跟踪和输出中间结果是最基本的调试方法,也是必须掌握的技巧。
下载本节示范代码。
3.2变量/对象合法性检查
在VC中检查变量合法性一般利用ASSERT(x)宏,ASSERT的作用在于检查表达式是否为假或为NULL,如果为假则会引发异常。
在MFC中ASSERT宏被大量使用,例如:
BOOLCWnd:
Attach(HWNDhWndNew)
ASSERT(m_hWnd==NULL);
//onlyattachonce,detachon
destroy
//…
returnTRUE;
voidCString:
AllocBuffer(intnLen)
ASSERT(nLen>
=0);
ASSERT(nLen<
=INT_MAX-1);
//
voidCDocument:
AddView(CView*pView)
//othercode…
ASSERT(pView->
m_pDocument==NULL);
ASSERT(m_viewList.Find(pView,NULL)==NULL);
当ASSERT失败并引发异常时会有对话框谈出并报告发生该ASSERT失败位置。
报错信息如:
assertionfailedinfile<
THIS_FILE>
inline<
__LINE__>
并允许你选择继续运行(Ignore)或是终止(Abort)程序。
(当然选择继续运行是很危险的)选择Retry将会启动调试软件对程序进行调试。
此外我们时常可以看到下面的用法:
ASSERT(pWnd);
//检查指针是否已经赋值
if(condition)
ASSERT(FALSE);
//强制抛出一个ASSERT异常
此外还有一点,ASSERT宏只在调试版本中才会有作用,在调试版本中ASSERT(f)宏被展开为
do
if(!
(f)&
&
AfxAssertFailedLine(THIS_FILE,__LINE__))
AfxDebugBreak();
}while(0)
//while(0)用来保证ASSERT宏后面可以不跟随“;
”如ASSERT(f)与ASSERT(f);
都合法
//THIS_FILE表示当前当前文件文件名,__LINE__为当前代码所在的行数
而在发行版本中会被展开为:
((void)0)
所以对程序内部状态改变的代码不能够放置在ASSERT宏中否则在发行版中会出现不正常的现象,例如下面的代码:
ASSERT(++m_iTick>
5);
ASSERT(DoSomething()==TRUE);
如果希望合法检查在发行版本中同样起作用则可以利用VERIFY宏,VERIFY宏与ASSERT宏的VERIFY的不同在与VERIFY在发行版本中同样会起作用,但是使用VERIFY会导致非常不友好的用户界面。
对象的合法性检查需要根据对象自身的状态和一些对象自己的逻辑来作出判断,因此在对象外部(对象自身的状态和对象自己的逻辑外部可能无法访问)就无法正确判断,一个省时有效的办法是在对象内部进行检查,有对象自己负责合法性检查,例如下面的代码:
voidCObList:
AssertValid()const
CObject:
AssertValid();
if(m_nCount==0)
//emptylist
ASSERT(m_pNodeHead==NULL);
ASSERT(m_pNodeTail==NULL);
//non-emptylist
ASSERT(AfxIsValidAddress(m_pNodeHead,sizeof(CNode)));
ASSERT(AfxIsValidAddress(m_pNodeTail,sizeof(CNode)));
MFC利用成员函数voidCObject:
AssertValid()const来实现对象的合法性检查,所以新的类必须是CObject的派生类,(在MFC中几乎所有的类都由CObject派生)由于C++的多态性派生类的AssertValid函数会被正确的调用。
函数定义中的const表示该函数体中不能改变成员变量的值。
我们所需要做的就是重载AssertValid,并实现对象状态合法性的检查。
在AssertValid我们不但可以检查数据的正确性,也可以对数据的逻辑性进行检查。
例如一个盒子中的白球不能多于黑球,而且总数不能多于100:
classCBox:
publicCObject
...
voidAssertValid()const;
intm_iWhiteBall,m_iBlackBall;
voidCBox:
//先调用父类的检查函数
ASSERT(m_iWhiteBall<
=m_iBlackBall);
ASSERT(m_iWhiteBall+m_iBlackBall<
=100);
到这里你会问什么时候调用AssertValid函数?
在MFC中对象的合法性检查都依赖AssertValid,比如在销毁窗口对象时会首先检查该窗口对象是否合法,而你自己也可以手工调用AssertValid来检查对象的合法性,例如下面的代码:
voidCDocument:
POSITIONpos=GetFirstViewPosition();
while(pos!
=NULL)
CView*pView=GetNextView(pos);
ASSERT_VALID(pView);
而当你对自己的CView派生类CYourView重载AssertValid后,CYourView的AssertValid就会在文档类检查视类合法性时调用。
此外MFC中定义了ASSERT_VALID宏来执行安全的对象检查,ASSERT_VALID宏会展开AfxAssertValidObject,并先检查指针的合法性。
这样避免了下面的错误:
CView*pV=NULL;
pV->
//安全的方法是利用
ASSERT_VALID(pView);
与ASSERT宏一样,ASSERT_VALID宏只在调试版本中起作用。
利用合法性检查可以帮助我们在由于变量非法而引发异常方便的定位错误,所以在开发程序时多利用合法性检查并在必要的地方使用检查宏会帮助我们更有效的进行调试。
3.3内存泄露检查
在VC中提供内存检查的机制是跟踪new操作,也就是说所有的new操作都会被记录,如果通过new操作所分配的内存未被正常delete将会在程序退出时在调试窗口中显示出具体的内存泄露信息。
同样通过malloc分配的内存也会被跟踪,但是在显示时就不会知道实在程序中何处进行了malloc操作。
先看一下下面的例子:
void_tmain()
char*pszNew=(char*)malloc(200);
char*pszNew2=newchar[100];
CString*pszNew3=newCString("
test"
);
//通过调试方式运行后并退出,可以看到调试信息中关于内存泄露的信息如下:
Detectedmemoryleaks!
Dumpingobjects->
strcore.cpp(118):
{37}normalblockat0x007702E0,17byteslong.
Data:
<
test>
01000000040000000400000074657374
G:
\temp2\sam_sp_33\sam_sp_33.cpp(42):
{36}normalblockat0x00770520,4byteslong.
w>
EC027700
//对于CString*pszNew3=newCString("
产生的信息
\temp2\sam_sp_33\sam_sp_33.cpp(41):
{35}normalblockat0x00770320,100byteslong.
>
CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD
//对于char*pszNew2=newchar[100];
{34}normalblockat0x007703B0,200byteslong.
CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD
//对于char*pszNew=(char*)malloc(200);
Objectdumpcomplete.
可以看到通过new分配的内存在显示信息时会报告出在那一个文件的那一行进行的new操作,而通过malloc分配的内存则仅仅是显示出内存泄露的信息而无法定位分配内存的程序位置。
此外如果需要在文件头部定义DEBUG_NEW宏才可以正确的跟踪new操作。
具体代码如下:
#definenewDEBUG_NEW
由于对new操作的跟踪只需要在调试版本中出现所以使用了条件编译。
我们可以看到VC所提供的检查内存泄露的方式是非常易于使用的,我们在开发程序时一定要注意内存的分配问题,特别是对于一些长时间运行的程序。
下载本节代码
3.4异常捕捉与处理
在软件开发的过程中错误捕捉显得尤为重要,因为有的错误会导致软件功能失常,而有的却会造成破坏性损失。
世上没有不出错的软件。
软件的逻辑错误,人为操作的失误,运行条件的改变等等因素都会导致异常的出现。
下面的代码是一个例子:
char*pszData=NULL;
//假设为全局变量
BOOLReadData(void)
FILE*pFile=fopen("
c:
\\data.dat"
"
r"
//假设c:
\data.dat文件长度为1024BYTE
if(pFile!
if(pszData)
deletepszData;
pszData=newchar[1024];
if(1024==fread(pszData,1024,1,pFile))
returnTRUE;
//打开文件失败错误,或文件长度不够
returnFALSE;
voidPrintData()
1024;
printf("
%x"
pszData[i]);
粗看这段代码应该是没有问题的,因为该段代码进行了错误处理,在操作没成功时返回了错误。
但是在PrintData中就有一个隐患,如果pszData为NULL时怎么办,毫无疑问,此时会导致异常情况发生。
也许在软件流程中如果ReadData返回错误后根本就无法进入PrintData,但是在一个十万行以上的程序中这种错误随时会存在。
另一个例子是关于内存分配的,如果你现在分配10K的内存出现失败,你的程序会如何反应,是退出还是继续。
更令人沮丧的是很多开发人员在开发过程中对与某些可能出现的错误情况都未加以考虑,这使得出现错误时对错误的跟踪和定位成为极大的困难。
所以使用一种强制的机制保证一些致命错误能够被处理是一个明智的选择。
比如说内存错,文件错等等。
在C++中引入了一种在C语言中不存在的特性,错误捕捉机制(try/catch),这是一种强制性的机制,如果程序中抛出的异常未被成功捕捉,该异常将一直会沿着函数调用的顺序上升,直到被捕捉到为止。
而默认的main函数之外存在有异常捕捉代码,这段默认的异常捕捉代码将会终止程序并报告异常的发生。
下面我们先看看try/catch的语法的一个例子:
voiddo_something()
//循环产生各种异常
staticintiTime=0;
switch(iTime++%3)
case(0):
throw(int)1;
break;
case
(1):
throw"
error"
case
(2):
throw(double)1.1;
voidCSam_sp_34Dlg:
OnTc()
try
do_something();
catch(inte)
AfxMessageBox("
errorhandler1\n"
catch(char*sz)
errorhandler2\n"
当你第三次执行OnTc时,由于产生的异常没有被成功捕捉所以将由默认的捕捉代码捕捉并终止程序。
这时候我们可以写另外一段代码来捕捉我们未能够估计到的异常。
OnTcE()
catch(...)
catchall\n"
catch(...)将会捕捉所有未指明类型的异常。
在这里我们可以看到异常是可以分为很多类的,而分类的依据就是抛出异常时候所使用的数据类型。
在上面的例子中我们看到抛出异常的语法很简单,使用关键字throw就可以了,后面跟异常常的类型。
如果单独使用throw则表示继续抛出当前异常,这种用法表明在处理当前异常后继续将该异常传递给其他的异常处理块进行处理。
voiddo_something_2()
catch(...)
catchedandthrow"
throw;
//继续传递该异常
OnJt()
do_something_2();
最后我们来看看异常的处理顺序,异常首先会被距离try块最近的catch块捕捉到。
看下面的例子:
voiddo_something_3()
catch(inte)
catchedint"
OnCp()
do_something_3();