`
huozheleisi
  • 浏览: 1226813 次
文章分类
社区版块
存档分类
最新评论

VC 调试技术与异常(错误)处理

 
阅读更多

调试技术与异常(错误)处理(1)

跟踪与中间过程输出

也许一个开发人员一半以上的时间都是在面对错误,所以好的调试/查错方法(工具)会减轻我们工作的负担,也可以让枯燥的DEBUG过程得以缩短。

VC开发环境所提供的调试环境是很优秀的,我们可以运用单步运行,设置断点的方法来查找问题所在。但是这种跟踪是非常耗时的,所以我们需要采用一些策略来让我们更容易的发现错误并对错误进行定位,所幸的是VC在这方面提供了强大的支持。在本节中我们先看看如何利用设置断点和利用TRACE宏来输出运行情况。

在VC开发环境中按下F9就可以在光标所在行设置断点,再按一次就可以取消该处断点。设置断点的意义在于在调试过程当运行到该行时回产生一个中断并返回到VC开发环境中,在开发环境中你可以查看各个变量的值。下面是我们用于测试的代码,前面有红色圆形的行表示该行设置有断点:



在调试过程中到达断点处你可以通过上下文变量窗口(Variables)观察该函数中的变量的值,如果需要观察未在该函数出现的全局变量或者类成员变量这需要将变量名添加到观察窗口(Watch)中输入变量名称。但程序编译完成后请按下F5键以调试的方式执行程序,当进入断点时VC开发环境会被自动激活,然后我们可以可以观察程序的运行情况。在调试过程中也可以添加和删除断点。如下图:

如果在运行过程中被观察的变量的值发生了变化则该变量在观察窗中会变为红色。

一般来讲设置断点有下面的技巧:

  • 设置在进行判断的代码处,这样可以在运行时可以观察判断所依赖的条件是否正确。
  • 设置函数开始处,观察该函数所依赖的变量是否都设置正确。
  • 设置函数结束处,观察该函数对变量的改变是否正确。
  • 设置进入其他函数前/后,通过黑盒法检查该函数功能是否正确。
  • 对于循环体,应该先测试一个循环次数小的条件来检查循环逻辑是否正确,或者在循环的前几次设置断点,在运行几次后取消断点。

MFC中提供的TRACE宏可以帮助我们在程序调试运行过程中方便的输出调试信息。TRACE宏的定义为:TRACE(exp),其中的表达式使用与printf相同的表达方法。例如下面的代码:

void CSam_sp_31Dlg::OnTest2()
{
    static int i=5,j=50;
    char szDeb[]="debug string";
    TRACE("trace i=%d j=%d/nstring=%s/n",i,j,szDeb);
    i+=1;
    j+=5;
}

在以调试方式运行程序是,当你点击TRACE按钮时会看到在调试窗口中输出了调试信息。

当程序在调试过程中执行到此处时会在输出窗口输入"trace i=5 j=50/n,string=debug string/n"。使用TRACE宏可以让我们随时掌握程序运行过程中变量的变化情况,因为大多数情况下我们都不希望使用断点进入到程序内部,而只是注意运行中数据的值。

注意:不要采用TRACE宏一次性输出大批量数据或不间断输出数据,因为这样有可能会时程序运行变得非常缓慢,如:

void test_trace_e(void)
{
    char *pszDeb=new char[1024*1024];

    TRACE("%s/n",pszDeb);
    //或者
    for(int i=0;i<sizeof(pszDeb);i++)
    {
        TRACE("%c/n",pszDeb[i]);
    }
}

有一点需要注意的是,TRACE宏在只在调试(DEBUG)版本中起作用,而在发行(RELEASE)版本无效,所以不要在TRACE宏中进行对程序状态进行改变的计算或是调用对状态有改变的函数,例如:

void yourClass::fun1()
{
    TRACE("%d",++m_iTick); //m_iTick状态改变
    TRACE("return value = %d",DoSomething());
}
void yourClass:DoSomething()
{
    if(m_szOut == "No")
    {
        return FALSE;
    }
    else
    {
        m_szOut="Yes"; //状态改变
        reutrn TRUE;
    }
}

在调试中还有一种方法可以将对象内部内容输出到调试窗口中,这就是使用转储(Dump)。转储的实现要通过对象自身实现,在通过对象自身实现时有一个好处就在于可以输入内部受保护层成员。首先CObject类定义了虚函数:virtual void Dump( CDumpContext& dc ) const;当你从CObject中派生新类时你需要重载该函数,例如下面是个很简单的例子:

class CMyButton : public CButton
{
public:
    CMyButton();
    ~CMyButton(){};
public:
#ifdef _DEBUG
    //由于转储只在调试版本中实现,所以使用条件编译
    virtual void Dump( CDumpContext& dc ) const;
#endif

protected:
    CString m_szHotText;//当鼠标移动过显示的文字
};

CMyButton::CMyButton():CButton()
{
}

#ifdef _DEBUG
void CMyButton::Dump( CDumpContext& dc ) const
{
    dc<<"/n";
    CButton::Dump(dc);
    dc<<"/ndump of CMyButton /ntext is "<<m_szHotText;
    dc<<"/n";
}
#endif

我们看到Dump函数接受一个参数为CDumpContext,通过该类可以将数据输出到调试窗口或是文件。CDumpContext重载了<<操作符,利用<<可以输出各种类型的数据。下面的代码示范了调用方法:

void CSam_sp_31Dlg::OnDump()
{
    CButton bu1;
    CMyButton bu2;
#ifdef _DEBUG
    //由于转储只在调试版本中实现,所以使用条件编译
    bu1.Dump(afxDump); //afxDump是一个CDumpContext类型的全局变量。
    bu2.Dump(afxDump);
#endif
}
//输出的调试信息为:
a CButton at $64F538
m_hWnd = 0x0

a CButton at $64F4F4
m_hWnd = 0x0
dump of CMyButton
text is not init

//上面两行输出了CButton的转储信息,后面四行输出了CMyButton的转储信息。

设置断点进行跟踪和输出中间结果是最基本的调试方法,也是必须掌握的技巧

调试技术与异常(错误)处理(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宏中否则在发行版中会出现不正常的现象,例如下面的代码:

voidyourClass::fun1()

{

ASSERT(++m_iTick>5);

ASSERT(DoSomething()==TRUE);

}

voidyourClass:DoSomething()

{

if(m_szOut=="No")

{

returnFALSE;

}

else

{

m_szOut="Yes";//状态改变

reutrnTRUE;

}

}

如果希望合法检查在发行版本中同样起作用则可以利用VERIFY宏,VERIFY宏与ASSERT宏的VERIFY的不同在与VERIFY在发行版本中同样会起作用,但是使用VERIFY会导致非常不友好的用户界面。

对象的合法性检查需要根据对象自身的状态和一些对象自己的逻辑来作出判断,因此在对象外部就无法正确判断,一个省时有效的办法是在对象内部进行检查,有对象自己负责合法性检查,例如下面的代码:

voidCObList::AssertValid()const

{

CObject::AssertValid();

if(m_nCount==0)

{

//emptylist

ASSERT(m_pNodeHead==NULL);

ASSERT(m_pNodeTail==NULL);

}

else

{

//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::AssertValid()const

{

CObject::AssertValid();//先调用父类的检查函数

ASSERT(m_iWhiteBall<=m_iBlackBall);

ASSERT(m_iWhiteBall+m_iBlackBall<=100);

}

到这里你会问什么时候调用AssertValid函数?在MFC中对象的合法性检查都依赖AssertValid,比如在销毁窗口对象时会首先检查该窗口对象是否合法,而你自己也可以手工调用AssertValid来检查对象的合法性,例如下面的代码:

voidCDocument::AssertValid()const

{

CObject::AssertValid();

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->AssertValid();

//安全的方法是利用

ASSERT_VALID(pView);

与ASSERT宏一样,ASSERT_VALID宏只在调试版本中起作用。

利用合法性检查可以帮助我们在由于变量非法而引发异常方便的定位错误,所以在开发程序时多利用合法性检查并在必要的地方使用检查宏会帮助我们更有效的进行调试。

调试技术与异常(错误)处理(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.

Data:<w>EC027700

//对于CString*pszNew3=newCString("test");产生的信息

G:/temp2/sam_sp_33/sam_sp_33.cpp(41):{35}normalblockat0x00770320,100byteslong.

Data:<>CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD

//对于char*pszNew2=newchar[100];产生的信息

{34}normalblockat0x007703B0,200byteslong.

Data:<>CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD

//对于char*pszNew=(char*)malloc(200);产生的信息

Objectdumpcomplete.

可以看到通过new分配的内存在显示信息时会报告出在那一个文件的那一行进行的new操作,而通过malloc分配的内存则仅仅是显示出内存泄露的信息而无法定位分配内存的程序位置。

此外如果需要在文件头部定义DEBUG_NEW宏才可以正确的跟踪new操作。具体代码如下:

#ifdef_DEBUG

#definenewDEBUG_NEW

#endif

由于对new操作的跟踪只需要在调试版本中出现所以使用了条件编译。

我们可以看到VC所提供的检查内存泄露的方式是非常易于使用的,我们在开发程序时一定要注意内存的分配问题,特别是对于一些长时间运行的程序

调试技术与异常(错误)处理(4)

异常捕捉与处理在软件开发的过程中错误捕捉显得尤为重要,因为有的错误会导致软件功能失常,而有的却会造成破坏性损失。世上没有不出错的软件。软件的逻辑错误,人为操作的失误,运行条件的改变等等因素都会导致异常的出现。下面的代码是一个例子:

char*pszData=NULL;//假设为全局变量

BOOLReadData(void)

{

FILE*pFile=fopen("c://data.dat","r");//假设c:/data.dat文件长度为1024BYTE

if(pFile!=NULL)

{

if(pszData)deletepszData;

pszData=newchar[1024];

if(1024==fread(pszData,1024,1,pFile))

returnTRUE;

}

//打开文件失败错误,或文件长度不够

returnFALSE;

}

voidPrintData()

{

for(inti=0;i<1024;i++)

{

printf("%x",pszData);

}

}

粗看这段代码应该是没有问题的,因为该段代码进行了错误处理,在操作没成功时返回了错误。但是在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";

break;

case(2):throw(double)1.1;

break;

}

}

voidCSam_sp_34Dlg::OnTc()

{

try

{

do_something();

}catch(inte)

{

AfxMessageBox("errorhandler1/n");

}catch(char*sz)

{

AfxMessageBox("errorhandler2/n");

}

}

当你第三次执行OnTc时,由于产生的异常没有被成功捕捉所以将由默认的捕捉代码捕捉并终止程序。

这时候我们可以写另外一段代码来捕捉我们未能够估计到的异常。

voidCSam_sp_34Dlg::OnTcE()

{

try

{

do_something();

}catch(inte)

{

AfxMessageBox("errorhandler1/n");

}catch(char*sz)

{AfxMessageBox("errorhandler2/n");

}catch(...)

{

AfxMessageBox("catchall/n");

}

}

catch(...)将会捕捉所有未指明类型的异常。在这里我们可以看到异常是可以分为很多类的,而分类的依据就是抛出异常时候所使用的数据类型。

在上面的例子中我们看到抛出异常的语法很简单,使用关键字throw就可以了,后面跟异常常的类型。如果单独使用throw则表示继续抛出当前异常,这种用法表明在处理当前异常后继续将该异常传递给其他的异常处理块进行处理。

voiddo_something_2()

{

try

{

do_something();

}catch(...)

{

AfxMessageBox("catchedandthrow");

throw;//继续传递该异常

}

}

voidCSam_sp_34Dlg::OnJt()

{

try

{

do_something_2();

}catch(inte)

{

AfxMessageBox("errorhandler1/n");

}catch(char*sz)

{

AfxMessageBox("errorhandler2/n");

}catch(...)

{

AfxMessageBox("catchall/n");

}

}

最后我们来看看异常的处理顺序,异常首先会被距离try块最近的catch块捕捉到。看下面的例子:

voiddo_something_3()

{

try

{

do_something();

}catch(inte)

{

AfxMessageBox("catchedint");

}

}

voidCSam_sp_34Dlg::OnCp()

{

try

{

do_something_3();

}catch(inte)

{//这段代码是无意义的,因为do_something_3已经捕捉这种类型的异常

AfxMessageBox("errorhandler1/n");

}catch(char*sz)

{

AfxMessageBox("errorhandler2/n");

}catch(...)

{

AfxMessageBox("catchall/n");

}

}

在MFC中定义了一些专门用于处理异常的类,所有这些类都由CException派生,并各自负责不同的异常情况,在MFC内部出现异常并抛出异常时将会抛出以下异常类:类用途CException异常基类CNotSupportedException进行系统不支持的操作时抛出的异常类CMemoryException内存分配失败时抛出的异常类CArchiveException文件串行化失败时抛出的异常类CFileException文件读写错误时抛出的异常类CResourceException资源无法装入时抛出的异常类COleExceptionOLE发生异常时抛出的异常类CInternetException使用WinInet功能时抛出的异常类CUserException用户定义的异常类

下面的代码演示了如何捕捉异常,我们需要注意到MFC抛出异常类的指针,但是我们不需要手工删除该指针,MFC在空闲时会自动删除,此外也不要调用delete对指针进行删除因为有些被抛出的异常指针可能是全局变量:

voidCSam_sp_34Dlg::OnMfcF()

{

try

{

CFilefileTest("c://not_exist.txt",CFile::modeRead);

}catch(CException*e)

{//使用基类进行捕捉

e->ReportError();

}

try

{

CFilefileTest2("c://autoexec.bat",CFile::modeRead);

charszLine[100]="remtestline/n";

fileTest2.Write(szLine,strlen(szLine));

}catch(CFileException*e)

{//使用文件异常类进行捕捉

e->ReportError();//不需要调用e->Delete();进行删除

}

}

MFC中的异常类提供了简便的获取错误信息的手段,但你捕捉到异常后可以通过检查异常类中的成员变量来检查错误原因。

掌握好异常处理对于编写无错代码帮助很大,你也应该在自己的代码中添加进异常抛出代码,这样可以提醒开发人员在开发过程加强对运行时错误的处理。

分享到:
评论

相关推荐

    VC专题教程

    打包下载 ------ 2.1 ActiveX控件介绍 ------ 2.2 利用MFC开发ActiveX控件 ------ 2.3 利用ATL(ActiveX模板库)创建ActiveX控件 ------ 2.4 调试并使用ActiveX控件 +-- 第三章 调试技术与异常(错误)处理 打包下载...

    debug程序,应用程序发生异常

    vc6 输入源代码,应用程序发生异常。调试发现是除零错误,但是类似的check2却运行得很好,只在上面加了几行字.

    闻怡洋VC专题教程ActiveX控件开发 进程/线程控制 文件操作 内存管理

    +-- 第三章 调试技术与异常(错误)处理 |------ 3.1 跟踪与中间过程输出 |------ 3.2 变量/对象合法性检查 |------ 3.3 内存泄露检查 |------ 3.4 异常捕捉与处理 +-- 第四章 进程/线程控制 |------ 4.1 为什么...

    一个专业的调试工具软件

    演示了如何使用_TRACE将调试信息输出到Tracer工具的窗口中,同时演示了当Demo程序运行崩溃时通过Tracer进行BUG的源码定位,您可以马上看到Tracer所报告出来的错误所在的源代码行与您自己在源码中所发现的是否一致。...

    Qualcomm平台BREW开发技术文档

     错误的资源处理方式 45  资源载入失败(ISHELL_LoadResString)现象。 46  文件路径错误。 46  数据缓冲区内存分配太小。 46  系统内部解析错误。 47 程序CLSID规范 48  CLSID是什么? 48  CLSID的...

    代码语法错误分析工具pclint8.0

    1.将pclint.rar解压至c:\, 这样lint文件就位与c:\pclint(安装目录)下了。 2.将c:\pclint\lnt 下的3个文件lib-w32.lnt,env-vc6.lnt,co-msc60.lnt拷贝至c:\pclint下, 再在安装目录下创建std.lnt和options.lnt两个...

    QUALCOMM平台BUIW开发文档

     错误的资源处理方式 45  资源载入失败(ISHELL_LoadResString)现象。 46  文件路径错误。 46  数据缓冲区内存分配太小。 46  系统内部解析错误。 47 程序CLSID规范 48  CLSID是什么? 48  CLSID的...

    pub类库

    关于异常和错误处理 也是仁者见仁,智者见智! 习惯于C开发的朋友,大概喜欢函数错误时返回错误码。函数有返回值,就要处理,就使程序逻辑较为复杂,看去也比较的混乱。 我则喜欢用异常代替,主要是代码简洁和...

    VC++6.0核心编程源码.rar

    第一章 对程序错误的处理 在我们开始介绍Microsoft Windows应该提供的许多特性之前,我们首先必须了解Windows的各个函数是如何进行错误处理的。 当你调用一个Windows函数时,它首先要检验你传递给它的的各个参数的...

    逆向工程实验报告.docx

    除了具有强大的反汇编能力外,OLLYDBG还提供了大量不同的视图,包括列出模块中的导入和导出、显示被调试者拥有的窗口和其它对象的列表、显示当前的异常句柄链以及对那些在库中正确命名的函数使用导入库(.lib文件)...

    C#微软培训资料

    8.4 异常处理语句 .95 8.5 小 结 .100 第三部分 面向对象的 C#.101 第九章 面向对象的程序设计 .101 9.1 面向对象的基本概念.101 9.2 对象的模型技术 .103 9.3 面向对象的分析 .105 9.4 面向对象的设计...

    精易编程助手3.3+精易模块v6.3.1

    6、修复“类_托盘”-“气泡提示”与“置提示信息”调用显示错误的BUG; 7、修复“网页_更改IE版本”的参数错误,感谢【@BlackMarch 】的反馈; 8、修复“类_JSON-删属性”命令的BUG,支持删除一层属性; 9、修复...

    asp.net知识库

    利用委托机制处理.NET中的异常 与正则表达式相关的几个小工具 你真的了解.NET中的String吗? .NET中的方法及其调用(一) 如何判断ArrayList,Hashtable,SortedList 这类对象是否相等 帮助解决网页和JS文件中的中文...

    DynamipsGUI2.8及2.83增量更新包

    2.修改FR端口、ATM端口、PC端口连接,自定义状况下,没有设置的端口不再显示,防止FRSWITCH、ATMSWITCH连接异常 3.修正若干小BUG 4.集成最新dynamips-0.2.6-RC5 2.2修正BUG&修改功能 1.修正无法计算3660 IDLE值的...

    DynamipsGUI_2.8_CN(附教程)

    2.修改FR端口、ATM端口、PC端口连接,自定义状况下,没有设置的端口不再显示,防止FRSWITCH、ATMSWITCH连接异常 3.修正若干小BUG 4.集成最新dynamips-0.2.6-RC5 2.2修正BUG&修改功能 1.修正无法计算3660 IDLE值的...

Global site tag (gtag.js) - Google Analytics