跳至主要內容

异常安全--当出现异常时,要保证程序是安全的(预判错误)

张威大约 13 分钟c/c++c++基础

异常安全--当出现异常时,要保证程序是安全的(预判错误)

1681304184845-87c43f84-fc0b-472f-ac86-2c5c6dfa9716
1681304184845-87c43f84-fc0b-472f-ac86-2c5c6dfa9716

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

  1. 语法错误在编译链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
  2. 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
  3. 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。

运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。

#include <iostream>
#include <string>
using namespace std;

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

运行代码,在控制台输出 ch1 的值后程序崩溃。下面我们来分析一下原因。 at() 是 string 类的一个成员函数,它会根据下标来返回字符串的一个字符。与[ ]不同,at() 会检查下标是否越界,如果越界就抛出一个异常;而[ ]不做检查,不管下标是多少都会照常访问。

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

上面的代码中,下标 100 显然超出了字符串 str 的长度。由于str[100]不会检查下标越界,虽然有逻辑错误,但是程序能够正常运行。而str.at(100)则不同,at() 函数检测到下标越界会抛出一个异常,这个异常可以由程序员处理,但是我们在代码中并没有处理,所以系统只能执行默认的操作,也即终止程序执行

异常出现之前/c语言 处理错误的方式

在C语言的世界中,对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单的理解为一个全局整型变量)去记录错误。当然C++中仍然是可以用这两种方法的。

这两种方法最大的就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。 还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值当然,你也可以通过指针或者C++的引用来返回另外的值,但是这样可能会令你的程序略微晦涩难懂

捕获异常

我们可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:try...catch语句块的catch可以有多个,但至少要有一个。

try{
    // 可能抛出异常的语句
}catch(异常类型){	//int double ……
    // 处理异常的语句
}catch(exceptionType variable1){
    // 处理异常的语句
}catch(exceptionType variable2){
    // 处理异常的语句
}
//类比switch case

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

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

#include <iostream>
#include <string>
#include <exception>
using namespace std;

int main(){
    string str = "http://c.biancheng.net";
  
    try{
        char ch1 = str[100];
        cout<<ch1<<endl;
    }catch(exception e){
        cout<<"[1]out of bound!"<<endl;
    }

    try{
        char ch2 = str.at(100);
        cout<<ch2<<endl;
    }catch(exception &e){  //exception类位于<exception>头文件中
        cout<<"[2]out of bound!"<<endl;
    }

    return 0;
}

运行结果: 1681212468246-41b064ed-63b7-4ef0-9763-446e058b91a5 可以看出,第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。 第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是第 17 行的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。 说得直接一点,检测到异常后程序的执行流会发生跳转,从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了;即使 catch 语句成功地处理了错误,程序的执行流也不会再回退到异常点,所以这些语句永远都没有执行的机会了。本例中,第 18 行代码就是被跳过的代码。

执行完 catch 块所包含的代码后,程序会继续执行 catch 块后面的代码,就恢复了正常的执行流。

关于「如何抛出异常」异常的处理流程

抛出(Throw)--> 检测(Try) --> 捕获(Catch)

try...catch语句块的catch可以有多个,但至少要有一个。

发生异常的位置--throw表达式

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

throw+表达式, 如:

throw “the input is error.”;//抛出const char* 的异常类型
throw 1;//抛出int型的异常类型
throw runtime_error(“Data must refer to ISBN”);//抛出runtime_error类类型的异常类型,其中runtime_error是一个标准的异常处理

发生异常的位置

  1. try 块中直接发生的异常:
#include <iostream>
#include <string>
#include <exception>
using namespace std;

int main(){
    try{
        throw "Unknown Exception";  //抛出const char*类型的异常
        cout<<"This statement will not be executed."<<endl;
    }catch(const char* &e){//抓取 异常类型 ,获取异常信息
        cout<<e<<endl;
    }

    return 0;
}
Unknown Exception
#include <iostream>
using namespace std;
void test()
{
    double x, y;
    cin >> x >> y;
    try 
    {
        if(0 == y)
        {
            throw y;	//double
        }                                                                                     
        else
        {
            cout << (x / y) << endl; 
        }       
    }   
    catch(double d)  
    {
        cout << "catch(double)" << endl;
    }   
    catch(int e)  
    {
        cout << "catch(int)" << endl;
    }
}
int main() {

    test();
    return 0;
}
1681217280598-bdb7d4b4-bd17-4ebd-b18c-d2f8037978a4
1681217280598-bdb7d4b4-bd17-4ebd-b18c-d2f8037978a4
  1. 下面的例子演示了 try 块中调用的某个函数中发生了异常:
#include <iostream>
#include <string>
#include <exception>
using namespace std;

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

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

    return 0;
}

运行结果:

Unknown Exception

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

  1. try 块中调用了某个函数,该函数又调用了另外的一个函数,这个另外的函数抛出了异常:
#include <iostream>
#include <string>
#include <exception>
using namespace std;

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

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

    return 0;
}

运行结果:

Unknown Exception

课堂代码

#include <iostream>

using std::cout;
using std::endl;
using std::cin;

void test()
{
    double x, y;
    cin >> x >> y;
    try
    {
        if(0 == y)
        /* if( y = 1 ) */
        {
            throw y;
        }
        else
        {
            cout << "(x/y) = " << x/y << endl;
        }
    }
    catch(int e)
    {
        cout << "catch(int)" << endl;
    }
    catch(double d)
    {
        cout << "catch(double)" << endl;
    }
}
int main(int argc, char **argv)
{
    test();
    return 0;
}

寻找处理代码的过程(匹配catch的过程)

如果try语句块有嵌套。那么,在throw抛出异常后,寻找catch异常处理的代码是怎样的一个过程呢?答案就是逐层向外寻找。即本层throw出的异常,若在本层没找到对应的catch异常处理,则在调用它的层里继续匹配寻找

注意当上述的匹配过程中最后都没能匹配到任何catch字句,程序会转到terminate的标准库函数,程序非正常退出

C++定义的标准异常类

C++ 提供了一系列标准的异常,它们是以父子类层次结构组织起来的,如下所示:

下面是对异常类的说明:

异常描述
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当发生数学下溢时,会抛出该异常。

使用C++的标准异常类实例

例子:

  • if (_b == 0) throw "输入的分母是0。";修改为 if (_b == 0) throw runtime_error("输入的分母是0。");

  • catch(const char* s)修改为catch(runtime_error err)std::cout << s修改为std::cout << err.what()

最后的结果和修改前一样,只不过使用的异常类。

注意。该字符串的目的是提供有关异常的一些文本信息。就如"输入的分母是0。"提示一样。对于那些无初始值的异常类型来说,what返回的内容由编译器决定。

#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
	//bad_alloc异常类实例
	try {
		char * p = new char[0x7fffffff];  //无法分配这么多空间,会抛出异常
	}
	catch (bad_alloc e)  {
		cerr << e.what() << endl;
	}

	//out_of_range异常类实例
	string s = "I love China";
	try {
		char c = s.at(100);  //拋出 out_of_range 异常
	}
	catch (out_of_range & e) {
		cerr << e.what() << endl;
	}
	return 0;
}
$./a.out 
basic_string::at

从这个例子可以看出,有些异常是系统自动抛出,并不需要我们自己写抛出异常。

如何定义和使用自己的异常类

我们可以通过继承C++标准异常类的方法来定义自己的异常类。

#include <iostream>
#include <exception>
#include <stdexcept>
using namespace std;

class MyException :public exception
{
public:
	MyException() :c(nullptr){}
	MyException(const char* _c):c(_c){}
	virtual ~MyException(){}

	//覆写what成员函数
	const char* what() throw()
	{
		if (c == nullptr)
			return "MyException() error catch!";
		return c;
	}
private:
	const char * c;
};

int main()
{
	//默认参数-构造一个MyException类
	try{
		throw MyException();
	}
	catch (MyException & myexp){
		cerr << myexp.what() << endl;
	}

	//实际传参-构造一个MyException类
	try{
		throw MyException("Error have been found.");
	}
	catch(MyException & myexp){
		cerr << myexp.what() << endl;
	}

	return 0;
}
$./a.out 
MyException() error catch!
Error have been found.

异常规范(C++11标例子准中已经被废弃)

异常规范可以分为两类:

  1. 动态异常规范(Dynamic Exception Specification):使用关键字 throw() 或 noexcept 来指定函数不会抛出任何异常或只会抛出特定类型的异常。例如:
void myFunction() throw(); // myFunction 不会抛出任何异常
void myFunction() noexcept; // myFunction 不会抛出任何异常
void myFunction() throw(std::bad_alloc); // myFunction 只会抛出 std::bad_alloc 异常
  1. 静态异常规范(Static Exception Specification):使用关键字 throw(...) 来指定函数可能会抛出任何类型的异常。例如:
void myFunction() throw(...); // myFunction 可能会抛出任何类型的异常

(异常安全有时间再看,先知道怎么用)

C++中使用异常时应注意的问题/总结

  1. 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存
  2. try块中如果抛出异常,会立即跳转到与异常相匹配的catch块中(throw后的语句不执行),执行完会再跳转到最后一个catch的下一条语句。
  3. try和catch必须成对出现。catch通过()中的异常声明来匹配throw抛出的异常类型(也就是throw后接的表达式类型)
  4. catch的匹配是逐层向外匹配。即当前函数没有catch,则在调用它的函数里找catch
  5. 若throw抛出的异常最终没有匹配到catch或则根本就没有catch,系统调用terminate后退出程序
  6. 函数的异常抛出列表:在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常
  7. C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。
  8. catch的匹配过程中,对类型的要求比较严格允许标准算术转换类类型的转换。(类类型的转化包括种:通过构造函数的隐式类型转化和通过转化操作符的类型转化)