编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.基础语法
  • 02.运算符和表达式
  • 03.判断和循环
  • 04.函数实践
  • 05.指针和引用
  • 06.数组和容器
  • 07.类和对象
  • 08.继承和派生
  • 09.多态与虚函数
  • 10.多线程和并发
  • 11.线程安全锁
  • 12.内存分配堆和栈
  • 13.IO流与文件
  • 14.异常处理
  • 15.STL标准模板库

14.异常处理

目录介绍

  • 14.1 异常入门介绍
    • 14.1.1 异常概念解释
    • 14.1.2 运行时异常案例
    • 14.1.3 捕获异常
    • 14.1.4 发生异常位置
  • 14.2 多级catch匹配
    • 14.2.1 多级catch使用
    • 14.2.2 匹配中类型转换
  • 14.3 throw抛出异常
    • 14.3.1 异常抛出概念
    • 14.3.2 throw抛异常用法
    • 14.3.3 throw异常规范
  • 14.4 exception异常
    • 14.4.1 抛出标准异常
    • 14.4.2 exception类介绍
    • 14.4.3 exception派生类
    • 14.4.4 定义新的异常

14.1 异常入门介绍

14.1.1 异常概念解释

程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:

  1. 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。

  2. 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。

  3. 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常(Exception)机制就是为解决运行时错误而引入的。

运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。

C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。

14.1.2 运行时异常案例

一个发生运行时错误的程序:

int main() {
    string str = "https://yccoding.com/";
    char ch1 = str[100];  //下标越界,ch1为垃圾值
    cout << ch1 << endl;
    char ch2 = str.at(100);  //下标越界,抛出异常
    cout << ch2 << endl;
    return 0;
}

运行代码后程序崩溃。崩溃日志如下所示:

ch1
libc++abi: terminating due to uncaught exception of type std::out_of_range: basic_string
Abort trap: 6

分析一下原因:

at() 是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与[ ]不同,at() 会检查下标是否越界,如果越界就抛出一个异常;而[ ]不做检查,不管下标是多少都会照常访问。

所谓抛出异常,就是报告一个运行时错误,程序员可以根据错误信息来进一步处理。

at() 函数检测到下标越界会抛出一个异常,这个异常可以由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,也即终止程序执行。

14.1.3 捕获异常

可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:

try{
    // 可能抛出异常的语句
}catch(exceptionType variable){
    // 处理异常的语句
}

try和catch都是 C++ 中的关键字,后跟语句块,不能省略{ }。

  1. try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。
  2. catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。

catch 关键字后面的exceptionType variable指明了当前 catch 可以处理的异常类型,以及具体的出错信息。

捕获异常的语句案例如下:

int main() {
    string str = "https://yccoding.com/";
    try {
        char ch1 = str[100];  //下标越界,ch1为垃圾值
        cout << "ch1" << ch1 << endl;
    } catch (exception e) {
        cout << "exception1 " << e.what() << endl;
    }
    try {
        char ch2 = str.at(100);  //下标越界,抛出异常
        cout << "ch2" << ch2 << endl;
    } catch (exception e) {
        cout << "exception2 " << e.what() << endl;
    }
    return 0;
}

打印结果如下所示:

ch1
exception2 std::exception

结果分析,我们大概可以得出这样的结论:

  1. 第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
  2. 第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。

14.1.4 发生异常位置

异常可以发生在当前的 try 块中,也可以发生在 try 块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try 检测到。

1.下面的例子演示了 try 块中直接发生的异常:

void test1() {
    try{
        throw "Unknown Exception";  //抛出异常
        cout<<"This statement will not be executed."<<endl;
    }catch(const char* &e){
        cout<<e<<endl;
    }
}

throw关键字用来抛出一个异常,这个异常会被 try 检测到,进而被 catch 捕获。

2.下面的例子演示了 try 块中调用的某个函数中发生了异常:

void func() {
    throw "Unknown Exception";  //抛出异常
    cout << "[1]This statement will not be executed." << endl;
}

void test2() {
    try {
        func();
        cout << "[2]This statement will not be executed." << endl;
    } catch (const char *&e) {
        cout << e << endl;
    }
}

func() 在 try 块中被调用,它抛出的异常会被 try 检测到,进而被 catch 捕获。从运行结果可以看出,func() 中的 cout 和 try 中的 cout 都没有被执行。

3.try 块中调用了某个函数,该函数又调用了另外的一个函数,这个另外的函数抛出了异常:

void func_inner() {
    throw "Unknown Exception";  //抛出异常
    cout << "[1]This statement will not be executed." << endl;
}

void func_outer() {
    func_inner();
    cout << "[2]This statement will not be executed." << endl;
}

void test3() {
    try {
        func_outer();
        cout << "[3]This statement will not be executed." << endl;
    } catch (const char *&e) {
        cout << e << endl;
    }
}

发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 try 才停止。在这个回退过程中,调用链中剩下的代码(所有函数中未被执行的代码)都会被跳过,没有执行的机会了。

14.2 多级catch匹配

14.2.1 多级catch使用

一个 try 对应一个 catch,这只是最简单的形式。其实,一个 try 后面可以跟多个 catch:

try{
    //可能抛出异常的语句
}catch (exception_type_1 e){
    //处理异常的语句
}catch (exception_type_2 e){
    //处理异常的语句
}
//其他的catch
catch (exception_type_n e){
    //处理异常的语句
}

当异常发生时,程序会按照从上到下的顺序,将异常类型和 catch 所能接收的类型逐个匹配。一旦找到类型匹配的 catch 就停止检索,并将异常交给当前的 catch 处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的 catch,就只能交给系统处理,终止程序的运行。

演示了多级 catch 的使用:

class Base {
};

class Derived : public Base {
};

int main() {
    try {
        throw Derived();  //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
        cout << "This statement will not be executed." << endl;
    } catch (int) {
        cout << "Exception type: int" << endl;
    } catch (char *) {
        cout << "Exception type: cahr *" << endl;
    } catch (Base) {  //匹配成功(向上转型)
        cout << "Exception type: Base" << endl;
    } catch (Derived) {
        cout << "Exception type: Derived" << endl;
    }
    return 0;
}

在 catch 中,我们只给出了异常类型,没有给出接收异常信息的变量。

14.2.2 匹配中类型转换

C/C++ 中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:

  1. 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
  2. 向上转型:也就是派生类向基类的转换。
  3. const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
  4. 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。
  5. 用户自定的类型转换。

catch 在匹配异常类型的过程中,也会进行类型转换,但是这种转换受到了更多的限制,仅能进行「向上转型」、「const 转换」和「数组或函数指针转换」,其他的都不能应用于 catch。

演示了 const 转换以及数组和指针的转换:

int main(){
    int nums[] = {1, 2, 3};
    try {
        throw nums;
        cout << "This statement will not be executed." << endl;
    } catch (const int *) {
        cout << "Exception type: const int *" << endl;
    }
    return 0;
}

运行结果如下所示:

Exception type: const int *

nums 本来的类型是int [3],但是 catch 中没有严格匹配的类型,所以先转换为int *,再转换为const int *。

14.3 throw抛出异常

14.3.1 异常抛出概念

C++ 异常处理的流程,具体为:抛出(Throw)--> 检测(Try) --> 捕获(Catch)

异常必须显式地抛出,才能被检测和捕获到;如果没有显式的抛出,即使有异常也检测不到。

14.3.2 throw抛异常用法

throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。

抛出异常:使用throw关键字后跟一个表达式,将异常抛出。这个表达式可以是任何类型,通常是一个异常类的对象。

throw MyException("Something went wrong");

捕获异常:在可能引发异常的代码块周围使用try块,并使用catch块来捕获和处理异常。

try {
    // 可能引发异常的代码
    throw MyException("Something went wrong");
} catch (const MyException& e) {
    // 处理异常
    std::cout << "Exception caught: " << e.what() << std::endl;
}

来看一个完整版本案例,如下所示:

void func() {
    int num;
    cout << "请输入整型变量:" << endl;
    cin >> num;
    if (num == 0) {
        throw "抛出异常";
    }
}

int main(){
    try {
        func();
        cout << "看看这段代码是否执行"<< endl;
    } catch (exception e) {
        cout << "异常:" << e.what() << endl;
    }
    return 0;
}

测试1,如下所示:

请输入整型变量:
1
看看这段代码是否执行

测试2,如下所示:

0
libc++abi: terminating due to uncaught exception of type char const*
Abort trap: 6

14.3.3 throw异常规范

虚函数中的异常规范

C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。请看下面的例子:

class Base{
public:
    virtual int fun1(int) throw();
    virtual int fun2(int) throw(int);
    virtual string fun3() throw(int, string);
};
class Derived:public Base{
public:
    int fun1(int) throw(int);   //错!异常规范不如 throw() 严格
    int fun2(int) throw(int);   //对!有相同的异常规范
    string fun3() throw(string);  //对!异常规范比 throw(int,string) 更严格
}

异常规范与函数定义和函数声明

C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。

//错!定义中有异常规范,声明中没有
void func1();
void func1() throw(int) { }

//错!定义和声明中的异常规范不一致
void func2() throw(int);
void func2() throw(int, bool) { }

//对!定义和声明中的异常规范严格一致
void func3() throw(float, char*);
void func3() throw(float, char*) { }

14.4 exception异常

14.4.1 抛出标准异常

C++语言本身或者标准库抛出的异常都是 exception 的子类,称为标准异常(Standard Exception)。你可以通过下面的语句来捕获所有的标准异常:

try{
    //可能抛出异常的语句
}catch(exception &e){
    //处理异常的语句
}

之所以使用引用,是为了提高效率。如果不使用引用,就要经历一次对象拷贝(要调用拷贝构造函数)的过程。

14.4.2 exception类介绍

exception 类位于 <exception> 头文件中,它被声明为:

class exception{
public:
    exception () throw();  //构造函数
    exception (const exception&) throw();  //拷贝构造函数
    exception& operator= (const exception&) throw();  //运算符重载
    virtual ~exception() throw();  //虚析构函数
    virtual const char* what() const throw();  //虚函数
}

这里需要说明的是 what() 函数。what() 函数返回一个能识别异常的字符串,正如它的名字“what”一样,可以粗略地告诉你这是什么异常。不过C++标准并没有规定这个字符串的格式,各个编译器的实现也不同,所以 what() 的返回值仅供参考。

14.4.3 exception派生类

先来看一下 exception 类的直接派生类:

  1. logic_error 逻辑错误。
  2. runtime_error 运行时错误。
  3. bad_alloc 使用 new 或 new[ ] 分配内存失败时抛出的异常。
  4. bad_typeid 使用 typeid 操作一个 NULL 指针,而且该指针是带有虚函数的类,这时抛出 bad_typeid 异常。
  5. bad_cast 使用 dynamic_cast 转换失败时抛出的异常。
  6. ios_base::failure io 过程中出现的异常。
  7. bad_exception 这是个特殊的异常,如果函数的异常列表里声明了 bad_exception 异常,当函数内部抛出了异常列表中没有的异常时,如果调用的 unexpected() 函数中抛出了异常,不论什么类型,都会被替换为 bad_exception 类型。

logic_error 的派生类:

  1. length_error,试图生成一个超出该类型最大长度的对象时抛出该异常,例如 vector 的 resize 操作。
  2. domain_error,参数的值域错误,主要用在数学函数中,例如使用一个负值调用只能操作非负数的函数。
  3. out_of_range,超出有效范围。
  4. invalid_argument,参数不合适。在标准库中,当利用string对象构造 bitset 时,而 string 中的字符不是 0 或1 的时候,抛出该异常

runtime_error 的派生类:

  1. range_error,当尝试存储超出范围的值时,会抛出该异常。
  2. overflow_error,当发生数学上溢时,会抛出该异常。
  3. underflow_error,当发生数学下溢时,会抛出该异常。

14.4.4 定义新的异常

创建异常类:创建一个新的类来表示您的异常。通常,您的异常类应该继承自std::exception或其派生类,以符合C++异常处理的标准。

struct MyException : public exception {
    //const throw() 不是函数,这个东西叫异常规格说明,表示 what 函数可以抛出异常的类型,类型说明放到 () 里,
    //这里面没有类型,就是声明这个函数不抛出异常,通常函数不写后面的 throw() 就表示函数可以抛出任何类型的异常。
    const char *what() const throw() {
        return "custom c++ exception";
    }
};

然后使用自定义异常

//您可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:
void test() {
    try {
        throw MyException();
    }
    catch (MyException &e) {
        std::cout << "MyException caught" << std::endl;
        std::cout << e.what() << std::endl;
    }
    catch (std::exception &e) {
        std::cout << "其他的错误" << std::endl;
    }
}

int main() {
    test();
    return 0;
}

通过定义自己的异常类,您可以更好地表示和处理特定的错误或异常情况,并提供更详细的错误信息。

贡献者: yangchong211
上一篇
13.IO流与文件
下一篇
15.STL标准模板库