【C++】异常

一、为什么要有异常?

在C语言中, 一般遇到错误会返回错误码, 而不同的错误码又对应不同的错误信息, 通过对应关系可以根据错误码得出错误的大致原因, 但是这还是有点局限了, 因为返回的信息有限, 并不能很好的反馈出错误信息, 而且有的情况下是无法通过返回值来返回错误码的, 比如:

T& func()
{
	//do something...
}

遇到此种情况, 是无法返回错误码的. 虽然在C语言中可以通过设置 assert 来解决遇到错误的情况, 但是 assert 有点太暴力了, 它会直接终止进程, 而且 assert 在 Debug 模式下有效, 而在 Release 模式下是无效的.

而异常可以抛出的错误类型就可以很多变了, 字符串、对象等, 可以携带更多有用的错误信息, 能够更有效的提高解决问题的效率.

二、语法规则

通过 throw 可以抛出异常, 再使用:

try
{}
catch(Exception type)
{}

来捕捉并处理异常, try 与 catch 必须配套使用, catch 可以有多个:

try
{}
catch(Exception type)
{}
catch(Exception type)
{}
...

并且 throw 抛出异常后一定要捕获处理, 如果抛出异常后没有捕获会报错, 如下:

在这里插入图片描述
而关于异常的执行流是这样的, 抛出异常后的语句都不会执行, 会直接跳过, 比如:

void func1(int n)
{
    if (n == 0)
    {
        throw "exception";
    }

    cout << "void func1(int n)" << endl;
    return;
}

void func2()
{
    int n;
    cin >> n;
    func1(n);

    cout << "void func2()" << endl;
    return;
}

int main()
{
    try
    {
        func2();
    }
    catch(const char* e)
    {
        cout << e << endl;
    }
    catch (...)
    {
        cout << "Unknown Exception" << endl;
    }
	
	cout << "int main()" << endl;
    return 0;
}

在func1中 throw 抛出异常后后面的语句不会执行, 且不会返回被调用的func2处, 而是直接跳到 main 函数中匹配抛出异常类型的 catch 语句中, 且只会执行一个 catch 语句, 与 if-else-else if 相似, 执行完 catch 语句后, 后面的语句会正常执行, 比如 " cout << “int main()” << endl; " 这句会被正常执行.

而对于抛出异常的捕获规则, 遵循就近原则, 上述例子中, func2 抛出异常, 如果调用其的 func1 写了 try-catch 语句则由其捕获, 如果没有则继续由调用 func1 的函数来处理, 就这样层层往回的处理, 如果直到 main 函数都没有捕获处理的话, 编译器会直接报错.

而对于抛出的异常, 必须由和异常类型一致的 catch 语句来捕捉, 否则会报错, 比如:

void func1(int n)
{
    if (n == 0)
    {
        throw "exception";
    }

    cout << "void func1(int n)" << endl;
    return;
}

void func2()
{
    int n;
    cin >> n;
    func1(n);

    cout << "void func2()" << endl;
    return;
}

int main()
{
    try
    {
        func2();
    }
    catch(int e)
    {
        cout << e << endl;
    }

    cout << "int main()" << endl;
    return 0;
}

这里抛出的异常类型是 const char*, 而只有一个 catch 且捕获类型为 int, 所以这里会直接报错:
在这里插入图片描述
所以要注意捕获异常类型与抛出异常类型的一致.

三、使用技巧

捕获异常类型与抛出异常类型可以一致, 也可以是父子类关系, 即抛出子类对象的异常, 可以由父类对象来接收该异常, 比如:

class Exception
{
public:
    Exception(int errid, string errmsg)
        :_errid(errid)
        , _errmsg(errmsg)
    {}

    virtual void what() const
    {
        cout << _errid << ":" << _errmsg << endl;
    }

protected:
    int _errid;
    string _errmsg;
};

class A_Exception : public Exception
{
public:
    A_Exception(int errid, string errmsg, string amsg)
        :Exception(errid, errmsg)
        , _amsg(amsg)
    {}

    virtual void what() const
    {
        cout << _errid << ":" << _errmsg << ":" << _amsg << endl;
    }

private:
    string _amsg;
};

class B_Exception : public Exception
{
public:
    B_Exception(int errid, string errmsg, string bmsg)
        :Exception(errid, errmsg)
        , _bmsg(bmsg)
    {}

    virtual void what() const
    {
        cout << _errid << ":" << _errmsg << ":" << _bmsg << endl;
    }

private:
    string _bmsg;
};

void func1(int n)
{
    if (n == 0)
    {
        throw A_Exception(1, "Exception", "A_Exception");
    }

    cout << "void func1(int n)" << endl;
    return;
}

void func2()
{
    int n;
    cin >> n;
    func1(n);

    cout << "void func2()" << endl;
    return;
}

int main()
{
    try
    {
        func2();
    }
    catch(const Exception& e)
    { 
        e.what();
    }

    cout << "int main()" << endl;
    return 0;
}

通过父类的指针或引用来接收子类异常的抛出, 这样可以大大规范乱抛异常不好捕获的情况, 也体现了OO语言多态的特性, 那么来看一个问题:

int main()
{
    try
    {
        func2();
    }
    catch(const Exception& e)
    { 
        e.what();
    }
    catch (const A_Exception& e)
    {
        e.what();
    }

    cout << "int main()" << endl;
    return 0;
}

此时抛出异常的类型为 A_Exception, 那么此时会进入哪个 catch 语句呢? 按照以往的理解, 应该会匹配类型最合适的那个, 但这里进入的是 catch(const Exception& e) 这个语句, 如果存在父类和自己同一类型的 catch, 依然是遵循就近原则, 如果此时把 catch(const Exception& e) 和 catch (const A_Exception& e) 的先后位置调换以下, 就换进入 catch (const A_Exception& e) 语句.

有时候会面临着对方抛出的异常不是属于自定义异常的体系中, 我们也不清楚对方抛出了什么类型, 那么此时就可以用以下方式来捕获这种不清楚类型的异常:

catch(...)
{}

这种写法就表示捕获任意类型的异常, 一般写在所有 catch 语句的最后, 用来兜底.

四、异常安全

异常的执行流是很跳跃的, C语言错误码的返回都是一层一层的, 而异常的这种跳跃性, 就很容易的引起内存泄漏的问题, 比如:

void func1(int n)
{
    if (n == 0)
    {
        throw "Exception";
    }

    cout << "void func1(int n)" << endl;
    return;
}

void func2()
{
    int n;
    cin >> n;

    int* a = new int;
    func1(n);

    delete a;
    cout << "delete a" << endl;
    cout << "void func2()" << endl;
    return;
}

int main()
{
    try
    {
        func2();
    }
    catch(const char* e)
    { 
        cout << "Exception" << endl;
    }
    catch (...)
    {
        cout << "Unknown Exception" << endl;
    }

    cout << "int main()" << endl;
    return 0;
}

向上面这种情况, 异常抛出后直接跳到了 main 函数的 catch 语句中, 那么此时的 a 就没有得到释放, 造成了内存泄漏.

解决这种问题的办法由两种:

  • 异常的重新抛出: 在申请有空间的函数内, 我们可以捕获抛出的异常, 然后在 catch 语句中先把该函数申请的空间给释放了, 再将捕获到的异常重新抛出(一般来说都由最外层来处理异常).
void func2()
{
    int n;
    cin >> n;

    int* a = new int;

    try
    {
        func1(n);
    }
    catch(...)
    {
        delete a;
        cout << "delete a" << endl;
        throw;
    }
    cout << "void func2()" << endl;
    return;
}
  • 使用智能指针来管理 new 出来的对象.

另外, 最好不要在构造函数和析构函数中抛出异常, 因为如果在构造函数中抛出异常, 可能导致对象不完整或者没有被完全初始化, 而在析构函数中抛出异常, 可能导致资源没有被全部释放的情况.

五、相关规范

在C++98中提供了一个有关某函数是否会抛出异常的规范, 如果某函数不会抛出异常, 则在该函数后面加上 throw(), 比如:

void func1(int n) throw()
{
    //do something...
}

如果某函数会抛出异常, 则在该函数后面加上 throw(A, B, C), 表示该函数可能抛出 A, B, C 三种异常.

这种写法不算太好, 存在混淆的情况, throw() 会让人误以为也会抛出异常, 所以在C++11中新增了 noexcept 关键字, 比如:

void func1(int n) noexcept
{
    //do something...
}

在函数的后面加上该关键字就表示该函数不会抛出异常, 反之就表示会抛出异常, 这就很浅显易懂了.

在 Microsoft Visual Studio Community 2019 版本 16.11.3 IDE下, 如果某函数加上了 throw() 或 noexcept 后仍抛出异常, 编译器会直接报错.

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值