标签:成员 匹配 静态编译 list hat 访问 调用 通过 检查
异常处理将问题的检测和问题的解决过程分离。
C++ 通过抛出一个表达式来引发一个异常,当执行一个 throw 时,跟在 throw 后面的语句将不再执行。程序的控制权从 throw 转移到与之匹配的 catch 模块。控制权转移有两个重要的含义:
异常抛出后,程序将暂停当前函数的执行过程并立即开始寻找与异常匹配的 catch 子句:
上述的过程称为栈展开,沿着嵌套函数调用链不断查找:
栈展开过程中,程序在语句块中创建的局部对象将随着块作用域的退出而被销毁。
如果异常发生在构造函数中,则当前对象可能只构造了一部分,必须要确保已构造的成员能被析构。
栈展开的过程中析构函数将被执行,在栈展开的过程中引发了异常但是没有处理它或者抛出异常后没有被正确捕获,则系统将调用 terminate 函数。如果析构函数也抛出了异常,并且析构函数自身没有捕获异常,则程序将被终止。因此,析构函数中不应该抛出不能被它自身处理的异常。实际上析构函数往往只是释放资源,所以不太可能抛出异常,因此,所有的标准库类型都确保它们的析构函数不会抛出异常。
异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。
异常对象位于编译器管理的空间中,编译器确保无论最终调用哪个子句都能访问该空间,当异常处理完毕,异常对象将被销毁。
抛出异常后将执行栈展开操作,如果抛出一个指向局部对象的指针,肯定是一种错误的行为。如果指针所指的对象位于某个块中,而该块在 catch 语句之前就已经退出了,则意味着在执行 catch 语句之前局部对象就被销毁了。
当抛出一个表达式时,该表达式的静态编译时类型将决定了异常对象的类型,因为抛出的对象经常来自于一个继承体系,如果 throw 表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出。
catch 子句中的异常声明类似只包含一个形参的函数形参列表,声明的类型决定了处理代码所能捕获的异常类型,这个类型必须是完全类型,可以是左值引用,但不能是右值引用。
进入一个 catch 语句后,通过异常对象初始化异常声明中的参数,如果 catch 的参数类型是非引用类型,则该参数是异常对象的一个副本。
catch 的参数如果是基类类型,可以使用其派生类类型的异常对象对其初始化,此时,如果 catch 的参数是非引用类型,则异常对象将被切掉一部分。如果 catch 的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。
异常声明的静态类型将决定 catch 语句所能执行的操作,如果 catch 的参数是基类类型,则 catch 无法使用派生类特有的成员。
通常, catch 接受的异常与某个继承体系有关,最好将该 catch 的参数定义成引用类型。
在搜寻 catch 语句的过程中,最终找到的 catch 未必是异常的最佳匹配,而是第一个能与异常匹配的 catch 子句,因此,越是专门的 catch 越应该设置在整个 catch 列表的最前端。特别的,程序使用多个具有继承关系的多个异常时,必须对 catch 语句的顺序进行组织,使得派生类异常处理的代码出现在基类异常的处理代码之前。
异常的类型要求和 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 语句块还未生效,所以构造函数体内的 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 语句块只能处理构造函数开始执行后发生的异常。
对于用户来说,提前知道某个函数不会抛出异常,将简化调用该函数的代码。
对于编译器来说,提前知道某个函数不会抛出异常,将有助于执行一些特殊的优化操作。
C++ 11 中 noexcept 说明符指定某个函数不会抛出异常。其形式是关键字 noexcept 紧跟着函数的参数列表之后,用以标识该函数不会抛出异常:
void recoup(int) noexcept; //@ 不会抛出异常
void alloc(int); //@ 可能抛出异常
如果一个函数在说明了 noexcept 的同时又含有 throw 语句或者调用了可能抛出异常的其他函数,编译器将能顺利编译通过,并不会因为这种违反异常说明的情况而报错:
void f() noexcept //@ 承诺不会抛出异常
{
throw exception(); //@ 违反了异常说明,抛出异常
}
一旦一个 noexcept 函数抛出了异常,程序就会调用terminate 以确保遵守不在运行时抛出异常的承诺。因此 noexcept 一般在两种情况下使用:
noexcept 说明符接受一个可选的实参,此实参必须能够转换成 bool 类型:
void recoup(int) noexcept(true); //@ recoup 不会抛出异常
void alloc(int) noexcept(false); //@ alloc 可能抛出异常
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 做了更加严格的限定,这是允许的
};
当编译器合成拷贝控制成员时,同时也生成一个异常说明:
异常类的继承体系,如图所示:
继承体系的第二层将 exception 划分为两大类:
类型 exception 仅仅定义了拷贝构造函数,拷贝赋值运算符,一个虚析构函数和一个名为 what 的虚成员。 what 返回一个 const char*
,该指针指向一个以 null 结尾的字符数组,并且确保不会抛出任何异常。
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;
}
}
标签:成员 匹配 静态编译 list hat 访问 调用 通过 检查
原文地址:https://www.cnblogs.com/xiaojianliu/p/12436733.html