【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(); // 调用functionC
    cout << "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(); // 调用functionA
    cout << "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 
#include 

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
    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

  • 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 
#include 
#include 

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;
}

你可能感兴趣的:(C++学习,c++,学习,笔记)