【Essential C++学习笔记】第七章 异常处理

2023-11-29 15:12

本文主要是介绍【Essential C++学习笔记】第七章 异常处理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第七章 异常处理

7.1 抛出异常 && 7.2 捕捉异常 && 7.3 提炼异常

这三节书里讲的比较晦涩,这里通过查看其他文章来了解C++的异常处理

一、什么是异常

**异常机制:**异常是程序在运行过程中出现非正常情况的处理机制。当出现异常时程序会停止运行并调用异常处理程序。

函数机制: 函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它可以在出现**“意外”时中断当前函数**,并以某种机制(类型匹配)回馈给隔代的调用者相关的信息。

基本语法:

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

  • 抛出异常(throw): 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • 提炼异常(catch): 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • 捕获异常(try): try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
// 异常发生第一现场,抛出异常
void  function( ){//... ...throw 表达式;//... ...
}
// 在需要关注异常的地方,捕捉异常
try{//程序function();        //把function至于try中//程序
}catch(异常类型声明){        //比如只写一个int//... 异常处理代码 ...
}catch(异常类型 形参){       //形参将会取得抛出的值//... 异常处理代码 ...
}catch(...){               //抛出的其它异常类型,可以接收任意类型//
}
//如果没有catch(...),并且没有catch子句与抛出的异常类型匹配,程序会直接中断报错。

二、异常处理机制

异常处理的整个过程是一种顺序执行模型,以下是它的基本流程:

  1. 程序执行到可能抛出异常的代码时,这段代码必须嵌入到 try 块中。
  2. 如果在 try 块中的代码引发了异常,程序会跳转到与抛出异常类型匹配的 catch 块中,catch 块负责处理异常。
  3. 最后程序会从 catch 块中退出,向下执行任何后续代码。

简单来说,我们可以将异常处理机制的处理过程看作是程序运行时一条流程线,它沿着 try-catch 块执行,遇到异常时跳转 catch 块那里处理异常,最后退出 catch 块。

  • throw后面可跟任何表达式,除了整数外,指针、字符常量等也可以,如:throw “文档打开失败”
  • 在需要捕捉异常的地方,将可能抛出异常的程序段嵌在try块之中
  • 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从try块后跟随的最后一个catch子句后面的语句继续执行下去

三、异常传递

在抛出异常时可以使用任何类型的值,但是为了能够被 catch 块所匹配,一般使用异常类的对象或指针

如果在函数中抛出了异常,那么异常会被抛到调用该函数的代码中。如果这个函数也没有捕获这个异常,那么异常就会继续传递到更高的层次,直到被捕获为止。

示例代码:

void functionC() {cout << "Starting function C" << endl;throw MyException(); // 抛出自定义异常cout << "Ending function C" << endl;
}void functionB() {cout << "Starting function B" << endl;functionC(); // 调用functionCcout << "Ending function B" << endl;
}void functionA() {cout << "Starting function A" << endl;try {functionB(); // 调用functionB} catch (const exception& e) {cerr << e.what() << endl; // 捕获并处理异常}cout << "Ending function A" << endl;
}int main() {cout << "Starting main function" << endl;functionA(); // 调用functionAcout << "Ending main function" << endl;return 0;
}

首先main函数调用 functionA。functionA 内部调用 functionB,并且包含一个 try 块,用来捕获异常。functionB 内部调用 functionC并抛出了一个异常,这个异常传递到了 functionA 中被 catch 块捕获并处理。

这里需要注意的是一旦抛出了一个异常,函数就会终止。因此functionC 中的 cout 语句不会被执行到

四、提炼(捕获)异常

1 catch语句

try {// 可能会发生异常的代码
} catch (exceptionType& e) {// 处理异常的代码
}

2 多个catch语句的顺序与匹配规则

当由 try 块抛出一个异常时,程序会按照由上到下的顺序遍历 catch 块,直到找到一个与抛出的异常匹配的 catch 块为止。这里的匹配指的是异常参数类型与抛出的异常对象类型一致,或者是该异常的基类类型。如果找不到合适的 catch 块,则程序将异常传递到更高层次的代码中。

**注意:**在编写多个 catch 块时,应该将最具体的 catch 块放在最前面

3 缺省

  • 如果想要捕获任何类型的异常,只需在异常声明部分指定省略号(…)(注意,英文省略号!)即可,如:
//捕获任何类型的异常
catch(...)
{log_message("exception of unknown type");//处理异常,然后退出catch子句
}

五、常见错误和异常处理

1 空指针异常

指针是我们编程过程中经常使用的一个概念,空指针异常指当我们使用一个空指针时,会出现意想不到的行为或意外的程序崩溃。

int* p = nullptr; // 定义一个空指针
int a = *p; // 这里会发生空指针异常

在代码示例中定义了一个空指针 p,然后试图去访问 p 指向的内存空间中的值,并将其赋值给变量 a,这里就会发生空指针异常。

在C++ 中我们可以通过以下方式来避免空指针异常的发生:

  1. 在使用指针之前要进行判断,确保指针不为空。
  2. 在给指针分配内存空间时,要使用 new 操作符,并进行异常处理
2 内存泄漏异常

内存泄漏指程序在分配了内存空间后,没有合适的方式来释放它。内存泄漏会导致程序内存空间的消耗过大,从而影响程序性能

int* p = new int; // 分配了一个动态内存空间
p = nullptr; // 将指针指向空

在代码示例中通过 new 操作符动态地分配了一段内存空间,但是在之后将指针置为空时,我们并没有使用 delete 来释放所分配的内存空间,从而导致了内存泄漏。

在C++ 中应该避免内存泄漏的发生。一种常见的做法是,在分配内存空间时使用智能指针smart pointer,它们会在指针不再需要时自动释放所分配的内存空间。

3 数组越界异常
int arr[5] = {1, 2, 3, 4, 5};
int n = 6;
int a = arr[n]; // 这里会发生数组越界
4 死锁异常

死锁是指两个或多个进程(线程)相互等待对方释放共享资源,从而导致进程(线程)阻塞的情况。死锁是多线程编程中比较常见的一个问题,一旦发生,会导致程序的挂起,从而影响程序的性能。

#include <mutex>
#include <thread>void func(std::mutex& m1, std::mutex& m2) {m1.lock();m2.lock(); // 这里会导致死锁// ...m2.unlock();m1.unlock();
}int main() {std::mutex m1, m2;std::thread t1(func, std::ref(m1), std::ref(m2));std::thread t2(func, std::ref(m2), std::ref(m1));t1.join();t2.join();return 0;
}

在代码示例中定义了两个线程 t1t2,它们分别执行函数 func。在函数中,我们使用了两个互斥量 m1m2 来保证在访问共享资源前线程的同步,但是在执行线程 t1t2 的时候,如果它们同时试图获取 m1m2 的互斥锁,就会导致死锁异常的发生。

在 C++ 中可以通过以下方式来避免死锁异常的发生:

  1. 避免使用多个互斥量进行同步。
  2. 在使用互斥量时,谨慎使用 lock 和 unlock
  3. 使用 RAII 实现自动加锁和解锁,在 C++11 中,我们可以使用 std::lock_guardstd::unique_lock 来实现在构造函数中加锁,在析构函数中解锁的操作

六、总结

1. 使用异常处理的优点:

传统错误处理技术,检查到一个错误,只会返回退出码或者终止程序等等,我们只知道有错误,但不能更清楚知道是哪种错误。使用异常,把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现的错误是什么错误,并去处理,而是否终止程序就把握在调用者手里了。

2. 使用异常的缺点:

如果使用异常,光凭查看代码是很难评估程序的控制流:函数返回点可能在你意料之外,这就导致了代码管理和调试的困难。启动异常使得生成的二进制文件体积变大,延长了编译时间,还可能会增加地址空间的压力。

C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。

C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。

3. 什么时候使用异常?

建议:除非已有的项目或底层库中使用了异常,要不然尽量不要使用异常,虽然提供了方便,但是开销也大。

4. 程序所有的异常都可以catch到吗?

并非如此,只有发生异常,并且又抛出异常的情况才能被 catch 到。例如,数组下标访问越界的情况,系统是不会自身抛出异常的,所以我们无论怎么 catch 都是无效的;在这种情况,我们需要自定义抛出类型,判断数组下标是否越

7.4 局部资源管理

首先看一个示例:

extern Mutex m;
void f()
{// 占用资源int *p = new int;m.acquire();process(p);//释放资源m.release();delete p;
}
  • 问题在于:如果在释放资源的语句之前这个函数或这个函数内调用的函数抛出了异常,函数执行之初所分配的资源不一定最终会被释放掉。

  • 怎么解决呢。需要资源管理的手法(在初始化阶段即进行资源请求)。

    void f()
    {// 占用资源int *p = new int;m.acquire();try {process(p);}catch(...){m.release();delete p;}m.release();delete p;
    }
    
  • 上面也不是很完美,因为释放资源的代码需要写两次,并且这些抛出异常的操作,会印象效率,有没有更好的办法呢? 用**auto_ptr**

    #include<memory>
    void f()
    {auto_ptr<int>p(new int);//模板类auto_ptr的类对象定义MutexLock m1(m);//非模板类MutexLock的类对象定义process(p);//执行完这些代码,p和m1的析构函数在此被自动调用。(如果前三行代码没有异常抛出的话)
    }
    

auto_ptr

  • auto_ptr是C++标准库(以前的博客说的标准库都认为是C++标准库)提供的模板类。

  • 这个auto_ptr模板类的作用:它会自动删除通过new表达式分配的对象。比如上面例子里的p对象

  • 使用auto_ptr模板类之前,要包含头文件#include<memory>

  • auto_ptr模板类将*运算符和->运算符重载,重载方式是用一个迭代器类里声明/定义重载运算符函数(iterator class)的方式来重载。所以我们可以像使用一般指针那样使用auto_ptr对象,如:

    auto_ptr<string>aps(new string("vermeer"));
    string *ps=new string("vermmer");
    if((aps->size()==ps->size())&&(*aps==*ps))//像使用一般指针使用auto_ptr对象
    //...
    

7.5 标准异常

无需硬背,混个眼熟即可~

异常描述
std::exception该异常是所有标准 C++ 异常的父类。
std::bad_alloc该异常可以通过 new 抛出。
std::bad_cast该异常可以通过 dynamic_cast 抛出。
std::bad_typeid该异常可以通过 typeid 抛出。
std::bad_exception这在处理 C++ 程序中无法预期的异常时非常有用。
std::logic_error理论上可以通过读取代码来检测到的异常。
std::domain_error当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument当使用了无效的参数时,会抛出该异常。
std::length_error当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator
std::runtime_error理论上不可以通过读取代码来检测到的异常。
std::overflow_error当发生数学上溢时,会抛出该异常。
std::range_error当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error当发生数学下溢时,会抛出该异常。

这里举一个内存分配的异常

#include <iostream>
#include <exception>
#include <stdexcept>using namespace std;class Student {public:Student(int age) {if (age > 249) {throw out_of_range("年龄太大,你是外星人嘛?");}m_age = age;m_space = new int[1024 * 1024 * 100];}private :int m_age;int *m_space;
};int main() {try {for (int i = 1; i < 1024; i++) {Student *xiao6lang = new Student(18);}} catch (out_of_range &e) {cout << "捕捉到一只异常:" << e.what() << endl;} catch (bad_alloc &e) {cout << "捕捉到动态内存分配的异常:" << e.what() << endl;}system("pause");return 0;
}

这篇关于【Essential C++学习笔记】第七章 异常处理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/433313

相关文章

Java.lang.InterruptedException被中止异常的原因及解决方案

《Java.lang.InterruptedException被中止异常的原因及解决方案》Java.lang.InterruptedException是线程被中断时抛出的异常,用于协作停止执行,常见于... 目录报错问题报错原因解决方法Java.lang.InterruptedException 是 Jav

Python进行JSON和Excel文件转换处理指南

《Python进行JSON和Excel文件转换处理指南》在数据交换与系统集成中,JSON与Excel是两种极为常见的数据格式,本文将介绍如何使用Python实现将JSON转换为格式化的Excel文件,... 目录将 jsON 导入为格式化 Excel将 Excel 导出为结构化 JSON处理嵌套 JSON:

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

Spring Boot 中的默认异常处理机制及执行流程

《SpringBoot中的默认异常处理机制及执行流程》SpringBoot内置BasicErrorController,自动处理异常并生成HTML/JSON响应,支持自定义错误路径、配置及扩展,如... 目录Spring Boot 异常处理机制详解默认错误页面功能自动异常转换机制错误属性配置选项默认错误处理

C++中detach的作用、使用场景及注意事项

《C++中detach的作用、使用场景及注意事项》关于C++中的detach,它主要涉及多线程编程中的线程管理,理解detach的作用、使用场景以及注意事项,对于写出高效、安全的多线程程序至关重要,下... 目录一、什么是join()?它的作用是什么?类比一下:二、join()的作用总结三、join()怎么

SpringBoot 异常处理/自定义格式校验的问题实例详解

《SpringBoot异常处理/自定义格式校验的问题实例详解》文章探讨SpringBoot中自定义注解校验问题,区分参数级与类级约束触发的异常类型,建议通过@RestControllerAdvice... 目录1. 问题简要描述2. 异常触发1) 参数级别约束2) 类级别约束3. 异常处理1) 字段级别约束

Java堆转储文件之1.6G大文件处理完整指南

《Java堆转储文件之1.6G大文件处理完整指南》堆转储文件是优化、分析内存消耗的重要工具,:本文主要介绍Java堆转储文件之1.6G大文件处理的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言文件为什么这么大?如何处理这个文件?分析文件内容(推荐)删除文件(如果不需要)查看错误来源如何避

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处

Java docx4j高效处理Word文档的实战指南

《Javadocx4j高效处理Word文档的实战指南》对于需要在Java应用程序中生成、修改或处理Word文档的开发者来说,docx4j是一个强大而专业的选择,下面我们就来看看docx4j的具体使用... 目录引言一、环境准备与基础配置1.1 Maven依赖配置1.2 初始化测试类二、增强版文档操作示例2.