0


异常处理的使用大全

概述

    异常是指程序在执行的过程中,没有按照预定的流程和逻辑去运行,从而导致数组越界、内存溢出、甚至程序崩溃等各种非正常的情况。在C++、Java和C#等高级语言中,都提供了对于异常的处理机制。异常处理,实际上是一种转移程序控制权的方式。当程序中抛出了异常时,我们可以捕获异常,进而进行相应的处理。处理模型一般有两种:一种是终止模型,表示该异常是致命的,无法恢复,会直接终止程序;另一种是恢复模型,表示该异常是临时的,可恢复的,会尝试去修正错误,确保程序可以继续运行。

C语言中的异常处理

    在介绍C++中的异常处理机制之前,我们先聊一聊C语言中的异常处理机制,这样便于我们更好地理解相关概念。在C语言中,一般有如下几种异常处理方式。

    1、使用断言assert宏。当断言不通过时,会终止程序的执行。注意:assert仅在调试版本(Debug)下有效,在发布版本(Release)下无效。这就意味着,千万不要在assert中编写正常业务流程中会执行的业务代码,因为在发布版本(Release)下,这些业务代码根本不会执行。
void SetText(const char *pszText)
{
    assert(pszText != NULL);
}

SetText(NULL);
    在Debug模式下运行上面的代码后,会直接弹出下面的警告框,提示我们触发了断言,程序已终止。

     继续来看下面的代码。
static const char *g_pszText = NULL;

void SetText(const char *pszText)
{
    assert((g_pszText = pszText) != NULL);

    if (g_pszText == NULL)
    {
        printf("text is null\n");
    } 
    else
    {
        printf("text is %s\n", g_pszText);
    }
}

SetText("CSDN");
    在Debug下运行该程序时,输出为:text is CSDN。但在Release下运行该程序时,输出为:text is null。这是因为,Release下assert断言中的表达式不会被执行,g_pszText没有被赋值,仍是原来的NULL。

    2、使用exit和atexit函数终止程序执行,并捕获异常信息。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        exit(-1);
    }
    
    // ...
    return 0;
}

void ExitFunc()
{
    printf("app exited\n");
}

atexit(ExitFunc);
UpdateNumber(0);
    执行上面的程序后,会输出:app exited,然后程序终止运行。

    3、使用errno这个全局错误码。errno既可以由代码写入,也可以由代码读取。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        errno = 66;
        return -1;
    }

    return 0;
}

int nRet = UpdateNumber(0);
if (nRet != 0)
{
    printf("update error: %d, %d\n", nRet, errno);
}
    执行上面的程序后,会输出:update error: -1, 66。

    4、使用goto。当发生异常时,进行程序逻辑的跳转,只能在一个函数内进行局部跳转。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        goto WRONG;
    }

    return 0;

WRONG:
    return -1;
}

int nRet = UpdateNumber(0);
if (nRet != 0)
{
    printf("update error: %d\n", nRet);
}
    执行上面的程序后,会输出:update error: -1。

    5、使用setjmp和longjmp。这两个函数结合使用时,可以进行非局部跳转。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        goto WRONG;
    }

    return 0;

WRONG:
    return -1;
}

int nRet = UpdateNumber(0);
if (nRet != 0)
{
    printf("update error: %d\n", nRet);
}
    执行上面的程序后,会输出:update error: -1。

    再来看看下面的代码。
static jmp_buf s_jmpBuf;

class CBase
{
public:
    CBase()
    {
        printf("CBase constructor\n");
    }

    ~CBase()
    {
        printf("CBase destructor\n");
    }
};

int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        CBase base;
        longjmp(s_jmpBuf, -1);
    }

    return 0;
}

int nRet = setjmp(s_jmpBuf);
if (nRet == 0)
{
    UpdateNumber(0);
}
else
{
    printf("update error: %d\n", nRet);
}
    在有的编译器下,执行程序后的输出如下。
CBase constructor
CBase destructor
update error: -1
    但在有的编译器下,执行程序后的输出如下。
CBase constructor
update error: -1
    此时,base对象只进入了构造函数,却没有进入析构函数。实际上,对于longjmp跳转前局部对象是否需要析构这一点,C++标准并无明确要求,这取决于编译器的具体实现。

    可以看到,断言assert宏、exit和atexit函数都是直接终止程序的运行,没有提供异常恢复的能力;errno实际上与判断函数返回值没有本质区别,当在多个地方调用同一个函数时,仍需要在每个地方对函数返回值和errno进行判断,会造成代码重复和臃肿;goto会造成逻辑跳转的混乱,且只能进行局部跳转;setjmp和longjmp虽然可以进行非局部跳转,但longjmp后,之前声明的对象实例可能不会调用析构函数释放内存。

    正是由于这些原因,C++中引入了新的异常处理机制。

使用要点

    1、在C++中,使用try、throw和catch三个关键字来处理异常。try用于包含可能会抛出异常的代码段,throw用于抛出某个异常,catch用于捕获异常。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        throw "number is zero\n";
    }

    return 0;
}

try
{
    UpdateNumber(0);
}
catch (const char *pszError)
{
    printf(pszError);
}
    2、在try中声明的变量,外部访问不了,catch中也访问不了。
try
{
    int nData = 66;
    UpdateNumber(0);
}
catch (const char *pszError)
{
    nData == 88;        // 编译错误,访问不了try中声明的变量
    printf(pszError);
}

nData == 99;            // 编译错误,访问不了try中声明的变量
    3、使用throw抛出异常后,throw后面到本函数结束之间的所有代码都不会执行。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        throw "number is zero\n";
        printf("update throw\n");
    }

    printf("update ok\n");
    return 0;
}

try
{
    UpdateNumber(0);
}
catch (const char *pszError)
{
    printf(pszError);
}
    执行上面的代码后,既不会输出:update throw,也不会输出:update ok。

    4、catch可以有多个,但至少要有一个。有多个catch时,具体进入哪个catch,要看throw抛出的异常类型与哪个catch的异常类型最匹配。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        throw "number is zero\n";
    }

    return 0;
}

try
{
    UpdateNumber(0);
}
catch (int nError)
{
    printf("error is %d\n", nError);
}
catch (const char *pszError)
{
    printf(pszError);
}
    执行上面的代码后,输出:number is zero。虽然int类型的catch写在了前面,但与throw抛出的字符串类型并不匹配,故最终还是进入了字符串类型的catch。

    5、想捕获所有异常,可以直接使用catch(...)。
try
{
    UpdateNumber(0);
}
catch (...)
{
    printf("exception caught\n");
}
    6、catch进行类型匹配时,不会发生隐式类型转换。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        short sTemp = 66;
        throw sTemp;
    }

    return 0;
}

try
{
    UpdateNumber(0);
}
catch (int nError)
{
    printf("error is int\n");
}
catch (short sError)
{
    printf("error is short\n");
}
    执行上面的代码后,输出:error is short。这是因为,throw抛出的数据类型short在与catch匹配时,并不会隐式类型转换为int。

    7、throw中抛出的异常类型是对象时,可以是对象的值,也可以是指向对象的指针。对应的,catch中匹配的,可以是对象的引用,也可以是指向对象的指针。
class CBase
{
public:
    const char *What()
    {
        return "base exception";
    }
};

int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        throw CBase();
    }

    if (nNumber == 1)
    {
        throw new CBase();
    }

    return 0;
}

try
{
    UpdateNumber(0);
}
catch (CBase &base)
{
    // 捕获对象的引用
    printf("error is base: %s\n", base.What());
}

try
{
    UpdateNumber(1);
}
catch (CBase *pBase)
{
    // 捕获指向对象的指针
    printf("error is pBase: %s\n", pBase->What());
    delete pBase;
}
    8、throw抛出的异常类型是派生类对象时,catch可以用基类对象捕获。
class CBase
{
public:
    const char *What()
    {
        return "base exception";
    }
};

class CDerived : public CBase
{
};

int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        throw CDerived();
    }

    if (nNumber == 1)
    {
        throw new CDerived();
    }

    return 0;
}

try
{
    UpdateNumber(0);
}
catch (CBase &base)
{
    // 可正常捕获派生类的异常
    printf("error is base: %s\n", base.What());
}

try
{
    UpdateNumber(1);
}
catch (CBase *pBase)
{
    // 可正常捕获派生类的异常
    printf("error is pBase: %s\n", pBase->What());
    delete pBase;
}
    9、throw抛出异常后,可以在catch中进行处理并继续抛出异常。
int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        throw 66;
    }

    return 0;
}

void Test()
{
    try
    {
        UpdateNumber(0);
    }
    catch (int nError)
    {
        printf("test error is: %d\n", nError);
        throw;        // 继续抛出异常
    }
}

try
{
    Test();
}
catch (int nError)
{
    printf("main error is: %d\n", nError);
}
    执行上面的代码后,输出如下。
test error is: 66
main error is: 66
    10、抛出异常后,try和throw之间所有的局部对象都会自动调用析构函数。
class CBase
{
public:
    CBase(int nData) : m_nData(nData)
    {
        printf("CBase constructor: %d\n", nData);
    }

    ~CBase()
    {
        printf("CBase destructor: %d\n", m_nData);
    }

private:
    int m_nData;
};

int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        CBase base(88);
        throw 1;
    }

    return 0;
}

try
{
    CBase base(66);
    UpdateNumber(0);
}
catch (int nError)
{
    printf("error is: %d\n", nError);
}
    执行上面的代码后,输出如下。
CBase constructor: 66
CBase constructor: 88
CBase destructor: 88
CBase destructor: 66
error is: 1
    可以看到,throw之前声明的局部变量均被正常析构了。

    11、异常被抛出后,会沿着调用堆栈一直查找匹配的catch。如果直到main函数都没有被catch,则会导致程序自动调用terminate函数而终止运行。

    12、可以在声明和定义函数时,为函数添加异常声明列表,用于指明该函数可能会抛出哪些异常,以增强程序的可读性。异常声明列表可以在声明函数时添加,也可以在定义函数时添加,如果两处都添加,则需要保证一致。如果一个函数不抛出任何异常,则可以用throw()来表示。如果不添加异常声明列表,则表示可以抛出任何异常。
// 可抛出int和const char *类型的异常
int UpdateNumber(int nNumber) throw(int, const char *)
{
    if (nNumber == 0)
    {
        throw 66;
    }

    return 0;
}

// 不抛出异常
void Test() throw()
{
    printf("test func\n");
}

// 可抛出任何异常
void Test2()
{
    printf("test2 func\n");
}
    在C++ 11中,取消了对异常声明列表的支持,并添加了noexcept,用于表明函数是否会抛出异常。
// 会抛出异常
int UpdateNumber(int nNumber) noexcept(false)
{
    if (nNumber == 0)
    {
        throw 66;
    }

    return 0;
}

// 不会抛出异常
void Test() noexcept(true)
{
    printf("test func\n");
}
    13、C++中已经定义了很多标准异常,这些标准异常都是从std::exception类派生的,具体可以参考下面的表格。

异常基类

异常派生类

描述

std::bad_alloc

--

该异常可以通过new抛出

std::bad_cast

--

该异常可以通过dynamic_cast抛出

std::bad_typeid

--

该异常可以通过typeid抛出

std::bad_exception

--

表示无法预期的异常

std::logic_error

--

理论上可以通过读取代码来检测到的异常

--

std::domain_error

当使用了一个无效的数学域时,会抛出该异常

--

std::invalid_argument

当使用了无效的参数时,会抛出该异常

--

std::length_error

当创建了太长的std::string时,会抛出该异常

--

std::out_of_range

当访问std::vector的元素越界时,会抛出该异常

std::runtime_error

--

理论上不可以通过读取代码来检测到的异常

--

std::overflow_error

当发生数学上溢时,会抛出该异常

--

std::range_error

当尝试存储超出范围的值时,会抛出该异常

--

std::underflow_error

当发生数学下溢时,会抛出该异常

    下面的代码列出了一些会抛出标准异常的情形。
try
{
    char *pData = new char[0x7FFFFFFF];
}
catch (std::bad_alloc &e)
{
    printf("%s\n", e.what());
}

try
{
    std::vector<int> vctNumber(5);
    vctNumber.push_back(66);
    int nNumber = vctNumber.at(9);
}
catch (std::out_of_range &e)
{
    printf("%s\n", e.what());
}
    执行上面的代码后,输出如下。
bad allocation
invalid vector subscript
    14、除了使用标准异常,我们也可以自定义异常处理类。
class CMyException : public std::exception
{
public:
    CMyException(int nError, const char *pszError) : std::exception(pszError), m_nError(nError)
    {
        NULL;
    }

    int GetErrorCode() const
    {
        return m_nError;
    }

private:
    int m_nError;
};

int UpdateNumber(int nNumber)
{
    if (nNumber == 0)
    {
        throw CMyException(66, "number is zero");
    }

    return 0;
}

try
{
    UpdateNumber(0);
}
catch (CMyException &me)
{
    printf("error is: %d, %s\n", me.GetErrorCode(), me.what());
}
    执行上面的代码后,输出为:error is: 66, number is zero。

    15、尽量不要在构造函数中抛出异常,因为此时不会调用析构函数,可能会导致内存泄漏和资源未释放。
class CBase
{
public:
    CBase() : m_pData(NULL)
    {
        printf("base constructor\n");
        m_pData = new char[66];
        throw 1;
    }

    ~CBase()
    {
        delete[] m_pData;
        m_pData = NULL;
        printf("base destructor\n");
    }

private:
    char *m_pData;
};

try
{
    CBase base;
}
catch (...)
{
    printf("exception occured\n");
}
    上面的代码执行后,输出如下。
base constructor
exception occured
    可以看到,base对象没有调用析构函数,从而导致构造函数中申请的内存m_pData没有被释放。
标签: c++ windows c语言

本文转载自: https://blog.csdn.net/hope_wisdom/article/details/129648504
版权归原作者 hope_wisdom 所有, 如有侵权,请联系我们删除。

“异常处理的使用大全”的评论:

还没有评论