异常

概述

当程序代码出现异常的时候,当前语境无法提供足够的信息用以决定处理方式的时候,程序员通常会抛出异常,C++中throw 用于抛出异常

#include <iostream>
class myErr
{
private:
	std::string _msg;
public:
	myErr(std::string msg) :_msg(msg) {}
	std::string what() { return _msg; }
	~myErr();

private:

};

myErr::~myErr()
{
}
int test() {
	throw myErr("test");
}
int main()
{
	try {
		test();
	}
	catch (myErr& e) {
		std::cout << "error:";
		std::cout << e.what() << std::endl;
		
	}
}

异常实际上是一个普通的类,抛出异常的throw 的函数实际上会返回一个这个异常类的拷贝try-catch 语句用于捕获并处理异常

try中的语句在异常发生之后程序会执行栈展开,即逐级回溯匹配catch 语句,然后从最近的匹配上catch 语句后开始执行代码,回复正常的控制流(throw语句之后的都不会执行)。并且在异常发生之前生成的局部变量将会被销毁,我们称之为栈反解。

如下面的例子

#include <iostream>
class myErr
{
private:
	std::string _msg;
public:
	myErr(std::string msg) :_msg(msg) {}
	std::string what() { return _msg; }
	~myErr(){}

};

class Resource
{
public:
	Resource() { std::cout << "Resource" << std::endl; }
	~Resource() { std::cout << "~Resource" << std::endl; }
};

void test1() {
	Resource res;
	throw myErr("test");
	std::cout << "test1" << std::endl;
}
void test2() {
	Resource res;
	test1();
}


int main()
{
	try {
		test2();
	}
	catch (myErr& e) {
		std::cout << "error:";
		std::cout << e.what() << std::endl;
	}
	
}

/*
结果:
Resource
Resource
~Resource
~Resource
error:test
*/

当然,不同的函数可以产生相同的异常,可以用相同的异常处理器处理

在异常处理模型中,有两个重要的处理模型:终止恢复。当发生不可恢复的错误时,程序不需要回到发生异常的地方。而恢复模型则是尝试重新运行,例如重新调用发生异常的函数,通常做法是try 块放入循环中循环调用(实际上非常困难)

异常匹配机制

前面已经重复过,发生异常以后会逐层寻找最近的catch 块进行匹配,在匹配时,异常并不要求与处理器完全相同,对象或者引用可以与异常的基类进行匹配,但是如果处理器接受的是对象的话,在和基类进行匹配时会被“切割”,所以我们通常会使用引用。

#include <iostream>
class myErr
{
private:
	std::string _msg;
public:
	myErr(std::string msg) :_msg(msg) {}
	std::string what() { return _msg; }
	~myErr(){}

};

class Err1 : public myErr
{
public:
	Err1(std::string msg) : myErr(msg) {}
	~Err1() {}
};
void test() {
	throw Err1("test");
}
int main()
{
	try {
		test();
	}
	catch (myErr& e) {
		std::cout << "error:";
		std::cout << e.what() << std::endl;
	}
}
/*
运行结果:
error:test
*/

但是,异常处理器不会将一个异常转换另一个异常,即使包含对应的构造函数

#include <iostream>
class myErr
{
private:
	std::string _msg;
public:
	myErr(std::string msg) :_msg(msg) {}
	std::string what() { return _msg; }
	~myErr(){}

};
class myErr2 {
public:
	myErr2(myErr &myerr){}
};
void test() {
	throw myErr("test");
}
int main()
{
	try {
		test();
	}
	catch (myErr2& e) { //这里的catch是捕获不到这个异常的
		std::cout << "error:";
	}
	
}

catch(...) 可以捕获所有的异常,因此最好放在最后处理,用于处理意外的异常情况,在异常处理器中,直接使throw 可以直接抛出异常,这样,这个异常的所有信息可以保留。

如果一个异常没有被成功捕获(因为是可以忽略异常的),那将会触发函terminate() 函数,该函数(<exception> 中定义)被调用,该函数会调abort() 函数,这个函数的调用会导致全局变量和静态局部变量的析构函数不被调用。在两种特殊情况下,该函数(terminate())也会被调用:

  • 局部函数在析构函数中抛出异常

  • 堆对象,全局变量或者静态局部变量的构造函数或析构函数抛出异常(一般不允许析构函数抛异常)

set_terminate() 函数可以自定义terminate() 函数,参数为一个新的terminate()函数指针,返回值为被替换掉的terminate() 函数指针(第一次返回库函数terminate()的指针)terminate() 函数是一个无返回值无参数的函数,也不能在内部抛异常的,且必须终止程序。

异常后对资源的清理

异常处理机制的一个重要作用是在发生异常后对这个作用域内所有的由构造函数建立的对象调用析构函数,但是有一点例外,如果在构造函数中抛出了异常,导致构造函数没有成功结束,则不会调用对应的析构函数

为了解决这样的问题,有两种解决方案,要么在构造函数中捕获异常,然后释放掉资源,或者使用RAII

  • RAII大致的意思是将资源封装成一个类,在类的构造函数中获取资源,在析构函数中释放资源,然后再要用这个类的作用域生成局部变量,这样再生命周期结束时,调用析构函数将会自动释放资源,事实auto_ptr 就是该方法的一个类封装

函数级try块

可以try 写为函数体的一部分,主要用于处理构造函数初始化列表中的异常,如下所示

#include <iostream>
#include <stdexcept>

// 自定义异常类
class ResourceException : public std::runtime_error {
public:
    ResourceException(const std::string& msg) : std::runtime_error(msg) {}
};

// 一个可能抛出异常的类
class Resource {
public:
    Resource(int id) : m_id(id) {
        if (id < 0) {
            throw ResourceException("Resource ID cannot be negative");
        }
        std::cout << "Resource " << m_id << " created\n";
    }
    ~Resource() {
        std::cout << "Resource " << m_id << " destroyed\n";
    }
private:
    int m_id;
};

// 使用函数级别 try 块的类构造函数
class MyClass : public Resource {
public:
    // 构造函数使用函数级别 try 块
    MyClass(int id1, int id2)
    try : Resource(id1), m_res(id2) {  // 初始化列表中可能抛出异常
        std::cout << "MyClass constructor body\n";
    }
    catch (const ResourceException& e) {
        // 在此处处理初始化列表或构造函数体中的异常
        std::cerr << "MyClass construction failed: " << e.what() << "\n";
        // 注意:构造函数必须重新抛出异常,否则对象会被视为构造成功!
        throw;  // 重新抛出,确保对象构造失败
    }

private:
    Resource m_res;
};

int main() {
    try {
        MyClass obj1(1, 2);   // 正常构造
        MyClass obj2(-1, 3);  // 触发异常
    }
    catch (const std::exception& e) {
        std::cerr << "Main caught: " << e.what() << "\n";
    }
    return 0;
}
/*
运行结果:
Resource 1 created
Resource 2 created
MyClass constructor body
Resource 2 destroyed
Resource 1 destroyed
MyClass construction failed: Resource ID cannot be negative
Main caught: Resource ID cannot be negative
*/

事实上,可以在所有的函数中应用应用函数try块,也可以catch 中返回,等价于用try包含函数的所有代码

标准的异常

C++库中由常见的标准异常,可以直接使用,也可以基于标准异常继承,主要的异常继承关系如下

std::exception
├── std::bad_alloc                (内存分配失败,定义于 <new>)
├── std::bad_cast                 (dynamic_cast 转换失败,定义于 <typeinfo>)
├── std::bad_typeid               (typeid 操作符应用于空指针,定义于 <typeinfo>)
├── std::ios_base::failure        (I/O 流错误,定义于 <ios>,C++11 后可能继承自 system_error)
├── std::logic_error              (逻辑错误,定义于 <stdexcept>)
│   ├── std::invalid_argument     (无效参数)
│   ├── std::domain_error         (数学运算定义域错误)
│   ├── std::length_error         (超出允许长度,如容器操作)
│   └── std::out_of_range         (访问越界,如 vector::at())
└── std::runtime_error            (运行时错误,定义于 <stdexcept>)
    ├── std::range_error          (数值超出有效范围)
    ├── std::overflow_error       (算术上溢)
    ├── std::underflow_error      (算术下溢)
    ├── std::system_error         (系统/OS 相关错误,定义于 <system_error>)
    │   └── std::filesystem::filesystem_error (文件系统错误,C++17,定义于 <filesystem>)
    └── std::format_error         (格式化错误,C++20,定义于 <format>)

exception派生下来的主要有两个类,一个是logic_error类用于描述程序的逻辑错误,第二个是runtime_error用于描述运行时不可预知的错误,例如内存耗尽等等,这两个类都有一个接受std::string的构造函数,用于保存错误消息,而exception提供了一个虚函数exception::what(),用于获取存放的消息

异常规格说明

函数可以显式的告知,此函数有可能抛出哪些异常,此时函数的声明可以写成

void function(void) throw(toobig,toosmall,divzero); //表明此函数可能抛出后面的三种类型异常
void function(void);//表明此函数可能抛出任何类型的异常

如果函数抛出了没有列在规格中的异常,则会调unexpction() 函数,默认会调terminate() 函数,同理也存set_unexpection() 函数,类似set_terminate() 函数

异常规格说明是继承的,共有函数本质上是一种用户与库程序员的约定,因此异常规格说明也是约定的一部分,因此在派生类中不可以增加新的异常规格,但是可以指定较少的或者不抛出异常。

最后,当无法知道会抛出什么异常的时候,不要使用异常规格说明

异常安全

这是一个深入的话题,以后再议

使用异常

当函数不符合他的规格说明时抛出异常

  • 不要在异步事件中使用异常,应当在中断程序中设计一个标记,主程序中检查

  • 不要在简单的事情中使用异常,有足够的信息处理错误应当及时处理,不应该抛出异常

  • 异常事件有很大的代价,不要用于替代控制流,例如switch语句

  • 不要强迫自己使用异常,如果仅仅是一些小型程序,存在内存分配失败,打开文件失败等等问题,显示一个消息然后退出即可,不需要捕获异常然后释放,让操作系统帮你完成

  • 兼容原来的没有异常的老代码最好的方式是用try块包含原来最大的代码块?

源码面前无秘密