目录
一、异常简述
1.1 异常是什么
异常,一句话来说就是程序错误,存在程序的正常功能之外,如果程序出现异常,但是异常没有被捕获,程序就会被终止。
C/C++,在内存管理方面提供了很大的自由度和灵活性,但用这种语言开发的应用程序的安全性很大程度上需要依赖程序员的测试、检测环节。类设计者在设计自定义类类型时,尤其需要注意对于异常的把控能力。
当然在类设计上, C/C++除了内存泄漏这个常被诟病的弊端外,其他异常捕获也需要同样谨慎处理,例如逻辑异常:越界异常、无效参数、长度异常等等,又例如运行时异常:系统异常、格式异常、范围异常等。
在编写代码时,我们常常会不自觉地写下异常的代码而不自知,例如:使用未经处理的或原始的指针、资源泄漏了、写出不合规行为的构造函数与析构函数,运行许久的程序突然崩溃。执行程序和库程序尺寸增加了,同时运行速度在降低。
异常出现就要进行处理,否则程序异常退出。C/C++异常处理提供了一种可以使程序从执行的某点将控制流和信息转移到与执行先前经过的某点相关联的处理代码的方法(换言之,异常处理将控制权沿调用栈向上转移,这是“栈展开”,后面章节再细说)。c/c++提供了多种异常处理方法:throw 表达式、dynamic_cast、typeid、new 表达式、分配函数,以及专门用来抛出特定异常以指示特定错误状态的任何标准库函数(例如 std::vector::at、std::string::substr 等)都可以抛出异常。
为捕获异常,throw 表达式必须在 try 块或者 try 块中所调用的函数中,而且必须有与异常对象的类型相匹配的 catch 子句。
在声明函数(非成员函数、成员函数)时,可以提供以下说明以限制函数能够抛出的异常类型:
- ◦动态异常说明 (C++17 前)(即throw(类型标识列表(可选)),见后面章节)
- ◦noexcept 说明 (C++11 起)(详细说明参考前本专栏一篇博文(篇七),本文不展开)。noexcept 是 throw() 的改进版本,后者在 C++11 中弃用。
异常处理过程中发生的错误由 std::terminate 和 std::unexpected (C++17 前) 处理(两个关键词阐述将后面章节)。
1.2 异常处理概念
【1】错误处理
异常的抛出用于从函数中为错误发信号,其中“错误”通常仅限于以下内容:
- 无法满足后置条件,例如不能产生有效的返回值对象;
- 无法满足另一个必须调用的函数的前置条件;
- (对于非私有成员函数)无法(再)建立类不变量。
这意味着构造函数和大多数运算符应该通过抛出异常来报告程序错误。另外,所谓宽契约(wide contract)函数用异常来指示非法输入,例如,std::string::at 没有前置条件,但它会抛出异常以指示下标越界。
【2】异常安全
在函数报告了错误状态后,可以提供附加保证以保障程序的状态。以下是四个被广泛认可的异常保证等级,每个是另一个的严格超集:
- 不抛出(nothrow)(或不失败)异常保证---函数始终不会抛出异常。析构函数和其他可能在栈回溯中调用的函数被期待为不会抛出(以其他方式报告或隐瞒错误)。析构函数默认为 noexcept。 (C++11 起)交换函数(swap),移动构造函数,及为提供强异常保证所使用的其他函数,都被期待为不会失败(函数总是成功)。
- 强(strong)异常保证---如果函数抛出异常,那么程序的状态会恰好被回滚到该函数调用前的状态。(例如 std::vector::push_back)
- 基础(basic)异常保证---如果函数抛出异常,那么程序处于某个有效状态。不泄漏资源,而所有对象的不变式都保持完好。
- 无异常保证---如果函数抛出异常,那么程序可能不会处于有效的状态:可能已经发生了资源泄漏、内存损坏,或其他摧毁不变式的错误。
此外,泛型组件还可以提供异常中性(exception neutral)保证:如果从某个模板形参(例如从 std::sort 的 Compare 函数对象,或从 std::make_shared 中 T 的构造函数)抛出异常,那么它会被无修改地传播给调用方。
【3】异常对象
虽然任意完整类型和指向 void 的 cv 指针都能作为异常对象抛出,但所有标准库函数都以值抛出匿名临时对象,而且这些对象的类型都(直接或间接)派生于 std::exception。用户定义的异常通常遵循此模式。为避免不必要的异常对象复制和对象切片,catch 子句在实践中最好以引用捕获。
二、异常处理
2.1 try……catch异常处理语法
异常不能被忽略。必要时,c/c++标准运行允许开发者调用用try……catch捕获异常,然后将异常信息传递一个对象后抛出该对象,通过该对象就可判定出现了什么错误,需要做怎样异常处理。
try……catch将一或多个异常处理块(catch 子句)与复合语句关联,其语法实现如下:
try 复合语句 处理块序列
//其中处理块序列是一或多个处理块的序列,它有下列语法:
catch (属性(可选) 类型说明符序列声明符) 复合语句
// (1) 声明一个具名形参的catch子句:try { /* */ } catch (const std::exception& e) { /* */ }
catch (属性(可选) 类型说明符序列抽象声明符(可选) ) 复合语句
//(2)声明一个无名形参的catch子句:try { /* */ } catch (const std::exception&) { /* */ }
catch ( ... ) 复合语句
//(3)catch-all处理块,可被任何异常激活:try { /* */ } catch (...) { /* */ }
/*参数说明
*复合语句 - 花括号环绕的语句序列
*属性 - (C++11 起) 任意数量的属性,应用于形参
*类型说明符序列 - 形参声明的一部分,与在函数形参列表中相同
*声明符 - 形参声明的一部分,与在函数形参列表中相同
*抽象声明符 - 无名形参声明的一部分,与在函数形参列表中相同
*/
try 块以关键字 try 开始,后面是用花括号起来的语句序列块。try 块后面是一个或多个 catch 子句。每个 catch 子句包括三部分:关键字 catch,圆括号内单个类型或者单个对象的声明---称为异常说明符,以及通常用花括号括起来的语句块。如果选择了一个 catch 子句来处理异常,则执行相关的块语句。一旦 catch 子句执行结束,程序流程立即继续执行紧随着最后一个 catch 子句
的语句。
catch 子句的形参(类型说明符序列与声明符,或者类型说明符序列与抽象声明符)决定何种类型的异常导致进入此 catch 子句。它不能是右值引用类型、抽象类、不完整类型或指向不完整类型的指针(但允许指向(可有 cv 限定的)void 的指针)。若形参的类型是数组类型或函数类型,则它被处理成对应的指针类型(类似于函数声明)。
2.2 动态异常说明-throw
throw异常说明符,列出函数可能直接或间接抛出的异常。
/*显式动态异常说明
*类型标识列表 - 逗号分隔的类型标识列表,后附省略号(...)的类型标识表示包展开 (C++11 起)
*/
throw(类型标识列表(可选)) //(C++11 中弃用)(C++17 中移除)
这种说明只能在作为类型为函数类型、函数指针类型、函数引用类型、成员函数指针类型的函数、变量、非静态数据成员的声明符的,顶层函数声明符上和形参的声明符或返回类型的声明符上出现。
void f() throw(int); // OK:函数声明
void (*pf)() throw (int); // OK:函数指针声明
void g(void pfa() throw(int)); // OK:函数指针形参声明
typedef int (*pf)() throw(int); // 错误:typedef 声明
通常有时我们也在try 块函数体内直接显示调用throw异常说明符抛出异常对象。
void f() throw(int); // 函数声明,定义异常抛出为int型
void func() {
try {
f(); //隐式,间接
} catch (int& obj) {
std::cout << " the exception info: " << obj << '\n';
}
}
void func() {
try {
throw std::string("test"); //主动抛出异常对象,显式
} catch (std::string& obj) {
std::cout << " the exception info: " << obj << '\n';
}
}
关于throw异常说明符更详细说明见后面章节。
2.3 标准异常体系
c++允许抛出任何类型的异常,通常建议抛出派生自std::exception的类型。std::exception是标准库提供的异常基类,提供异常类集一致的接口,标准库所生成的所有异常继承自 std::exception,整个标准异常类的继承体系如下图。
这些异常类大多都是见名知意的,基类std::exception的定义也很简单,如同我们自定义最基本的类成员一样,提供了简要的成员函数(构造、虚析构、复制赋值和一个返回异常描述信息函数):
//成员函数
(构造函数) 构造异常对象 (公开成员函数)
(析构函数) [虚] 析构该异常对象 (虚公开成员函数)
operator= 复制异常对象 (公开成员函数)
what [虚] 返回解释性字符串 (虚公开成员函数)
//类声明
namespace std {
class exception {
public:
exception() noexcept;
exception(const exception&) noexcept;
exception& operator=(const exception&) noexcept;
virtual ~exception();
virtual const char* what() const noexcept;
};
}
2.4 try ...catch抛出异常对象的处理
C++异常可以由与抛出异常相同类型的catch表达式捕获,或者由可以捕获任何类型异常的catch(...)表达式捕获。如果抛出的异常类型是一个类,它也有一个基类或多个子类,那么接收这个类型或这个类型基类的引用可以捕获这个异常。请注意,当异常被引用捕获,它会绑定到实际抛出的异常对象。
#include <iostream>
#include <vector>
void g4() {
try {
std::cout << "Throwing an integer exception...\n";
throw 42; //主动抛出int型对象
} catch (int i) {
std::cout << " the integer exception was caught, with value: " << i << '\n';
}
//
try {
std::cout << "Creating a vector of size 5... \n";
std::vector<int> v(5);
std::cout << "Accessing the 11th element of the vector...\n";
std::cout << v.at(10); // vector::at() 抛出 std::out_of_range
} catch (const std::exception& e) { // 按基类的引用捕获
std::cout << " a standard exception was caught, with message '"
<< e.what() << "'\n";
}
}
异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。当 try 复合语句 中的任何语句抛出了类型 E 的异常时,以之对 处理块序列 中的每个 catch 子句的形参类型 T,按 catch 子句的列出顺序进行匹配。若下列任一为真则异常得到匹配:
- E 与 T 是同一类型(忽略 T 上的顶层 cv 限定符)
- T 是到(可为 cv 限定的)E 的左值引用
- T 是 E 的无歧义公开基类
- T 是到 E 的无歧义公开基类的引用
- T 是(可为 cv 限定的)U 或 const U& (C++14 起),且 U 是指针或成员指针 (C++17 起)类型,且 E 亦为能通过下列转换中的一或多个转换成 U 的指针或成员指针 (C++17 起)类型,并非到指向私有、受保护或有歧义基类指针的指针转换、限定性转换、函数指针转换 (C++17 起)
- T 是指针或成员指针,或 const 指针的引用 (C++14 起),而 E 是 std::nullptr_t。
try {
throw E;
} catch (const std::overflow_error& e) {
// 若 E 抛出 std::overflow_error 则执行之(“相同类型”规则)
} catch (const std::runtime_error& e) {
// 若 E 抛出 std::underflow_error 则执行之(“基类”规则)
} catch (const std::exception& e) {
// 若 E抛出 std::logic_error 则执行之(“基类”规则)
} catch (...) {
// 若 E 抛出 std::string 或 int 或任何其他无关类型则执行之
}
特别注意的是,全捕获(catch-all)子句 catch (...) 匹配任何类型的异常。若存在,则它(catch-all)必须是 处理块序列中的最后一个 catch 子句。Catch-all 块可用于确保不可能有未捕获异常从提供不抛出异常保证的函数逃逸。
catch (...) {
//our code
}
捕获所有异常的 catch 子句与任意类型的异常都匹配。如果 catch(...) 与其他 catch 子句结合使用,它必须是最后一个,否则,任何跟在它后面的 catch 子句都将不能被匹配。
当 catch 为了处理异常只需要了解异常的类型的时候,异常说明符可以省略形参名;如果处理代码需要已发生异常的类型之外的信息,则异常说明符就包含形参名,catch 使用这个名字访问异常对象。
void g1()
{
try {
//...code
} catch (const std::exception& ) {
// 仅需要匹配发生了那种类型异常,异常处理与对象信息无关要紧
} catch (...) {
// 强制处理异常,与对象类型及信息无关要紧!
}
}
2.5 异常捕获处理级别
若检测所有 catch 子句后仍无匹配,则到外围的 try 块继续异常的传播,如 throw 表达式中所述。若没有剩下的外围 try 块,则执行 std::terminate(此情况下,由实现定义是否完全进行栈回溯:容许抛出未捕获异常导致程序终止而不调用任何析构函数)。
void f()
{
try{
std::invalid_argument e("test");
throw e; //主动抛出一个异常
}catch (const std::exception& e){
//类型不匹配被跳过
}
}
void g()
{
try {
f();
} catch (const std::overflow_error& e) {
// ...
} catch (const std::runtime_error& e) {
// ...
} catch (const std::exception& e) {
// ...
} catch (...) {
// ...,这里捕获
}
}
基类的异常说明符可以用于捕获派生类型的异常对象,而且,异常说明符的静态类型决定 catch 子句可以执行的动作。如果被抛出的异常对象是派生类类型的,但由接受基类类型的 catch 处理,那么,catch 不能使用派生类特有的任何成员。
若派生类的 catch 子句被置于基类的 catch 子句后,则永远不会执行派生类 catch 子句。将异常类型组织成类层次的时候,设计者应谨慎选择应用程序处理异常的粒度级别。
void g1()
{
try {
//...code
} catch (const std::exception& e) {
// 若 f() 抛 std::runtime_error 则执行
} catch (const std::runtime_error& e) {//warning: exception of type 'std::runtime_error' will be caught by earlier handler [-Wexceptions]
// 死代码!
}
}
2.6 抛出对象方式
如果 catch 子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。
如果 catch 形参是引用类型,catch 对象就直接访问异常对象,catch 对象的静态类型可以与 catch 对象所引用的异常对象的动态类型不同。
如果异常说明符不是引用,则 catch 对象是异常对象的副本,如果 catch 对象是基类类型对象而异常对象是派生类型的,就将异常对象分割为它的基类子对象。届时对象(相对于引用)不是多态的。当通过对象而不是引用使用虚函数的时候,对象的静态类型和动态类型相同,函数是虚函数也一样。只有通过引用或指针调用时才发生动态绑定,通过对象调用不进行动态绑定。
当进入一个 catch 子句时,若其形参是异常类型的基类,则它从异常对象的基类子对象进行复制初始化。否则,它从异常对象复制初始化(这个复制服从复制消除规则)。若 catch 子句的形参是引用类型,则对它所做的任何更改都会反映到异常对象之中,且若以 throw; 重抛这个异常,则可为另一处理块所观测到。若形参不是引用,则任何对它的更改都是局域的,且其生存期在处理块退出时结束。
#include <string>
void g2()
{
try {
std::string("abc").substr(10); // 抛出 std::length_error
// } catch (std::exception e) { // 从 std::exception 基类复制初始化
// std::cout << e.what(); // 丢失来自 length_error 的信息
// }
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what(); // 打印来自 length_error 的信息
}
}
C++11 起,在 catch 子句内,可以使用 std::current_exception 把异常捕获到一个 std::exception_ptr 之中,而且可以使用 std::throw_with_nested 来构建嵌套的异常。
2.7 try 块以及处理块内严禁跳转语法使用
goto 和 switch 语句不能用来转移控制进入 try 块以及处理块。若用 goto 退出 try 块,且若这个 goto 所执行的任何块作用域自动变量的析构函数由于抛异常而退出,则那些异常被该变量定义于其中的 try 块所捕捉:
class A1{};
class A2{};
void g3()
{
int i=0;
label:
try {
A1 a1;
try {
A2 a2;
if(i<2)
goto label; // 销毁 a2,然后销毁 a1,再跳到 label
} catch (...) { } // 捕捉来自 a2 析构函数的异常
} catch (...) { } // 捕捉来自 a1 析构函数的异常
}
除了抛出或重抛异常以外,普通的 try 块(非函数 try 块)之后的 catch 子句还可以通过 return、continue、break、goto,或通过抵达其复合语句尾而退出。任何这些情况,都会销毁异常对象(除非存在指代它的 std::exception_ptr 实例)。
2.8 异常捕获及异常处理分离
C++ 的try ...catch异常处理中,需要由问题检测部分抛出一个对象给处理代码,通过这个对象的类型和内容,判定异常信息及作出处理。通过异常我们能够将问题的检测和问题的解决分离,程序的一个部分能够检测出本部分无法解决的问题,这个问题检测部分可以将问题传递给准备处理问题的其他部分,这样程序的问题检测部分可以不必了解如何处理问题。
class exception_def: public std::exception{}; //自定义异常类
void f(const exception_def& e)
{
//code,专门处理异常的函数
}
void g5() {
try {
exception_def e;
throw e; //传递一个自定义对象
} catch (const exception_def& e) { // 多态对象派生类的引用
f(e);
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
在try块体内,执行抛出的时候,就不会执行跟在抛出对象后面的语句,而是将控制从 抛出对象执行语句转移到匹配的 catch,该 catch 可以是同一函数中局部的 catch,也可以在直接或间接调用发生异常的函数的另一个函数中。
2.9 数组及函数类型传递
在c/c++编译中,传递数组或函数类型实参的时候,该实参自动转换为一个指针。被抛出的对象将发生同样的自动转换,因此,不存在数组或函数类型的异常。相反,如果抛出一个数组,被抛出的对象转换为指向数组首元素的指针。
void g6() {
try {
int excep[2]={1,2};
throw excep; //转换为指针
} catch (int e[]) { // 转换为指针
//
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
类似地,如果抛出一个函数,函数被转换为指向该函数的指针,与其这样还不如直接创建一个函数对象来传递。
class OperObj
{
public:
void operator()(){};
};
void g6() {
try {
OperObj e;
throw e; //转换为指针
} catch (OperObj& e) { // 转换为指针
e(); //
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
2.10 异常与指针
用抛出表达式抛出指针总是麻烦的在于抛出中对指针解引用。对指针解引用的结果是一个对象,其类型与指针的类型匹配。如果指针指向继承层次中的一种类型,指针所指对象的类型就有可能与指针的类型不同。无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割,只抛出基类部分。
class exception_PTR: public std::exception{};
void g8() {
std::exception *eptr= new exception_PTR();
try {
//do something
throw eptr; //抛出局部指针
} catch (const std::exception* e) { // 多态对象基类的引用
//只能使用基类的成员函数,派生类内重载的不可用
std::cout << e->what();
}
if(nullptr!=eptr){
delete eptr;
eptr = nullptr;
}
}
如果抛出指针本身,抛出指向局部对象的指针总是错误的,其理由与从函数返回指向局部对象的指针是错误的一样抛出指针的时候,必须确定进入处理代码时指针所指向的对象存在。
void g8() {
try {
char* e = new char[10];
memcpy(e,"test",5);
throw e; //抛出局部指针
delete[] e; //被跳过
e = nullptr;
} catch (char* e) { // 指针异常对象处理
std::cout << std::string(e);
//再处理e指针释放已经晚了
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
如果抛出指向局部对象的指针,而且处理代码在另一函数中,则执行处理代码时指针所指向的对象将不再存在。即使处理代码在同一函数中,也必须确信指针所指向的对象在 catch 处存在。如果指针指向某个在 catch 之前退出的块中的对象,那么,将在 catch 之前撤销该局部对象。
抛出指针通常是个坏主意:抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象。
2.11 自定义异常最好继承标准异常类
在实践中,许多应用程序所抛出的表达式,基类型都来自某个继承层次。标准异常定义在一个继承层次中,标准异常类可以用于许多应用程序。此外,还通过从exception 类或者中间基类派生附加类型来扩充 exception 层次。这些新派生的类可以表示特定于应用程序领域的异常类型。如果我们自定义异常类型时,最好以标准异常为基类来创建一个异常类。
class myExcep : public exception {};
class myExcep : public runtime_error{};
.....
class myExcep : public std::runtime_error {
public:
explicit myExcep (const std::string &s) : std::runtime_error(s) { };
};
通过从标准异常类派生,定义了特定于应用程序的异常类型。像任何层次一样,可以认为异常类按层组织。随着层次的加深,每一层变得更特殊的异常。例如,层次中第一层即最一般的层由 exception 类代表,当捕获这一类型的对象时,我们所知道的只是有些地方出错了。第二层将 exception 特化为两个大类:运行时错误和逻辑错误。用户自定异常类最好不要直接继承exception,而是更细分一下,表示更特化的层中的事件。例如myExcep类表示可能在运行时出现问题的特定于应用程序的事情。
class myExcep : public std::runtime_error {
public:
explicit myExcep (const std::string &s) : std::runtime_error(s) { };
};
void myExcepf()
{
try{
//our code
}catch (const myExcep& e){
}catch (const std::runtime_error& e){
}catch (const std::exception& e){
}
};
用户和使用标准库类相同的方法使用自己的异常类。程序的一个部分抛出某个这些类型的对象,程序的另一部分捕获并处理指出的问题。
另外使用静态类型抛出对象不成问题。当抛出一个异常的时候,通常在抛出点构造将抛出的对象,该对象表示出了什么问题,所以我们知道确切的异常类型。
void g4() {
try {
std::cout << "Throwing an integer exception...\n";
throw 42; //主动抛出int型对象
} catch (int i) {
std::cout << " the integer exception was caught, with value: " << i << '\n';
}
}
2.12 异常处理之“栈展开”
抛出异常的时候,将暂停当前函数的执行,开始查找匹配的 catch 子句。首先检查 throw 本身是否在 try 块内部,如果是,检查与该 catch 相关的catch 子句,看是否其中之一与抛出对象相匹配。如果找到匹配的 catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内在并撤销局部对象),并且继续在调用函数中查找。
如果对抛出异常的函数的调用是在 try 块中,则检查与该 try 相关的catch 子句。如果找到匹配的 catch,就处理异常;如果找不到匹配的 catch,调用函数也退出,并且继续在调用这个函数的函数中查找。
这个过程,称之为栈展开(stack unwinding),沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句。只要找到能够处理异常的 catch 子句,就进入该 catch 子句,并在该处理代码中继续执行。当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行。
void f()
{
try{
std::invalid_argument e("test");
throw e; //主动抛出一个异常
}catch (const std::exception& e){ //[1]
//类型不匹配被跳过
}
}
void g()
{
try {
f();
} catch (const std::overflow_error& e) {//[2]
// ...
} catch (const std::runtime_error& e) {//[3]
// ...
} catch (const std::exception& e) {//[4]
// ...
} catch (...) {//[5]
// ...,这里捕获
}
//terminate //[6]
}
栈展开期间,提早退出包含 throw 的函数和调用链中可能的其他函数。一般而言,这些函数已经创建了可以在退出函数时撤销的局部对象。因异常而退出函数时,编译器保证适当地撤销局部对象。每个函数退出的时候,它的局部存储都被释放,在释放内存之前,撤销在异常发生之前创建的所有对象。如果局部对象是类类型的,就自动调用该对象的析构函数。通常,编译器不撤销内置类型的对象。
栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。例如,一个块可以通过调用 new 动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。
由类类型对象分配的资源一般会被适当地释放。运行局部对象的析构函数,由类类型对象分配的资源通常由它们的析构函数释放。建议面对异常使用类管理资源分配的编程技术。
程序不能不处理异常,程序存在未捕获的异常就终止程序。异常是足够重要的、使程序不能继续正常执行的事件。如果找不到匹配的 catch,程序就调用库函数 terminate。
2.13 类的构造、析构函数与异常处理
析构函数应该从不抛出异常, 栈展开期间会经常执行析构函数。在执行析构函数的时候,已经引发了异常但还没有处理它。如果在这个过程中析构函数本身抛出新的异常,将会导致调用标准库 terminate 函数。一般而言,terminate函数将调用 abort 函数,强制从整个程序非正常退出。
因为 terminate 函数结束程序,所以析构函数做任何可能导致异常的事情通常都是非常糟糕的主意。在实践中,因为析构函数释放资源,所以它不太可能抛出异常。标准库类型都保证它们的析构函数不会引发异常。自定义类型时,最好不要再析构函数再做抛出异常处理,以免覆盖旧异常。
异常与构造函数,与析构函数不同,构造函数内部所做的事情经常会抛出异常。如果在构造函数对象的时候发生异常,则该对象可能只是部分被构造,它的一些成员可能已经初始化,而另一些成员在异常发生之前还没有初始化。即使对象只是部分被构造了,也要保证将会适当地撤销已构造的成员。
class ExcepTest
{
public:
ExcepTest();
~ExcepTest();
private:
char* p1;
char* p2;
std::string *p3;
std::string *p4;
};
ExcepTest::ExcepTest()
{
try {//防止构造时异常出现内存泄漏
//do something
}
catch (...) {
delete p4; p4 = nullptr;//不良排版,为了节省行数显示
delete p3; p3 = nullptr;
delete p2; p2 = nullptr;
delete p1; p1 = nullptr;
}
}
ExcepTest::~ExcepTest()
{
if (nullptr != p4) { delete p4; p4 = nullptr;}//不良排版,为了节省行数显示
if (nullptr != p3) { delete p3; p3 = nullptr;}
if (nullptr != p2) { delete p3; p3 = nullptr;}
if (nullptr != p1) { delete p1; p3 = nullptr;}
}
异常可能发生在构造函数中,或者发生在处理构造函数初始化式的时候。在进入构造函数函数体之前处理构造函数初始化式,构造函数函数体内部的 catch 子句不能处理在处理构造函数初始化时可能发生的异常。为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数 try块。可以使用函数测试块将一组 catch 子句与函数联成一个整体。
class ExcepTest2
{
public:
//构造函数要处理来自构造函数初始化式的异常,最好是将构造函数编写为函数测试块。
ExcepTest2(char* pc_, int* pi_)
try : p1(pc_),p2(pi_){
}
catch(const std::exception& e)
{
destroy();
std::cerr << e.what() << '\n';
};
~ExcepTest2(){
destroy();
};
private:
void destroy(){
if(nullptr!=p2){ delete p2; p2 = nullptr;};
if(nullptr!=p1){ delete p1; p1 = nullptr;};
}
private:
char* p1;
int* p2;
};
类似地,在初始化数组或其他容器类型的元素的时候,也可能发生异常,同样,也要保证将会适当地撤销已构造的元素。
2.14 重新抛出异常
有可能单个 catch 不能完全处理一个异常。在进行了一些校正行动之后,catch 可能确定该异常必须由函数调用链中更上层的函数来处理,catch 可以通过重新抛出将异常传递函数调用链中更上层的函数。重新抛出是后面不跟类型或表达式的一个 throw:
throw;
空 throw 语句将重新抛出异常对象,它只能出现在 catch 或者从 catch调用的函数中。如果在处理代码不活动时碰到空 throw,就调用 terminate 函数。
class my_error : public std::exception
{
public:
enum status{client_error,server_error,char_error,port_error};
void reset_status(status st_){ st = st_;};
private:
status st;
};
void g10()
{
try {
throw my_error();// 抛出 my_error
} catch (my_error &eObj) { // specifier is a reference type
//异常判断,继续排除
eObj.reset_status(my_error::server_error);// 修改异常对象
std::cout <<"my_error reset\n";
throw ; // 直接抛给上层调用
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
void g11()
{
try {
g10();
}catch(...)
{
std::cout <<"error from g10\n";
}
}
//
g11();
//out log
my_error reset
error from g10
虽然重新抛出不指定自己的异常,但仍然将一个异常对象沿链向上传递,被抛出的异常是原来的异常对象,而不是 catch 形参。当 catch 形参是基类类型的时候,我们不知道由重新抛出表达式抛出的实际类型,该类型取决于异常对象的动态类型,而不是 catch 形参的静态类型。例如,来自带基类类型形参 catch的重新抛出,可能实际抛出一个派生类型的对象。一般而言,catch 可以改变它的形参。在改变它的形参之后,如果 catch 重新抛出异常,那么,只有当异常说明符是引用的时候,才会传播那些改变。
catch(...) 经常与重新抛出表达式结合使用,catch 完成可做的所有局部工作,然后重新抛出异常:
void g12() {
try {
// 异常捕获触发
}
catch (...) {
// 不再本函数处理,抛给上层函数统一处理
throw;
}
}
2.15 用类管理资源分配
为确保析构函数的运行正确,使程序更为异常安全的(异常安全的意味着,即使发生异常,程序也能正确操作)。在这种情况下,析构函数的“安全”来自于保证“如果发生异常,被分配的任何资源都适当地释放”。通过定义一个类来封闭资源的分配和释放,可以保证正确释放资源。这一技术常称为“资源分配即初始化”,简称 RAII。
unique_ptr (C++11) 拥有独有对象所有权语义的智能指针 (类模板)
shared_ptr (C++11) 拥有共享对象所有权语义的智能指针 (类模板)
weak_ptr (C++11) 到 std::shared_ptr 所管理对象的弱引用 (类模板)
auto_ptr (C++17中移除) 拥有严格对象所有权语义的智能指针 (类模板)
应该设计资源管理类,以便 构造函数分配资源而析构函数释放资源。想要分配资源的时候,就定义该类类型的对象。如果不发生异常,就在获得资源的对象超出作用域的进修释放资源。更为重要的是,如果在创建了对象之后但在它超出作用域之前发生异常,那么,编译器保证撤销该对象,作为展开定义对象的作用域的一部分。
三、异常处理辅助关键词
3.1 noexcept 说明符
见“c/c++开发,无可避免的自定义类类型(篇七).碎银几两_py_free-物联智能的博客-CSDN博客”的“一、noexcept说明符与类成员函数”章节。
3.2 动态异常说明-throw
动态异常说明throw(c++17前),列出函数可能直接或间接抛出的异常。
- 如果某个函数的声明将类型 T 列于其动态异常说明,那么该函数可以抛出该类型或从该类型派生的类型的异常。
- 没有类型标识列表的动态异常说明(即 throw())是不抛出的。
- 带有不抛出动态异常说明的函数不允许任何异常。
- 异常说明中不能出现不完整类型,除 cv void* 之外的到不完整类型的指针或引用,以及右值引用类型。
- 如果使用数组和函数类型,那么它们会被调整到对应的指针类型。
- 允许形参包。 (C++11 起)
- 动态异常说明不会被认为是函数类型的一部分。
- 如果函数抛出了没有列于其异常说明的类型的异常,那么调用函数 std::unexpected。
- 默认的该函数会调用 std::terminate,但它可以(通过 std::set_unexpected)被替换成可能调用 std::terminate 或抛出异常的用户提供的函数。
- 如果异常说明接受从 std::unexpected 抛出的异常,那么栈回溯照常持续。如果它不被接受,但异常说明允许 std::bad_exception,那么抛出 std::bad_exception。否则,调用 std::terminate。
#include <exception>
#include <assert.h>
static_assert(__cplusplus < 201703,"ISO C++17");
class X {};
class Y {};
class Z : public X {};
class W {};
void func1() throw(X, Y)
{
int n = 0;
if (n) throw X(); // OK
if (n) throw Z(); // OK
throw W(); // 将调用 std::unexpected()
}
void throw_test()
{
std::set_unexpected([]
{
std::cout << "unexpected!" << std::endl; // 需要清除缓冲区
std::abort();
});
func1();
}
通常不建议在函数try块体内主动显式调用throw抛出异常对象,而是采用间接、隐式方式抛出异常。
3.3 throw与潜在异常
每个函数 f
,函数指针 pf
,与成员函数指针 pmf
都拥有一个“潜在异常集合”,它由可能抛出的类型构成。所有类型的集合指示可能抛出任何异常。对于隐式声明的特殊成员函数(构造函数、赋值运算符和析构函数),以及对于继承的构造函数 (C++11 起),潜在异常的集合是它们会调用的所有函数的潜在异常集合的并集:非变体非静态数据成员、直接基类及适当场合的虚基类的构造函数/赋值运算符/析构函数(还包括默认实参表达式)。
每个表达式 e 都拥有一个潜在异常集合。如果 e 是核心常量表达式,那么集合为空,否则集合是 e 的全部直接子表达式(包含默认实参表达式)的潜在异常集合的并集,再与下列取决于 e 的形式的另一个集合合并。
1) 如果 e 是一个函数调用表达式,令 g 代表被调用的函数、函数指针或成员函数指针,那么:
◦如果 g 的声明使用了动态异常说明,那么添加 g 的潜在异常集合到集合;
◦如果 g 的声明使用了 noexcept(true),那么集合为空;(C++11 起)
◦否则,该集合是所有类型的集合。
2) 如果 e 隐式调用了某个函数(它是运算符表达式且该运算符被重载,是 new 表达式且其分配函数被重载,或是完整表达式且调用了临时量的析构函数),那么该集合是这个函数的集合。
3) 如果 e 是一个 throw 表达式,那么该集合是以其操作数所初始化的异常,或对于重抛出表达式(无操作数者)是所有异常的集合。
4) 如果 e 是对多态类型引用的 dynamic_cast,那么该集合由 std::bad_cast 组成。
5) 如果 e 是应用到多解引用指向多态类型指针的 typeid,那么该集合由 std::bad_typeid 组成。
6) 如果 e 是一个拥有非常量数组大小的 new 表达式,且选择的分配函数拥有非空的潜在异常集合,那么该集合由 std::bad_array_new_length 组成。 (C++11 起)
void f() throw(int); // f() 的集合是“int”
void g(); // g() 的集合是所有类型的集合
struct A { A(); }; // “new A”的集合是所有类型的集合
struct B { B() noexcept; }; // “B()”的集合为空
struct D() { D() throw (double); }; // “new D”的集合是所有类型的集合
3.4 throw与成员函数
所有隐式声明的成员函数和继承的构造函数 (C++11 起)都拥有异常说明,成员函数声明的异常说明跟在函数形参表之后,选择如下:
◦如果潜在异常的集合是类型全集,那么隐式异常说明允许所有异常(该异常说明被认为存在,即使它不可用代码表达,且表现如同无异常说明) (C++11 前)是 noexcept(false) (C++11 起)。
◦否则,如果潜在异常的集合非空,那么隐式异常说明列出每个来自该集合的类型
◦否则,隐式异常说明是 throw() (C++11 前)noexcept(true) (C++11 起)。
//
struct A
{
A(int = (A(5), 0)) noexcept;
A(const A&) throw();
A(A&&) throw();
~A() throw(X);
};
struct B
{
B() throw();
B(const B&) = default; // 异常说明是“noexcept(true)”
B(B&&, int = (throw Y(), 0)) noexcept;
~B() throw(Y);
};
int n = 7;
struct D : public A, public B
{
int * p = new (std::nothrow) int[n];
// D 拥有下列隐式声明的成员:
// D::D() throw(X, std::bad_array_new_length);
// D::D(const D&) noexcept(true);
// D::D(D&&) throw(Y);
// D::~D() throw(X, Y);
};
注意,在 const 成员函数声明中,异常说明跟在 const 限定符之后。
3.5 异常说明throw与虚函数
基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的可抛出异常。例如:
class Base {
public:
virtual double f1(double) throw (){};
virtual int f2(int) throw (std::logic_error){};
virtual std::string f3() throw(std::logic_error, std::runtime_error){};
};
class Derived : public Base {
public:
// error: 异常列表范围收窄
// double f1(double) throw (std::underflow_error){};
// ok: 相同异常列表
int f2(int) throw (std::logic_error){};
// ok: 异常列表范围变大
std::string f3() throw (){};
};
派生类中 f1 的声明是错误的,因为它的异常说明在基类 f1 版本列出的异常中增加了一个异常。派生类不能在异常说明列表中增加异常,原因在于,继承层次的用户应该能够编写依赖于该说明列表的代码。如果通过基类指针或引用进行函数调用,那么,这些类的用户所涉及的应该只是在基类中指定的异常。PS:关键词noexcept也类似。
通过派生类抛出的异常限制为由基类所列出的那些,在编写代码时就可以知道必须处理哪些异常。代码可以依赖于这样一个事实:基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集。
异常说明是函数类型的一部分。这样,也可以在函数指针的定义中提供异常说明:
void (*pf)(int) throw(runtime_error);
这个声明是说,pf 指向接受 int 值的函数,该函数返回 void 对象,该函数只能抛出 runtime_error 类型的异常。如果不提供异常说明,该指针就可以指向能够抛出任意类型异常的具有匹配类型的函数。
3.6 std::terminate
//定义于头文件 <exception>
void terminate(); //(C++11 前)
[[noreturn]] void terminate() noexcept; //(C++11 起)
C++ 运行时在程序因任何下列原因不能继续时调用 std::terminate() :
1) 未捕捉抛出的异常(此情况下是否进行任何栈回溯是实现定义的)
2) 在处理仍未经由异常捕捉的异常时(例如由某局部对象的析构函数,或构造 catch 子句参数的复制构造函数抛出),由异常处理机制所直接调用
3) 静态或线程局域 (C++11 起)对象的构造函数或析构函数抛出异常
4) 以 std::atexit 或 std::at_quick_exit (C++11 起)注册的函数抛出异常
5) 违反动态异常说明,并执行了 std::unexpected 的默认处理函数 (C++17 前)
6) std::unexpected 的非默认处理函数抛出了违背先前所违背动态异常规定的异常,若这种规定不包含 std::bad_exception (C++17 前)
7) 违反 noexcept 说明(此情况下是否进行任何栈回溯是实现定义的)(C++11 起)
8) 为一个不保有被捕获异常的对象调用 std::nested_exception::rethrow_nested(C++11 起)
9) 从 std::thread 的起始函数抛出异常(C++11 起)
10) 可结合的 std::thread 被析构或赋值(C++11 起)
11) std::condition_variable::wait、 std::condition_variable::wait_until 或 std::condition_variable::wait_for 无法达成其前条件(例如若重锁定互斥抛出)(C++11 起)
12) 并行算法所调用的函数经由未捕捉异常退出,且该执行策略指定了终止。(C++17 起)
亦可直接从程序调用 std::terminate() 。
任何情况下, std::terminate 调用当前安装的 std::terminate_handler 。默认的std::terminate_handler 调用 std::abort 。
*若析构函数在栈回溯时重设 terminate_handler ,且后面的回溯导致调用 terminate ,则在 throw 表达式的结尾安装的处理函数会得到调用。(注意:重抛出是否应用新处理函数是有歧义的) (C++11 前)
*若析构函数在栈回溯时重设 terminate_handler ,则若后面的栈回溯导致调用 terminate ,调用哪个处理函数是未指定的。 (C++11 起)
3.7 std::unexpected
std::unexpected() 为 C++ 运行时在违背动态异常规定时调用:从其异常规定禁止此类型异常的函数抛出该类型异常。
//定义于头文件 <exception>
void unexpected(); //(C++11 前)
[[noreturn]] void unexpected(); //(C++11 起)(弃用)(C++17 中移除)
可从程序直接调用 std::unexpected() 。 任一情况下, std::unexpected 调用当前安装的 std::unexpected_handler 。默认的 std::unexpected_handler 调用 std::terminate 。
*若析构函数在栈回溯期间重置 unexpected_handler 且之后的回溯导致调用 unexpected ,则将调用于 throw 表达式结尾安装的处理函数。(注意:重抛出是否应用新的处理函数是有歧义的) (C++11 前)
*若析构函数在栈回溯期间重置 unexpected_handler ,则若之后的回溯导致调用 unexpected ,则调用哪个处理函数是未指定的。
四、关于断言-宏assert
4.1 宏 assert 定义
C/C++定义了一个调试程序时经常使用的宏assert:
//定义于头文件 <cassert>或 <assert.h>
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif
在程序运行时它计算括号内的表达式,如果表达式为FALSE (0),程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。如上所示,宏 assert 的定义依赖于标准库中没有定义的另一个名为 NDEBUG 宏。
4.2 宏 assert 与NDEBUG 宏配套运用
若没有定义名为 NDEBUG 的宏,则 assert 将其参数(必须拥有标量类型)与零比较相等。若相等,则 assert 在标准错误输出上输出实现指定的诊断信息,并调用 std::abort 。诊断信息要求包含 expression 的文本,还有预定义变量 __func__ 与 (C++11 起)预定义宏 __FILE__、__LINE__ 的值。
在最后定义或重定义(即包含了头文件 <cassert> 或 <assert.h> ) assert 位置定义了 NDEBUG,表达式 assert(E) 保证为常量子表达式,即E按语境转换成 bool 后,是求值为 true 的常量子表达式。
void assertion_test(void)
{
assert(1+1==2);
std::cout << "Execution continues past the first assert\n";
assert(1+1==3); //断言捕获,直接调用abort告警及退出
std::cout << "Execution continues past the second assert\n";
//do other
std::cout << "dosomething!\n"; //程序退出,还得到执行
}
int main(int argc, char* argv[])
{
assertion_test();
std::cout <<"main finish!\n";
return 0;
}
编译测试g++ main.cpp test*.cpp -o test.exe -std=c++11,输出
编译测试g++ main.cpp test*.cpp -o test.exe -std=c++11 -DNDEBUG,输出
因此断言宏assert 是与宏DNDEBUG配套使用的,主要用于在调试程序中进行异常捕获,大多数时候是用于函数入口形参监测,例如用于指针校验。
struct TestA
{
/* data */
char* pc;
};
void test_assert(TestA* ptr)
{
if(nullptr==ptr) return; //可预见的直接函数自行判定
assert(nullptr!=ptr->pc); //形参内部的采用断言检测
//do other
}
4.3 注意宏assert替换表达式异常
因为 assert(E)是仿函数宏,在使用是,需要注意常量表达式E被预编译替换时会造成它未被括号保护而不能正确实现我们代码意愿:
#include <complex>
void test_assert_def(void)
{
std::complex<double> c;
// assert(c == std::complex<double>{0, 0}); // 错误,未被括号保护的逗号都被转译成宏参数的分隔符
assert((c == std::complex<double>{0, 0})); // OK
};
使用assert的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过在包含#include <assert.h>的语句之前插入 #define NDEBUG来禁用assert调用,示例代码如下,或者在编译指令添加-DNDEBUG,又或在编译工程中追加NDEBUG:
#include <stdio.h>
#define NDEBUG
#include <assert.h>
4.4 编译断言-static_assert
宏assert主要是用于在运行时进行断言。在C++11标准中,引入了 static_assert宏来实现编译期断言。static_assert使用起来非常简单,它接收两个参数,一个是断言表达式,这个表达式通常需要返回一个bool值;一个则是警告信息,它通常也就是一段字符串。该关键字的用法类似C++11标准之前开源库 Boost内置的BOOST_STATIC_ASSERT断言机制类似,利用1/(e)这个表达式来判定:
#define assert_static(e) \
do{ \
enum { assert_static = 1/(e) }; \
)while(0)
static_assert可以用于任何名字空间,在预处理阶段,static_assert 宏会被展开成名为 _Static_assert 的 C 关键字。该关键字以类似“函数调用”的形式在 C 代码中使用,它的第一个参数接收一个常量表达式。程序在被编译时,编译器会对该表达式进行求值,并将所得结果与数字 0 进行比较。若两者相等,则程序终止编译,并会将通过第二个参数指定的错误信息,与断言失败信息合并输出。若两者不相等,程序会被正常编译,且该关键字对应的 C 代码不会生成任何对应的机器指令。
int bit_deal (int& a) (
static_assert(sizeof(int) >=4,"the parameters of bit should have at least 4 width.");
);
该程序限定程序在被编译时,其所在平台上 int 类型的宽度需要大于等于 4 字节,编译会被终止,对应的错误信息也会被打印出来:
error: static assertion failed: "the parameters of bit should have at least 4 width."
这样的错误信息就非常清楚,也非常有利于程序员排错。一般来说,我们会在程序运行前使用静态断言,来检查它所需要满足的一系列要求。如上面例子,通过静态断言,开发者便可以提前得知,程序如果运行在当前平台上,是否能正常工作。
类似的用例还有很多,比如判断 char 类型的默认符号性,或是判断指针类型与 int 类型的宽度是否相等,或是判断某个结构体的大小是否满足预期要求,等等。这些都是可能影响 c/c++ 程序运行正确性的因素,而通过静态断言,它们都可以在编译时被提前检测出来。
通常建议,static_assert写在函数体外通常是较好的选择,这让代码阅读者可以较容易发 现static_assert为断言而非用户定义的函数。
另外必须注意的是,static_assert的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式。如果读者使用了变量,则会导致错误,如:
int bit_deal (int a) (
static_assert( a>=1,"the parameters of a should >=1.");
);
上面使用了参数变量a ,因而static_assert无法通过编译。如果需要对变量进行检查,就要是实现运行时的检查,使用assert宏了。
五、异常处理建议
【1】区分异常和非异常
不要把业务逻辑实现错误归并到程序错误来。例如某个数据达到阀值,应该采用条件判定来实现其业务逻辑处理,不要抛出异常来强制程序运行。需要正确区分对待错误的处理方法(例如:例外和错误代码)和技术(例如:安全保证、关键词、先决条件 、 后置条件 )。.
【2】确保异常处理确切需要而非偷懒
异常处理时引入效率代价的,有效使用异常处理需要理解业务逻辑及代码逻辑:在抛出异常时会发生什么,在捕获异常时又会发生什么,还有用来传递错误的对象的含义等。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。异常以类似于将实参传递给函数的方式抛出和捕获。异常可以是可传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。
【3】对于可穷举例外采用逻辑条件处理而非异常处理
当我们可以预判到该功能函数执行会出现的错误情况,并能通过条件分支来处理它时,就不必要采用异常处理的方式。
【4】异常处理通过反复调试后消除一些异常级别
在项目实践中,通常一开始因为业务场景、业务逻辑理解问题,不得以引入众多异常处理层级,随着业务消化及版本迭代,很多异常被提前抑制了,那么对于的异常处理层级也应该同步撤除。
六、演示源代码
编译指令:g++ main.cpp test*.cpp -o test.exe -std=c++11 (可选:-DNDEBUG)
main.cpp
#include "test1.h"
#include <iostream>
int main(int argc, char* argv[])
{
assertion_test();
exception_test();
std::cout <<"main finish!\n";
return 0;
}
test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
void assertion_test(void);
void exception_test(void);
#endif //_TEST_1_H_
test1.cpp
#include "test1.h"
#include <cassert>
#include <iostream>
void assertion_test(void)
{
assert(1+1==2);
std::cout << "Execution continues past the first assert\n";
assert(1+1==3); //断言捕获,直接返回main退出
std::cout << "Execution continues past the second assert\n";
//do other
std::cout << "dosomething!\n"; //程序退出,还得到执行
}
struct TestA
{
/* data */
char* pc;
};
void test_assert(TestA* ptr)
{
if(nullptr==ptr) return; //可预见的直接函数自行判定
assert(nullptr!=ptr->pc); //形参内部的采用断言检测
//do other
}
#include <complex>
void test_assert_def(void)
{
std::complex<double> c;
// assert(c == std::complex<double>{0, 0}); // 错误,未被括号保护的逗号都被转译成宏参数的分隔符
assert((c == std::complex<double>{0, 0})); // OK
};
class myExcep : public std::runtime_error {
public:
explicit myExcep (const std::string &s) : std::runtime_error(s) { };
};
void myExcepf()
{
try{
//our code
}catch (const myExcep& e){
}catch (const std::runtime_error& e){
}catch (const std::exception& e){
}
};
void f()
{
try{
std::invalid_argument e("test");
throw e;
}catch (const std::exception& e){
}
}
void g()
{
try {
f();
} catch (const std::overflow_error& e) {
// ...
} catch (const std::runtime_error& e) {
// ...
} catch (const std::exception& e) {
// ...
} catch (...) {
// ...
}
}
void g1()
{
try {
//...code
} catch (const std::exception& e) {
// 若 f() 抛 std::runtime_error 则执行
} catch (const std::runtime_error& e) {
// 死代码!
}
}
#include <string>
void g2()
{
try {
std::string("abc").substr(10); // 抛出 std::length_error
// } catch (std::exception e) { // 从 std::exception 基类复制初始化
// std::cout << e.what(); // 丢失来自 length_error 的信息
// }
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what(); // 打印来自 length_error 的信息
}
}
class A1{};
class A2{};
void g3()
{
int i=0;
label:
try {
A1 a1;
try {
A2 a2;
if(i<2)
goto label; // 销毁 a2,然后销毁 a1,再跳到 label
} catch (...) { } // 捕捉来自 a2 析构函数的异常
} catch (...) { } // 捕捉来自 a1 析构函数的异常
}
#include <iostream>
#include <vector>
void g4() {
try {
std::cout << "Throwing an integer exception...\n";
throw 42; //主动抛出int型对象
} catch (int i) {
std::cout << " the integer exception was caught, with value: " << i << '\n';
}
//
try {
std::cout << "Creating a vector of size 5... \n";
std::vector<int> v(5);
std::cout << "Accessing the 11th element of the vector...\n";
std::cout << v.at(10); // vector::at() 抛出 std::out_of_range
} catch (const std::exception& e) { // 按基类的引用捕获
std::cout << " a standard exception was caught, with message '"
<< e.what() << "'\n";
}
}
class exception_def: public std::exception{};
void f(const exception_def& e)
{
//code
}
void g5() {
try {
exception_def e;
throw e;
} catch (const exception_def& e) { // 多态对象派生类的引用
f(e);
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
void g6() {
try {
int excep[2]={1,2};
throw excep; //转换为指针
} catch (int e[]) { // 转换为指针
//
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
class OperObj
{
public:
void operator()(){};
};
void g7() {
try {
OperObj e;
throw e; //转换为指针
} catch (OperObj& e) { // 转换为指针
e(); //
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
class exception_PTR: public std::exception{};
void g8() {
std::exception *eptr= new exception_PTR();
try {
//do something
throw eptr; //抛出局部指针
} catch (const std::exception* e) { // 多态对象基类的引用
//只能使用基类的成员函数,派生类内重载的不可用
std::cout << e->what();
}
if(nullptr!=eptr){
delete eptr;
eptr = nullptr;
}
}
void g9() {
try {
char* e = new char[10];
memcpy(e,"test",5);
throw e; //抛出局部指针
delete[] e; //被跳过
e = nullptr;
} catch (char* e) { // 指针异常对象处理
std::cout << std::string(e);
//再处理e指针释放已经晚了
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
class ExcepTest
{
public:
ExcepTest();
~ExcepTest();
private:
char* p1;
char* p2;
std::string *p3;
std::string *p4;
};
ExcepTest::ExcepTest()
{
try {//防止构造时异常出现内存泄漏
//do something
}
catch (...) {
delete p4; p4 = nullptr;//不良排版,为了节省行数显示
delete p3; p3 = nullptr;
delete p2; p2 = nullptr;
delete p1; p1 = nullptr;
}
}
ExcepTest::~ExcepTest()
{
if (nullptr != p4) { delete p4; p4 = nullptr;}//不良排版,为了节省行数显示
if (nullptr != p3) { delete p3; p3 = nullptr;}
if (nullptr != p2) { delete p3; p3 = nullptr;}
if (nullptr != p1) { delete p1; p3 = nullptr;}
}
class ExcepTest2
{
public:
ExcepTest2(char* pc_, int* pi_)
try : p1(pc_),p2(pi_){
}
catch(const std::exception& e)
{
destroy();
std::cerr << e.what() << '\n';
};
~ExcepTest2(){
destroy();
};
private:
void destroy(){
if(nullptr!=p2){ delete p2; p2 = nullptr;};
if(nullptr!=p1){ delete p1; p1 = nullptr;};
}
private:
char* p1;
int* p2;
};
class my_error : public std::exception
{
public:
enum status{client_error,server_error,char_error,port_error};
void reset_status(status st_){ st = st_;};
private:
status st;
};
void g10()
{
try {
throw my_error();// 抛出 my_error
} catch (my_error &eObj) { // specifier is a reference type
//异常判断,继续排除
eObj.reset_status(my_error::server_error);// 修改异常对象
std::cout <<"my_error reset\n";
throw ; // 直接抛给上层调用
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
void g11()
{
try {
g10();
}catch(...)
{
std::cout <<"error from g10\n";
}
}
void g12() {
try {
// 异常捕获触发
}
catch (...) {
// 不再本函数处理,抛给上层函数统一处理
throw;
}
}
#include <exception>
#include <assert.h>
static_assert(__cplusplus < 201703,"ISO C++17");
class X {};
class Y {};
class Z : public X {};
class W {};
void func1() throw(X, Y)
{
int n = 0;
if (n) throw X(); // OK
if (n) throw Z(); // OK
throw W(); // 将调用 std::unexpected()
}
void throw_test()
{
std::set_unexpected([]
{
std::cout << "unexpected!" << std::endl; // 需要清除缓冲区
std::abort();
});
func1();
}
class Base {
public:
virtual double f1(double) throw (){};
virtual int f2(int) throw (std::logic_error){};
virtual std::string f3() throw(std::logic_error, std::runtime_error){};
};
class Derived : public Base {
public:
// error: 异常列表范围收窄
// double f1(double) throw (std::underflow_error){};
// ok: 相同异常列表
int f2(int) throw (std::logic_error){};
// ok: 异常列表范围变大
std::string f3() throw (){};
};
void exception_test(void)
{
g7();
g11();
throw_test();
}