【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

相关文章

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

电脑提示xlstat4.dll丢失怎么修复? xlstat4.dll文件丢失处理办法

《电脑提示xlstat4.dll丢失怎么修复?xlstat4.dll文件丢失处理办法》长时间使用电脑,大家多少都会遇到类似dll文件丢失的情况,不过,解决这一问题其实并不复杂,下面我们就来看看xls... 在Windows操作系统中,xlstat4.dll是一个重要的动态链接库文件,通常用于支持各种应用程序

C++中RAII资源获取即初始化

《C++中RAII资源获取即初始化》RAII通过构造/析构自动管理资源生命周期,确保安全释放,本文就来介绍一下C++中的RAII技术及其应用,具有一定的参考价值,感兴趣的可以了解一下... 目录一、核心原理与机制二、标准库中的RAII实现三、自定义RAII类设计原则四、常见应用场景1. 内存管理2. 文件操

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

SQL Server数据库死锁处理超详细攻略

《SQLServer数据库死锁处理超详细攻略》SQLServer作为主流数据库管理系统,在高并发场景下可能面临死锁问题,影响系统性能和稳定性,这篇文章主要给大家介绍了关于SQLServer数据库死... 目录一、引言二、查询 Sqlserver 中造成死锁的 SPID三、用内置函数查询执行信息1. sp_w

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

Python主动抛出异常的各种用法和场景分析

《Python主动抛出异常的各种用法和场景分析》在Python中,我们不仅可以捕获和处理异常,还可以主动抛出异常,也就是以类的方式自定义错误的类型和提示信息,这在编程中非常有用,下面我将详细解释主动抛... 目录一、为什么要主动抛出异常?二、基本语法:raise关键字基本示例三、raise的多种用法1. 抛

C++作用域和标识符查找规则详解

《C++作用域和标识符查找规则详解》在C++中,作用域(Scope)和标识符查找(IdentifierLookup)是理解代码行为的重要概念,本文将详细介绍这些规则,并通过实例来说明它们的工作原理,需... 目录作用域标识符查找规则1. 普通查找(Ordinary Lookup)2. 限定查找(Qualif