【C++】异常

目录

一、C语言传统的异常处理

二、C++异常概念

1、基本概念

2、异常的抛出和匹配原则

3、函数调用链中异常栈展开匹配原则

三、异常的使用

1、基本用法

2、重新抛出

四、异常的规范

五、异常的优缺点

1、异常的优点

2、异常的缺点

总结


前言

C++语言能够解决的问题规模千变万化,有的小到一个程序员几小时能够完成,有的是含有几千几万行代码的庞大系统,需要几百个程序员协同工作好几年,当我们编写比较复杂的管理系统时,异常就十分的有用


一、C语言传统的异常处理

C语言的异常处理机制有很多的缺点

1. 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
并且assert只有在debug版本下才会有用,也就是说需要我们在没有发布之前就找到所有的错误这
是不可能的事情,并且处理措施也十分的粗暴,它会直接终止程序
2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把
错误码放到errno中,表示错误。实际中C语言基本都是使用返回错误码的方式处理错误,部分情况
下使用终止程序处理非常严重的错误
例如fopen这个C语言文件接口,如果打开文件成功就会返回文件指针,如果打开文件失败,就会返回空指针。

二、C++异常概念

1、基本概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的
直接或间接的调用者处理这个错误
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try catch 关键字。try 块中放置可能抛
出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
    try
    {
        /* code */
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
    }
    

在C++语言中,我们通过抛出一条表达式来引发异常,被抛出的表达式类型及当前的调用链共同决定了哪段处理代码将用来处理异常。被选中的代码段是在调用链与被抛出对象类型匹配且最近的处理代码。

当执行throw时,跟在throw后面的语句将不再被执行,这也就是将该函数提前结束,类似与return

一旦程序开始执行异常处理代码,则沿着调用链创建的对象被销毁,因为跟在throw后面的语句将不再被执行。

2、异常的抛出和匹配原则

1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,
使用基类捕获
这里有十分多的细节
1、只有在try内部的语句的异常才会被捕获,catch中的异常则不会被捕捉——重新抛出
2、一般异常都是在最外面进行捕捉
3、抛异常可以跑任意类型对象
4、捕获时要求类型匹配
5、...是C++异常的最后一道底线,如果有抛出的异常没有被捕获,该程序将会被终止
6、不要在构造函数中抛异常:会导致对象不完整或者没有完全初始化
7、不要在析构函数中抛异常:会导致资源泄露
8、针对于6,7条,如果非要在这两个函数中抛异常,就需要在这两个函数中捕获

3、函数调用链中异常栈展开匹配原则

1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则
调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行

三、异常的使用

1、基本用法

在使用异常之前,虽然可以throw任何类型,但是C++建议最好要抛一个异常类对象

一个最简单的异常类,应该包括:错误码,错误信息

并且至少要有拷贝构造,析构函数,移动构造

一般的异常抛出与接受需要类型匹配,这里抛出派生类也可以,catch时使用异常类基类的引用来接收,构成多态

class Exception
{
public:
    Exception(const string &errmsg, int id)
        : _errmsg(errmsg), _id(id)
    {
    }
    virtual string what() const
    {
        return _errmsg;
    }

    virtual int GetId() const
    {
        return _id;
    }

protected:
    string _errmsg;
    int _id;
};
int division(int a, int b)
{
    if (b == 0)
    {
        throw Exception("除数为0", -1);
    }
    return a / b;
}

int main()
{
    int a = 0, b = 0;
    while (true)
    {
        try
        {
            cin >> a >> b;
            cout << division(a, b) << endl;
        }
        catch (const Exception &e)
        {
            cout << "错误码为: " << e.GetId() << endl;
            cout << "错误信息为: " << e.what() << endl;
        }
        catch (...)
        {
            cout << "未知异常" << endl;
        }
    }

    return 0;
}

2、重新抛出

在实际开发过程中异常是不可缺少的存在,但是一出现异常就直接返回到外部,也不太好

例如:在我们使用微信时,因为网络情况较差,不尝试多次直接抛出异常,对于用户来说那是接受不了的事情。所有这里就有重新抛出的概念

这里是模拟实现的发微信的过程


class Exception
{
public:
    Exception(const string &errmsg, int id)
        : _errmsg(errmsg), _id(id)
    {
    }
    virtual string what() const
    {
        return _errmsg;
    }

    virtual int GetId() const
    {
        return _id;
    }

protected:
    string _errmsg;
    int _id;
};

class SqlException : public Exception
{
public:
    SqlException(const string &errmsg, int id, const string &sql)
        : Exception(errmsg, id), _sql(sql)
    {
    }
    virtual string what() const
    {
        string str = "SqlException:";
        str += _errmsg;
        str += "->";
        str += _sql;
        return str;
    }

private:
    const string _sql;
};
class CacheException : public Exception
{
public:
    CacheException(const string &errmsg, int id)
        : Exception(errmsg, id)
    {
    }
    virtual string what() const
    {
        string str = "CacheException:";
        str += _errmsg;
        return str;
    }
};
class HttpServerException : public Exception
{
public:
    HttpServerException(const string &errmsg, int id, const string &type)
        : Exception(errmsg, id), _type(type)
    {
    }
    virtual string what() const
    {
        string str = "HttpServerException:";
        str += _type;
        str += ":";
        str += _errmsg;

        return str;
    }

private:
    const string _type;
};

void SQLMgr(const string &msg)
{
    srand(time(0));
    if (rand() % 7 == 0)
    {
        throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
    cout << "信息发送成功   ->" << msg << endl;
}

void SendMsg(const string &msg)
{
    srand(time(0));
    if(rand() % 2 == 0)
    {
        throw Exception("网络错误", 200);
    }
    SQLMgr(msg);
}
void HttpServer()
{
    string msg("今晚一起看电影吗?");
    int n = 3;
    while(n--)
    {
        try
        {
            SendMsg(msg);
        }
        catch(const Exception& e)
        {
            if(e.GetId() == 200 && n > 0)
            {
                continue;
            }
            else
            {
                throw;
            }
        }
    }

}
int main()
{
    while (1)
    {
        sleep(1);
        try
        {
            HttpServer();
        }
        catch (const Exception &e)
        {
            cout << e.what() << endl;
        }
        catch (...)
        {
            cout << "未知异常" << endl;
        }
    }
    return 0;
}

细心的同学可能会注意到,重新抛出时直接使用了throw而没有指明对象。

这里是因为catch无需访问抛出表达式,则可以忽略形参的名字,并且这个形参可以是左值引用,而不可以是右值引用。空throw只能出现在catch或catch直接或间接调用的函数中

四、异常的规范

1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的
后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
2. 函数的后面接throw(),表示函数不抛异常。
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

虽然C++11提供了noexcept关键字,但是编译器不会在编译时检查noexcept。事实上如果一个函数在说明了noexcept的同时又含有throw语句或者调用了可能抛出异常的其它函数,编译器将会顺利通过,并不会因为违反了异常的说明的情况而报错

五、异常的优缺点

1、异常的优点

1、异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
2、返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误
3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。

2、异常的缺点

1、异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。


总结


以上就是今天要讲的内容,本文仅仅简单介绍了C++异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值