• 异常处理


    异常处理将问题的检测和问题的解决过程分离。

    抛出异常

    C++ 通过抛出一个表达式来引发一个异常,当执行一个 throw 时,跟在 throw 后面的语句将不再执行。程序的控制权从 throw 转移到与之匹配的 catch 模块。控制权转移有两个重要的含义:

    • 沿着调用链的函数可能会提早退出。
    • 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。

    栈展开

    异常抛出后,程序将暂停当前函数的执行过程并立即开始寻找与异常匹配的 catch 子句:

    • 当 throw 出现在一个 try 语句块内,检查与这个 try 块关联的 catch 子句。如果找到,则使用该 catch 处理异常。
    • 如果上面的步骤没哟找到匹配的 catch 子句,并且上面的 try 语句块嵌套在别的 try 块中,则继续在外层的 try 语句块中查找匹配的 catch 子句,以此类推。
    • 如果无法找到匹配的 catch 子句,则退出当前函数,在调用当前函数的外层函数中继续查找,以此类推。

    上述的过程称为栈展开,沿着嵌套函数调用链不断查找:

    • 如果找到了匹配的 catch 子句,则程序进入该子句并执行其中的程序,执行完毕,找到 try 语句块关联的最后一个 catch 子句之后的点,并从这里继续执行。
    • 如果无法找到匹配的 catch 子句,程序将调用 terminate 终止程序的执行。所以,一旦异常没有被捕获,将导致当前程序终止执行。

    栈展开过程中对象被自动销毁

    栈展开过程中,程序在语句块中创建的局部对象将随着块作用域的退出而被销毁。

    • 如果局部对象是类类型,则对象的析构函数将自动被调用。
    • 内置类型的对象在销毁时不需要做任何事情。

    如果异常发生在构造函数中,则当前对象可能只构造了一部分,必须要确保已构造的成员能被析构。

    析构函数与异常

    栈展开的过程中析构函数将被执行,在栈展开的过程中引发了异常但是没有处理它或者抛出异常后没有被正确捕获,则系统将调用 terminate 函数。如果析构函数也抛出了异常,并且析构函数自身没有捕获异常,则程序将被终止。因此,析构函数中不应该抛出不能被它自身处理的异常。实际上析构函数往往只是释放资源,所以不太可能抛出异常,因此,所有的标准库类型都确保它们的析构函数不会抛出异常。

    异常对象

    异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。

    • 如果该表达式是一个类类型的话,则相应的类必须有一个可访问的析构函数、拷贝构造函数、移动构造函数。
    • 如果表达式是数组类型或者函数类型,则将表达式转换成与之对应的指针类型。

    异常对象位于编译器管理的空间中,编译器确保无论最终调用哪个子句都能访问该空间,当异常处理完毕,异常对象将被销毁。

    抛出异常后将执行栈展开操作,如果抛出一个指向局部对象的指针,肯定是一种错误的行为。如果指针所指的对象位于某个块中,而该块在 catch 语句之前就已经退出了,则意味着在执行 catch 语句之前局部对象就被销毁了。

    当抛出一个表达式时,该表达式的静态编译时类型将决定了异常对象的类型,因为抛出的对象经常来自于一个继承体系,如果 throw 表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出。

    捕获异常

    catch 子句中的异常声明类似只包含一个形参的函数形参列表,声明的类型决定了处理代码所能捕获的异常类型,这个类型必须是完全类型,可以是左值引用,但不能是右值引用。

    进入一个 catch 语句后,通过异常对象初始化异常声明中的参数,如果 catch 的参数类型是非引用类型,则该参数是异常对象的一个副本。

    catch 的参数如果是基类类型,可以使用其派生类类型的异常对象对其初始化,此时,如果 catch 的参数是非引用类型,则异常对象将被切掉一部分。如果 catch 的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。

    异常声明的静态类型将决定 catch 语句所能执行的操作,如果 catch 的参数是基类类型,则 catch 无法使用派生类特有的成员。

    通常, catch 接受的异常与某个继承体系有关,最好将该 catch 的参数定义成引用类型。

    查找匹配的处理代码

    在搜寻 catch 语句的过程中,最终找到的 catch 未必是异常的最佳匹配,而是第一个能与异常匹配的 catch 子句,因此,越是专门的 catch 越应该设置在整个 catch 列表的最前端。特别的,程序使用多个具有继承关系的多个异常时,必须对 catch 语句的顺序进行组织,使得派生类异常处理的代码出现在基类异常的处理代码之前。

    异常的类型要求和 catch 声明的类型是精确匹配的,除了下面几种情况:

    • 允许从非常量向常量的类型转换,即一个非常量的 throw 语句可以匹配一个接受常量引用的 catch 语句。
    • 允许从派生类向基类的类型转换。
    • 数组被转换成指向数组元素类型的指针,函数被转换成指向该函数类型的指针。

    除此之外,包括标准算术类型转换和类类型转换在内,其他所有转换规则都不能匹配 catch 的过程中使用。

    重新抛出

    一条 catch 语句通过重新抛出的操作将异常传递给另外一个 ctach 语句。这里的重新抛出仍然是一条 throw 语句,只不过不包含任何表达式:

    throw;
    

    这种空的 throw 语句只能出现在 catch 语句或 catch 语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到空的 throw 语句,编译器将调用 terminate。

    一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。

    catch (my_error& eObj){			//@ 引用类型
    	eObj.status = errCodes::severeErr;			//@ 修改异常对象
    	throw;		//@ 异常对象的 status 成员是 severeErr
    }catch (my_error eObj){		//@ 非引用类型
    	eObj.status = errCodes::severeErr;			//@ 只修改了异常对象的局部副本
    	throw;		//@ 异常对象的 status 成员没有改变
    }
    

    捕获所有异常的处理代码

    catch(...) 可以捕获所有的异常,通常将其与重新抛出异常一起使用:

    try{
    	//@ do some thing
    }
    catch(...){
    	trhow;
    }
    

    catch(...) 既能单独使用,也能与其他几个 catch 一起出现,如果与其他的 catch 一起出现 ,则 catch(...) 必须在最后位置。出现在捕获所有异常语句后面的 catch 语句将永远不会被匹配。

    函数 try 语块与构造函数

    构造函数在进入其函数体之前首先执行初始值列表,因为在初始值列表抛出异常时构造函数体内的 try 语句块还未生效,所以构造函数体内的 catch 语句无法处理构造函数初始值列表抛出的异常。

    要想处理构造函数初始值抛出的异常,必须将构造函数写成函数 try 语句块,也称为函数测试块。函数测试块使得一组 catch 既能处理构造函数体(析构函数体),也能处理构造函数的初始化过程(或析构函数的析构过程)。

    template <typename T>
    Blob<T>::Blob(std::initializer_list<T> il) try:
    	data(std::make_shared<std::vector<T>>(il)){
    	//@ function body
    	}catch(const std::bad_allo& e){handle_out_of_memory(e);}
    

    try 位于初始值列表的冒号以及构造函数体的花括号之前。

    初始化构造函数的参数时也能发生异常,这样的异常不属于函数 try 语句块的一部分。try 语句块只能处理构造函数开始执行后发生的异常。

    noexcept 异常说明

    对于用户来说,提前知道某个函数不会抛出异常,将简化调用该函数的代码。

    对于编译器来说,提前知道某个函数不会抛出异常,将有助于执行一些特殊的优化操作。

    C++ 11 中 noexcept 说明符指定某个函数不会抛出异常。其形式是关键字 noexcept 紧跟着函数的参数列表之后,用以标识该函数不会抛出异常:

    void recoup(int) noexcept;	//@ 不会抛出异常
    void alloc(int);			//@ 可能抛出异常
    
    • noexcept 说明符要么出现在所有声明和定义的语句中,要么一次也不出现。
    • noexcept 说明符应该在函数的尾置返回类型之前。
    • 可以在函数指针的声明和定义中指定 noexcept。
    • 在 typedef 或类型别名中则不能出现 noexcept。
    • 成员函数中,noexcept 说明符需要跟在 const 及引用限定符之后,而在 final、override或虚函数 =0 之前。

    违反异常说明

    如果一个函数在说明了 noexcept 的同时又含有 throw 语句或者调用了可能抛出异常的其他函数,编译器将能顺利编译通过,并不会因为这种违反异常说明的情况而报错:

    void f() noexcept	//@ 承诺不会抛出异常
    {
    	throw exception();	//@ 违反了异常说明,抛出异常
    }
    

    一旦一个 noexcept 函数抛出了异常,程序就会调用terminate 以确保遵守不在运行时抛出异常的承诺。因此 noexcept 一般在两种情况下使用:

    • 确认函数不会抛出异常。
    • 根本不知道如何处理异常。

    异常说明的实参

    noexcept 说明符接受一个可选的实参,此实参必须能够转换成 bool 类型:

    • 如果实参是 true,则函数不会抛出异常;
    • 如果实参是 false,则函数可能抛出异常。
    void recoup(int) noexcept(true);	//@ recoup 不会抛出异常
    void alloc(int) noexcept(false);	//@ alloc 可能抛出异常
    

    noexcept 运算符

    noexcept 说明符常常与 noexcept 运算符混合使用。noexcept 运算符是一个一元运算符,它的返回值是一个 bool 类型的右值常量表达式,noexcept 不会求其运算对象的值:

    noexcept (recoup(i))	//@ recoup 不抛出异常则结果为 true,否则结果为 false
    

    更普通的形式:

    noexcept(e) 
    
    

    当 e 调用的所有函数都做了不抛出说明,本身也不含有 throw 语句,上述表达式为 true,否则为 false。

    void f noexcept(noexcept(g())) //@ f 和 g 异常说明一致
    
    

    异常说明与指针、虚函数和拷贝控制

    尽管 noexcept 说明符不属于函数类型的一部分,但是函数的异常说明仍然会影响函数的使用,函数指针及该指针所指的函数必须具有一致的异常说明:

    • 如果某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。
    • 如果显式或隐式地说明了指针可能抛出异常,则该指针可以指向任何函数,即使承诺了不抛出异常的函数也可以。
    void (*pf1)(int) noexcept = recoup;	//@ recoup,pf1 都承诺不会抛出异常
    void (*pf2)(int) = recoup; //@ ok,recoup 不会抛出异常,pf2可能抛出异常,二者之间互不干扰
    
    pf1 = alloc;	//@ error,alloc 可能抛出异常,但是 pf1 已经说明了它不抛出异常
    pf2 = alloc;	//@ ok,pf2 和 alloc 都能抛出异常
    
    

    如果一个虚函数承诺了不会抛出异常,则后续派生出的虚函数也必须做出同样的承诺,与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数可以允许抛出异常也可以不允许抛出异常。

    class Base {
    public:
    	virtual double f1(double) noexcept;	//@ 不会抛出异常
    	virtual int f2() noexcept(false);	//@ 可能抛出异常
    	virtual void f3();	//@ 可能抛出异常
    };
    
    class Derived : public Base {
    public:
    	double f1(double) ;			//@ error,Base::f1 承诺不会抛出异常
    	int f2() noexcept(false);	//@ ok,与 Base::f2 的异常说明一致
    	void f3() noexcept;			//@ ok,Derived::f3 做了更加严格的限定,这是允许的
    };
    
    

    当编译器合成拷贝控制成员时,同时也生成一个异常说明:

    • 如果对所有成员和基类的所有操作都承诺了不会抛出异常,则合成的成员是 noexcept 的。
    • 如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是 noexcept(false)。
    • 如果合成一个析构函数但是没有为它提供异常说明,则编译器将合成一个。合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致。

    异常类层次

    异常类的继承体系,如图所示:

    继承体系的第二层将 exception 划分为两大类:

    • 运行时错误,表示只有在程序运行时才能检测到的错误。
    • 逻辑错误,表示可以在代码中发现的错误。

    类型 exception 仅仅定义了拷贝构造函数,拷贝赋值运算符,一个虚析构函数和一个名为 what 的虚成员。 what 返回一个 const char* ,该指针指向一个以 null 结尾的字符数组,并且确保不会抛出任何异常。

    • 类 exception、bad_cast、bad_alloc 定义了默认构造函数。
    • runtime_error、logic_error 没有默认构造函数,但是有一个可接受 C 风格字符串或者 string 类型实参的构造函数,负责提供错误的更多信息。
    • what 负责返回用于初始化异常对象的信息, what 是虚函数,所以当捕获基类的引用时,对 what 函数的调用将执行与异常对象动态类型对应的版本。

    定义自己的异常类型

    class out_of_stock : public std::runtime_error {
    public: 
    	explicit out_of_stock(const std::string& s):
    		std::runtime_error(s) {}
    };
    
    class isbn_mismatch : public std::logic_error {
    public:
    	explicit isbn_mismatch(const std::string& s) :
    		std::logic_error(s) {}
    	isbn_mismatch(const std::string& s,const std::string& lhs, const std::string& rhs):
    		std::logic_error(s), left(lhs), right(rhs) {}
    	const std::string left, right;
    };
    
    

    使用自定义的异常类型

    Sales_data& Sales_data::operator+=(const Sales_data& rhs)
    {
    	if (isbn() != rhs.isbn())
    		throw isbn_mismatch(("wrong isbn"),isbn(),rhs.isbn());
    	units_sold += rhs.units_sold;
    	revenue += rhs.revenue;
    	return *this;
    }
    
    Sales_data item1, item2, sum;
    while (cin >> item >> item)
    {
    	try {
    		sum = item1 + item2;
    	}
    	catch (const isbn_mismatch& e) {
    		cerr << e.what() << ": left isbn(" << e.left << ") right isbn(" << e.right << ")" << endl;
    	}
    }
    
    
  • 相关阅读:
    MyBatis学习(五)resultMap测试
    MyBatis学习(四)XML配置文件之SQL映射的XML文件
    Mybatis学习(三)XML配置文件之mybatis-config.xml
    每次回顾,总会有一点小收获!
    php数组去重、魔术方法、redis常用数据结构及应用场景
    MySQL使用可重复读作为默认隔离级别的原因
    后端程序猿标配之linux命令
    常用字符串函数
    nginx配置隐藏index.php
    MySQL的sql_mode解析与设置
  • 原文地址:https://www.cnblogs.com/xiaojianliu/p/12436733.html
Copyright © 2020-2023  润新知