• 【C++】异常


    在这里插入图片描述

    🚀write in front🚀
    📜所属专栏: C++学习
    🛰️博客主页:睿睿的博客主页
    🛰️代码仓库:🎉VS2022_C语言仓库
    🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
    关注我,关注我,关注我你们将会看到更多的优质内容!!

    在这里插入图片描述

    前言

    一.异常的概念:

      异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
      在异常里面,我们分别用throw抛出异常),catch(捕获异常),try(激活特定异常)这三个关键字来捕获异常。用法如下:

    try//表示异常要开始捕获了
    {
    // 保护的标识代码
    throw .....//抛出异常
    }
    catch( ExceptionName e1 )//根据异常的类型接收异常
    {
    // catch 块
    }
    catch( ExceptionName e2 )
    {
    // catch 块
    }
    catch( ExceptionName eN )
    {
    // catch 块
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    记住!!抛出异常一定要在try里面(现有函数栈帧里的try里面抛出异常或异常在函数抛出,但是函数调用在try里面,这两个都可以),这是前提!不然就会报错!!
    举个🌰🌰

    double Division(int a, int b)
    {
    	// 当b == 0时抛出异常
    	if (b == 0)
    	{
    		//throw "Division by zero condition!";
    		throw 1;
    	}
    	else
    	{
    		return ((double)a / (double)b);
    	}
    }
    
    void Func()
    {
    	
    	int len, time;
    	cin >> len >> time;
    	cout << Division(len, time) << endl;
    	cout << "yyyyyyyyy" << endl;
    }
    
    int main()
    {
    	Func();
    	try 
    	{
    		cout << "你干嘛哎哟" << endl;
    	}
    	catch (const char* errmsg) 
    	{
    		cout << errmsg << endl;
    	}
    	catch (...) 
    	{
    		cout << "unknown exception" << endl;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

      这里的func就没有在try里面,程序就会强制报错:
    在这里插入图片描述

    二.异常的使用:

    1. 异常的抛出和捕获

    1.1异常的抛出和匹配原则

    1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码
    2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
    3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回
    4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
    5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用,我们后面会详细讲解这个。
    6. 在抛出异常之后,会直接跳到catch语句,try里面的抛出异常语句的后面几句就不会执行了。

    1.2在函数调用链中异常栈展开匹配原则

    1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理
    2. 没有匹配的catch退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
    3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止
      举个🌰:
    double Division(int a, int b)
    {
    	// 当b == 0时抛出异常
    	if (b == 0)
    	{
    		throw "Division by zero condition!";
    	}
    	else
    	{
    		return ((double)a / (double)b);
    	}
    }
    
    void Func()
    {
    	int len, time;
    	cin >> len >> time;
    	cout << Division(len, time) << endl;
    }
    
    int main()
    {
    	try 
    	{
    		Func();
    	}
    	catch (int errmsg)
    	{
    		cout << errmsg << endl;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述

    1. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行
      在这里插入图片描述
      下面我们来看一个正确使用的🌰:
    double Division(int a, int b)
    {
    // 当b == 0时抛出异常
    if (b == 0)
    throw "Division by zero condition!";
    else
    return ((double)a / (double)b);
    }
    void Func()
    {
    int len, time;
    cin >> len >> time;
    cout << Division(len, time) << endl;
    cout << "yyyyyyyyy" << endl;//如果抛出异常,直接跳到catch,执行不到这一句了
    }
    int main()
    {
    try {
    Func();
    }
    catch (const char* errmsg) 
    {
    cout << errmsg << endl;
    }
    catch(...)//处理其他情况
    {
    cout<<"unkown exception"<<endl;
    }
    cout<<" hh"<<endl;//catch后面的代码继续执行。
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述
    在这里插入图片描述

    1.3 异常的重新抛出

      有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出异常传递给更上层的函数进行处理

    我们来看看下面的🌰:

    double Division(int a, int b)
    {
    // 当b == 0时抛出异常
    if (b == 0)
    {
    throw "Division by zero condition!";
    }
    return (double)a / (double)b;
    }
    void Func()
    {
    	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
    	int* array = new int[10];
    	int len, time;
    	cin >> len >> time;
    	cout << Division(len, time) << endl;//在这里直接跳catch去了,下面的语句就不会执行了
    	cout << "delete []" << array << endl;
    	delete[] array;
    }
    
    int main()
    {
    	try
    	{
    		Func();
    	}
    	catch (const char* errmsg)
    	{
    		cout << errmsg << endl;
    	}
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

      在这里我们就会发现,由于异常的跳跃空间没有得到释放。那么怎么解决这个问题呢?

    double Division(int a, int b)
    {
    // 当b == 0时抛出异常
    if (b == 0)
    {
    throw "Division by zero condition!";
    }
    return (double)a / (double)b;
    }
    void Func()
    {
    	int* array = new int[10];
    
    	try
    	{
    		int len, time;
    		cin >> len >> time;
    		cout << Division(len, time) << endl;
    
    		// func()
    	}
    	catch (...)  // 异常的重新抛出,为了内存释放
    	{
    		cout << "delete []" << array << endl;
    		delete[] array;
    		throw; // 捕获什么,抛什么
    	}
    
    	//...
    	cout << "delete []" << array << endl;
    	delete[] array;
    }
    
    int main()
    {
    	try
    	{
    		Func();
    	}
    	catch (const char* errmsg)
    	{
    		cout << errmsg << endl;
    	}
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    在这里插入图片描述

    这里我们就要用到异常的重新抛出!!这里捕获异常后并不处理异常,而是为了内存释放,异常还是交给外面处理!!

    1.4异常安全

    • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
    • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
    • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII我们智能指针这节进行讲解

    1.5异常规范

    1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型)列出这个函数可能抛掷的所有异常类型
    2. 函数的后面接throw(),表示函数不抛异常
    3. 若无异常接口声明,则此函数可以抛掷任何类型的异常
    4. C++11后面加noexcept,表示不会抛异常
    // 这里表示这个函数会抛出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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们来看看new的底层就知道了:
    在这里插入图片描述
    这里就是说抛出类型一定是bad_alloc_RAISE是宏定义,就是throw)

    二.自定义异常体系:

      在服务器开发里面,异常是怎么工作的呢?

    首先异常就封装了一个类如下:

    class Exception
    {
    public:
    Exception(const string& errmsg, int id)
    :_errmsg(errmsg)
    ,_id(id)
    {}
    virtual string what() const
    {
    return _errmsg;
    }
    protected:
    string _errmsg;//错误描述
    int _id;//错误编号
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    而在同一个错误描述下也有不同的情况,所以会有错误编号,比如我们给别人发送消息,如果发送失败,错误描述就是发送失败,但是发送失败也有很多情况:
    在这里插入图片描述
    根据不同的情况,我们写不同的判断:
    在这里插入图片描述

      但是这里还是不够的,在公司里面,抛出了一大堆异常,如果不知道是哪个模块抛出的异常,那就会很难找,所以我们可以对上面的异常类进行继承以下几个模块:
    在这里插入图片描述

    // 服务器开发中通常使用的异常继承体系
    class Exception
    {
    public:
    Exception(const string& errmsg, int id)
    :_errmsg(errmsg)
    ,_id(id)
    {}
    virtual string what() const
    {
    return _errmsg;
    }
    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()
    {
    srand(time(0));
    if (rand() % 7 == 0)
    {
    throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
    //throw "xxxxxx";
    }
    void CacheMgr()
    {
    srand(time(0));
    if (rand() % 5 == 0)
    {
    throw CacheException("权限不足", 100);
    }
    else if (rand() % 6 == 0)
    {
    throw CacheException("数据不存在", 101);
    }
    SQLMgr();
    }
    void HttpServer()
    {
    // ...
    srand(time(0));
    if (rand() % 3 == 0)
    {
    throw HttpServerException("请求资源不存在", 100, "get");
    }
    else if (rand() % 4 == 0)
    {
    throw HttpServerException("权限不足", 101, "post");
    }
    CacheMgr();
    }
    int main()
    {
    while (1)
    {
    this_thread::sleep_for(chrono::seconds(1));
    try{
    HttpServer();
    }
    catch (const Exception& e) // 这里捕获父类对象就可以
    {
    // 多态
    cout << e.what() << endl;
    }
    catch (...)
    {
    cout << "Unkown Exception" << endl;
    }
    }
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121

    这样就很好找到问题所在了。这里就运用到了我们之前学的多态了,是不是活学活用呀?

    2.1C++标准库的异常体系

      其实C++里面自己就有一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的。
    在这里插入图片描述
    说明:实际中我们自己可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计的不够好用。

    int main()
    {
    try{
    vector<int> v(10, 5);
    // 这里如果系统内存不够也会抛异常
    v.reserve(1000000000);
    // 这里越界会抛异常
    v.at(10) = 100;
    }
    catch (const exception& e) // 这里捕获父类对象就可以
    {
    cout << e.what() << endl;
    }
    catch (...)
    {
    cout << "Unkown Exception" << endl;
    }
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    三.异常的优缺点:

    C++异常的优点:

    1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
    2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,具体看下面的详细解释。
    // 1.下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,
    ServerStart再返回给main函数,main函数再针对问题处理具体的错误。
    // 2.如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因
    为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
    int ConnnectSql()
    {
    // 用户名密码错误
    if (...)
    return 1;
    // 权限不足
    if (...)
    return 2;
    }
    int ServerStart() {
    if (int ret = ConnnectSql() < 0)
    return ret;
    int fd = socket()
    if(fd < 0return errno;
    }
    int main()
    {
    if(ServerStart()<0)
    ...
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    1. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
    2. 部分函数使用异常更好处理,比如构造函数没有返回值不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误

    C++异常的缺点:

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

    总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是用异常处理错误,这也可以看出这是大势所趋。

      更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

    专栏订阅:
    每日一题
    C语言学习
    算法
    智力题
    初阶数据结构
    Linux学习
    C++学习
    更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

    在这里插入图片描述

  • 相关阅读:
    解决vue-cli node-sass安装不成功问题
    maven打包可运行jar
    基于SpringBoot的企业客户信息反馈平台的设计与实现
    Fiddle日常运用手册(2)-使用过滤器进行接口精准拦截
    blender光照系统设置
    写一篇nginx配置指南
    剑指 Offer 04. 二维数组中的查找
    Node.js环境安装与服务设置,结合内网穿透随时随地公网访问!
    贪心算法详解
    一文教会你用 IDEA 从 0 到 1 构建 Tomcat , Maven 再到 Servlet(输出hello world)
  • 原文地址:https://blog.csdn.net/qq_74310471/article/details/132773822