65.9K
CodeProject 正在变化。 阅读更多。
Home

异常处理的基础

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (11投票s)

2006年12月7日

13分钟阅读

viewsIcon

43464

downloadIcon

528

本文讲述了如何优雅地跟踪和处理程序中的 bug。让你完美理解异常和 bug 产生的原因。

引言

在本文中,我将描述如何在不干扰复杂代码的情况下准确地处理错误。大多数程序员并不按照应有的方式使用错误处理,而且我们还忽略了需要维护的错误日志。为什么我们需要这些东西?我将作为一名新手来解释这一点。也许有更多关于此主题的优秀文章,但本文的目的是让初学者理解为什么、如何以及何时使用错误处理和日志,并结合不同的场景进行讲解。

注意:- 英语不是我的母语,如果我说错了什么,请多多包涵。

背景

当我们在 C++ 中编写代码时,有时会遇到诸如程序异常终止、某些函数失败、代码损坏等错误。没有哪个错误是无法跟踪和解决的,但我们程序员有时仅仅因为懒惰或害怕代码复杂性而不用这些功能。

C++ 是一种非常强大的语言,它通过异常处理技术(如使用 trycatchthrow)提供了出色的错误跟踪和解决方案。但是,使用 try-catch 是一种艺术,并非需要在每一段代码上都加上 try-catch,这会影响速度并使代码难以理解。

下面是一张图,描述了 try-catch 方法的目的

在这里,我们有一个场景:一个男孩在一个安全的游乐场玩耍,旁边有一辆救护车。此外,男孩还有一个警报器,如果他受伤了可以帮助他,然后救护车肯定会将他送往医院。这就是使用 try-catch-throw 机制时发生的情况。我敢肯定您会想我在这张图上画了什么鬼东西。好吧,我将在这里解释。男孩实际上是一个在 try 的大括号内的代码,这实际上就像一个安全的游乐场。假设我们的代码突然变得不稳定,然后出乎意料地发现了一些错误,那么在做任何事情之前,它只会 throw(男孩会按下警报按钮),然后 catch 会捕获错误(或者只是将男孩移到救护车里让他继续玩)。下面是展示我们如何做到这一点的代码示例

void main void()
{

    try
    //this provide the protected playground 

    //where ambulance is reside

    {
        //this is a boy which was playing 

        Somefunction()
        Somefunction2();    
        Somefunction3();    
    }
    catch(somerror err)
    //this is our embulance

    {
        //here we do operation 

    }

}

void Somefunction2()
{
    //do some code processes

    //suddenly code got wrong value suppose value came

    //in minue while you need the unsigned numbers


    If(data<0)
        throw  someerrorobject;
}

这里,代码在 try 块内。在第二个函数中,如果发生意外情况,在 Somefunction2 完成之前,主代码会获取错误并将其抛出给该函数的调用者。抛出者会认为您肯定在其函数调用周围定义了安全的 try 块,否则将显示类似下面的错误。之后,代码将直接进入其适当的 catch 块。

使用代码

Throw 是一种功能,可以根据编码器的要求将代码从其函数作用域中移出。假设我调用了某种 Windows API 函数,它返回了一个无效值,那么在继续前进时,我只需检查返回类型并根据我的要求抛出错误。

处理 Win API 函数

假设我调用 RegOpenKey,成功时它将返回 ERROR_SUCCESS,否则将返回其他值。现在我们来看一下这段代码……

void  Function1()
{
    
    If( RegOpenKey(………,…….,…..) ==ERROR_SUCCESS)
    {
        //    good code 

    }
    else
    {
        throw “ RegOpenKey failed”;
    }

    //.some other code work ………………………………

    // some other code work ………………………………


}


void main(void)
{
    try
    {
        Function1();
    }
    catch(char * errorword)
    {
        cout<< errorword;
    }

}

上面的示例将在屏幕上显示此消息:“RegOpenKey 失败”。现在您可以看到我们可以跟踪任何我们想要的错误。我们必须抛出那些我们知道可能导致问题的事务。我为什么要做 catch (char* errorword),因为在我的程序中,我期望抛出的错误代码或错误字是字符字符串或数组,这就是为什么我捕获 char* 这个术语。

处理多个错误

在上面的示例中,我们假设所有抛出的数据都将是 char*,但如果您想抛出数字和 char* 怎么办?那么我们只需根据特定类型追加另一个 catch 选项。

void function()
{
    try
    {
        //we assume that its thrower throws the char* 

        FunctionA()
        // here we assume that its thrower is integer, 

        //and its throws suppose any invalid 

        //id that occour in program

        FunctionB()
    }
    catch(char* data)
    {
         // error words according to the user to make him 

         // understand what he was doing

         // and why it had to be thrown

         cout<<data;
    }
    catch(int id)
    {
         cout<<id; //wrong id we found

    }

处理自定义错误对象

在之前的示例中,我们知道要捕获的数据类型,但是如果我们调用不同的 Windows API 或其他 SDK API,它们会抛出一些自定义对象怎么办?对此,我只是建议阅读他们的帮助或 MSDN。有时,我们创建自己的类,而该类就是为了错误处理而创建的。让我们创建一个示例错误处理类来阐述我的意思。

Class ErrorHandle()
{
    Public:
    String m_FunctionName;
    String m_ReasonofErr;
    Int Error_id;

    ErrorHandle(string strReason,strFunction,int d);
    {
            m_FunctionName= strFunction;
            m_ReasonofErr =  strReason;
            Error_id = d;
    }        
    ~ErrorHandle();

}

在这里,我们有了简单的 ErrorHandler 类。现在,而不是从我们这边抛出任何其他数据,我们将使用这个对象来抛出。

int MyfunctionDivision(int data)
{
    int return=1;
    
    if(data<=0)
    {    
       ErrorHandle obj("less than 1 cant be use for to divide numbers",
                       " MyfunctionDivision",data);

       throw  obj; //this is our error handler object

    }
    else
    {
        //……………………………………………………….

    }

}

void main(void)
{
    try
    {
        MyfunctionDivision(-1);
    }
    catch(ErrorHandle obj)
    {

        cout<<”Function Name:”<<obj. m_FunctionName;
        cout<<”Reason of Error”<<obj.m_ ReasonofErr;
        cout<< “Error data:”<<obj. Error_id;

    }
}

这个自定义对象包含很多细节,例如函数名称以及原因,还有导致错误的那些数据。因此,这个自定义类可以包含函数和属性,并且可以根据需要进行扩展。这种方法是捕获错误推荐的方法。当然,您并不总是需要函数名,但需要函数名,而且如果您知道错误发生的原因,那就更好了。现在,我只是检查 Error_Id 的值就能理解,我错误地获取了无效数据,所以代码才崩溃。您现在可以看到处理这些愚蠢的错误有多么简单。但请记住,捕获器的类型应该与抛出器的类型相同。假设在最近的示例中,您抛出了一个整数错误 ID,除非您专门定义了一个单 ID 捕获器,否则它将捕获不到任何内容。

catch(ErrorHandle obj)
{
    cout<<”Function Name:”<<obj. m_FunctionName;
    cout<<”Reason of Error”<<obj.m_ ReasonofErr;
    cout<< “Error data:”<<obj. Error_id;
}
catch(int id_anotherCodeeror) //single id catcher

{
    Cout<< id_anotherCodeeror;   
    /*now if any function in the try block just throw 
      a single integer instead of  “ErrorHandle” 
      object then it will be catch ea
      sily without any crash or problem*/
}

处理意外或未知错误

上面的示例都是我们知道将要面对的错误类型的情况,无论是整数、字符串还是任何自定义错误处理类。对于意外类型的错误,我们必须使用“…”语句来捕获。简而言之,这三个点是为了捕获发生的任何类型的错误,但它们不会显示任何错误描述,因为我们不知道发生了什么类型的错误。通过这个语句,我们可以理解,我们的程序内部有什么东西正在崩溃,而且它一定是某种我们还不知道的类型。

void main(void)
{
    try
    {
        MyfunctionDivision(-1);
    }
    catch(ErrorHandle obj)
    {
        cout<<”Function Name:”<<obj. m_FunctionName;
        cout<<”Reason of Error”<<obj.m_ ReasonofErr;
        cout<< “Error data:”<<obj. Error_id;
    }
}
catch(ErrorHandle obj)
{
        cout<<”Function Name:”<<obj. m_FunctionName;
        cout<<”Reason of Error”<<obj.m_ ReasonofErr;
        cout<< “Error data:”<<obj. Error_id;
}
catch(int id_anotherCodeeror) //single id catcher

{
    cout<< id_anotherCodeeror;
    /*now if any function in the try block just 
      throw a single integer instead of  “ErrorHandle” 
      object then it will be catch easily 
      without any crash or problem*/
}
catch(…)
{
    cout<<”unexpected error came”;
}

现在,这段代码可以处理两种类型的错误。如果出现 ErrorHandle 对象,ErrorObject 处理程序将对其进行处理。如果函数抛出整数,那么具有整数类型的第二个捕获器将对其进行处理。但是,如果 try 块中的任何函数抛出的不是 ErrorHandle 或整数,那么最后的 catch 将显示消息:“发生了意外错误”。这就是我们如何跟踪意外错误,然后逐一调试它们以消除不良错误。

处理 ATL/COM 错误

在 Windows 编程中,使用 ATL/COM 等并非寻常。通常,我们在使用 COM 对象时会遇到意外错误。假设我正在使用 Microsoft ADO 并为一个 MDB 文件初始化数据库连接。

_ConnectionPtr   g_pConPtr;
_CommandPtr      g_pCPtr;
_RecordsetPtr    g_pRPtr;

int OpenConnection()
{
    
  string Path ;  
    
  Path = "DB\\mydatabasefile.mdb";

  string CnnStr = "PROVIDER=Microsoft.Jet.OLEDB.4.0; DATA SOURCE=";
    
  CnnStr+=Path;    
    
  CnnStr+=";USER ID=admin;PASSWORD=;Jet OleDB:Database Password = abcdefgh;";
    
  CoInitialize(NULL);

  try
  {
    g_pConPtr.CreateInstance(__uuidof(Connection));
    
    if(SUCCEEDED(g_pConPtr ->Open(CnnStr.c_str(),"","",0)))
    {
        g_pRPtr.CreateInstance(__uuidof(Recordset));
        g_pCPtr.CreateInstance(__uuidof(Command));
        g_pCPtr ->ActiveConnection = g_pConPtr;
    }
  }
  catch (...)
    {
        return 0;
  }

return 1;
}

如果连接字符串无效,或者初始化未被调用,或者由于任何可能的 COM 错误(可能由于我们的错误使用而发生),上述代码都会崩溃。假设连接字符串 CnnStr 的路径不正确,那么 COM 错误将在其函数调用中被抛出。它会直接跳转到 catch 块。但我们仍然不知道错误是什么,为什么会发生错误。为了理解 COM 错误,Microsoft 提供了一个非常好的类,名为 _com_error。这个类有很多很酷的成员函数,如 ErrorMessage()Description(),它们可以轻松解决我们的 COM 错误问题,而无需使事情变得更复杂。

现在让我们添加一个 catch 来处理所有类型的 COM 错误

try
{
    g_pConPtr.CreateInstance(__uuidof(Connection));
    

    if(SUCCEEDED(g_pConPtr ->Open(CnnStr.c_str(),"","",0)))
    {
        g_pRPtr.CreateInstance(__uuidof(Recordset));
        g_pCPtr.CreateInstance(__uuidof(Command));
        g_pCPtr ->ActiveConnection = g_pConPtr;
    }
}
catch(_com_error &e)  
{
    string st = e.Description();
    cout<<st;
}
catch (...)
{
    return 0;
}

这次,如果发生错误,程序不会默默退出,而是会进入 _com_errorcatch 块,并显示:“'F:\.........\DB\ mydatabasefile.mdb' is not a valid path. Make sure that the path name is spelled correctly and that you are connected to the server on which the file resides.” Microsoft 类就是这样描述错误的。非常简单准确,不是吗?

处理 C++ STL 异常

就像 Microsoft 的 _com_error 一样,STL 中有一个名为 exception 的自定义类,它可以包含在 exception.h 中。下面是一个示例,展示了如何处理任何类型的 STL 错误,包括 vector、string、map 等的错误。

#include <iostream>


#include <vector>


#include <algorithm>


#include <iterator>


#include <exception>


using namespace std;


int main () throw (exception)
{

    vector<int> v(5);

    fill(v.begin(),v.end(),1);

    copy(v.begin(),v.end(),

    ostream_iterator<int>(cout," "));

    cout << endl;

    try
    {
        for ( int i=0; i<10; i++ )
            cout << v.at(i) << " ";

        cout << endl;
    }
    catch( exception& e )
    {
        cout << endl << "Exception: "<< e.what() << endl;
    }
    cout << "End of program" << endl;

    return 0;}

OUTPUT
// 1 1 1 1 1
// 1 1 1 1 1
// Exception: invalid vector subscript
// End of program 

Exception 类函数“what()”包含最近捕获的错误的描述。

忽略错误

有时 catch(…) 也用于忽略运行时错误或意外错误。这是一个糟糕的主意,但有时变得有必要使用,这完全取决于程序员的代码和逻辑。

嵌套抛出

在这里,我将展示一个嵌套函数抛出错误的示例,以及主调用者如何捕获错误并显示它们。我们这里有两个类,一个是 Sample1,另一个是 Sample2Sameple1 有一个函数 DoSomeWork()。而 Sample2 有四个成员函数。

class Sample2  
{
    
public:
    Sample2();
    virtual ~Sample2();

    void AddMoreData(int dt);
private:
    void Function3(int);
    void Function1(int);
    void Function2(int);

};

class Sample1  
{
public:
    Sample1();
    virtual ~Sample1();
    void DoSomeWork();

};

在这里,我们必须实现嵌套调用和 bug 抛出。为此,我创建了 AddMoreData(int),它将调用其私有成员函数 Function1(int)

void Sample2::AddMoreData(int dt)
{
    if(dt<1)
        throw "[AddMoreData](Invalid argument given)";

    Function1(5);

}

void Sample2::Function1(int d)
{
    if(d>10)
        throw "problem in function 1";

    Function2(4);
}

void Sample2::Function2(int d)
{
    if(d<5)
        throw "problem in function 2";

    Function3(133);
}

void Sample2::Function3(int d)
{
    if(d>100)
        throw "problem in function 3";
}

//this is other class function not the Sample2

void Sample1::DoSomeWork()
{
    Sample2 Obj;

    Obj.AddMoreData(2);
}

正如您所见,Sample2 中的主调用者是 AddMoreData,它实际调用 Function1,而 Function1() 调用 Function2()Function2() 调用 Function3()。在主函数中,让我们声明 Sample1 的对象。在我们的 try-catch 边界内

void main(void)
{
    Sample1 Obj;

    try
    {

        Obj.DoSomeWork();
    }
    catch(char* data)
    {
        cout<<data;
    }
    catch(...)
    {
        cout<<"unexpected Error";
    }

}

运行上述代码后,Sample1 对象将调用函数 DoSomeWork,在该函数内,它实际上调用 Sample2 的函数 AddMoreData(),该函数具有嵌套函数调用。现在,如果在任何地方发生错误,它将被抛出,退出所有函数而不完成它们,并显示结果。上述示例将显示“function 2 中有问题”,如果问题已解决,则必须调用 function3。因此,我们在这里看到,无论函数调用有多深,从它们中抛出错误并捕获到第一个函数被调用的第一层是没有问题的。

注意:- 如果我没能很好地解释这一点,请直接复制粘贴并尝试运行程序,然后逐一更改 Function1Function2Function3 的参数值。

多重抛出

多重抛出是一种技巧,用于在两个或多个 try-catch 边界之间抛出任何特定的错误。如果编码器需要在让作用域退出函数调用之前执行某些操作,则可以做到这一点。现在,让我们假设我们有两个函数,function1()function2()Function1() 应该调用 function2(),但 function1 应该捕获 function2() 中发生的任何错误,并在捕获后,执行自己的命令,然后再次将该错误抛出给主调用者。

void function2();
void function1()
{
    try
    {
        function2();
    }
    catch(char* errorDesc)
    {
        //do some work before letting the scope of function1 destroy

        throw errorDesc;
    }

}

void function2()
{
    throw "we have got problem inside our second function";
}


void main(void)
{
    try
    {
        function1();
    }
    catch(char* data)
    {
        cout<<data;
    }
    catch(...)
    {
        cout<<"unexpected Error";
    }
}

上述示例的输出将是“我们在第二个函数中遇到了问题”。因此,您可以看到 function1 捕获了 function2 的错误,然后在清理内存作用域之前执行其最终工作,然后再次将相同的错误抛出给父调用者 Main 函数。这就是多次抛出多个错误的方法。

处理窗口句柄

在这里,我们将停止只讨论 try-catch 理论,并将向您展示如何处理基本的 Win32 窗口句柄问题。要记住的事情……

  • 始终关闭您打开的句柄,例如注册表句柄、进程句柄等。
  • 不要尝试关闭不属于您的线程的句柄,例如,如果您获得了任何特定的桌面窗口句柄,那么关闭其句柄就不是您的责任,因为它仍然有所有者。
  • 在声明任何句柄时,以及在关闭任何句柄或完成任何句柄的作用域后,请始终使用 NULL
  • 在对其进行进一步操作之前,请始终检查句柄的内部值。
  • 如果您正在处理不属于您自己的线程或窗口的句柄,请务必检查它是否仍然存在。
HWND hd =NULL; //initialize it as null

//some api function returns the handle of something

Hd =  SomeApiFunctionReturnsHWND(........);
if(hd) 
{
    //do some work by that hwnd 

}

//Or for more care about handle do  this

if(hd)
{
  if(IsWindow(hd))
  {
        cout<< “yes window exist so lets work with it”
  }
}

IsWindow(HWND) 是一个 Win32 API 函数,如果当前的 HWND 窗口当前存在,该函数将返回 TRUE,否则将返回 FALSE。但要小心使用此 API 函数,因为窗口句柄会在窗口销毁时被回收,否则旧句柄甚至可能指向另一个新窗口。

使用 SendMessageTimeOut 而不是 SendMessage

发送消息时,请始终使用超时,因为如果目标窗口或类处于线程阻塞状态,简单的 SendMessage 可能会导致程序挂起。但是,如果 SendMessageTimeOut API 在规定的时间内未收到结果,它将从消息 API 返回到当前程序流。有关更多信息,请查看 MSDN

错误日志记录器

错误日志是 bug 跟踪的另一个部分,有助于以报告的形式更准确地理解结果。只需创建一个具有错误日志成员函数的类,如打开文件、关闭文件、向文件写入数据,并在错误日志的构造函数中调用打开日志文件的函数。

  • 将此对象声明为静态对象,放在一个空的头文件(如 StdAfx.h)或您自己定义的文件中。
  • 现在,使用 stdafx.h 头文件或您为静态全局对象创建的头文件包含所有 .cpp 文件。
  • 现在,在每个 try-catch 块中,只需通过静态错误日志对象调用“写入日志文件”函数。
#include “mystdfx.h”

//it has ErrorLog statis object “g_ErrorLogObj”


void SomeClass::SoemFunction()
{
    try
    {
        function1();
    }
    catch(char* data)
    {
        g_ErrorLogObj.writeLog(data);
    }
    catch(...)
    {
        g_ErrorLogObj.writeLog(“unexpected error”);
    }
}

编译器宏用于错误日志

写入文件可能会导致速度损失,所以我通常将错误日志放在调试模式下,并在发布模式下禁用错误日志。这样,就不会发生流式传输,而且编译器根本不会读取所有写入代码。为此,让我们创建自定义宏,这个朋友将帮助我们实现我所说的。

  1. 定义一个宏 #define MyErrorLogStart
  2. 现在,在 ErrorLog 类的 writeLog 函数内部,只需放置宏条件,例如
Void ErrorLog:writeLog(string str)
{

#ifdef MyErrorLogStart
 ……………….Writing inside the FILE or by stl streaming way 
 ………………other log processes
#endif

}

现在假设如果我不想写入日志,那么我只需从我的头文件中删除 MyErrorLogStart 的定义。否则,我将只使用此头文件让编译器知道必须使用写入函数和错误日志的处理过程。您还可以在打开日志文件时放置这些宏,这样在没有 MyErrorLogStart 的情况下,它永远不会打开文件。

使用流程日志处理关键情况

流程日志是一种通过各种描述(包括其变量值)以报告形式理解程序输入输出的方法。只需使用典型的错误日志,在函数开头、函数结尾或函数中间,无论您觉得哪里需要使用。

void Myfunction()
{
    g_ErrorLogObj.writeLog(“Inside Myfunction()”);

    ………… different functional processes 


    g_ErrorLogObj.writeLog(“Outside Myfunction()”);

} 

GetLastError()

这个 Win32 函数可以帮助提取程序的错误描述,它只返回 DWORD,其值可以通过 FormatMessage API 函数进行转换。

结论

总而言之,在本文中,我的想法是给出一些技巧,并告诉初学者如何编写没有错误的程序。try-catch 是编写安全代码的最佳方法,而错误报告是捕获和消除所有类型错误的好方法。我遵循以上技巧已有两年,并发现它们在理解任何程序的难题和尽可能多地消除 bug 方面非常有效。希望我的这点知识能在未来帮助到其他人。

© . All rights reserved.