C++异常

异常是什么

异常处理机制允许程序中独⽴开发的部分能够在运⾏时就出现的问题进⾏通信并做出相应的处理
异常使得我们能够将问题的检测与解决问题的过程分开,程序的⼀部分负责检测问题的出现,然后
解决问题的任务传递给程序的另⼀部分,检测环节⽆须知道问题的处理模块的所有细节。
简单的来说就是检测问题并抛出问题给程序处理
在c语言当中也有像异常的处理方式,只不过c语言是通过返回错误码实现的,比如return 0就代表程序正常运行完毕退出 return 1就代表函数非正常终止等,拿到错误码以后还要去查询错误信息,⽐较⿇烦。异常时抛出⼀个对象,这个对象可以函数更全⾯的各种信息。

异常的抛出与捕获

程序出现问题时,我们通过抛出(throw)⼀个对象来引发⼀个异常,该对象的类型以及当前的调⽤
链决定了应该由哪个catch的处理代码来处理该异常。
我们可以来写一个代码看看

cpp 复制代码
# include<iostream>
using namespace std;

double dev(double input1, double input2)
{
	if (input1 == 0 || input2 == 0)
	{
		string s = "不能除以零";
		throw s;
	}
	else
	{
		return input1 / input2;
	}
}
int main()
{
	double a = 0.0, b = 0.0;
	while (1)
	{
		cin >> a >> b;
		try
		{
			cout << dev(a, b) << endl;
		}
		catch(const string& error)
		{
			cout << error << endl;
		}
		a = 0.0, b = 0.0;
	}

	return 0;
}

这段代码是实现一个除法,程序会检测是否除以0。如果发现除以0的话会throw一个string对象告诉你除以0了,并用catch捕获我们来运行一下


发现成功捕获了我们抛出的异常
还有一个规律就是
被选中的处理代码是调⽤链中与该对象类型匹配且离抛出异常位置最近的那⼀个。根据抛出对象的 类型和内容,程序的抛出异常部分告知异常处理部分到底发⽣了什么错误。
就比如下面的代码,我们有两个catch一模一样,但第二个永远不会被执行对应这句话被****选中的处理代码是调⽤链中与该对象类型匹配且离抛出异常位置最近的那⼀个

cpp 复制代码
try
{
	cout << dev(a, b) << endl;
}
catch(const string& error)
{
	cout << error << endl;
}
catch (const string& error)
{
	;
}

当throw执⾏时,throw后⾯的语句将不再被执⾏。程序的执⾏从throw位置跳到与之匹配的catch
模块,catch可能是同⼀函数中的⼀个局部的catch,也可能是调⽤链中另⼀个函数中的catch,这一句话是什么意思,我们可以看看下面的代码

cpp 复制代码
void funcB()
{
    // 这里没有try/catch
    throw 200;
}

void funcA()
{
    funcB();// 异常向上抛,本函数也不处理
}

int main()
{
    try
    {
        funcA();
    }
    catch (int err)
    {
        cout << "在调用链最外层main函数捕获异常" << endl;
    }
    return 0;
}

函数调用关系:main → funcA → funcB异常在 funcB 抛出,funcB 没有 try-catch,就退出 funcB,回到 funcA;funcA 也没有 try-catch,继续往外回到 main;如果 main 有匹配的 catch,就在 main 中处理异常。
控制权从throw位置转移到了catch位置。这⾥还有两个重要的含义:1、沿着调⽤链的函数可能提早 退出。2、⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。这句话是什么意思,我们来一个例子

cpp 复制代码
class Test
{
public:
    Test() { cout << "创建对象" << endl; }
    ~Test() { cout << "销毁对象" << endl; }
};

void fun2()
{
    Test t;
    throw 1;  // 抛出异常
    cout << "这句永远不会执行";
}

void fun1()
{
    Test t2;
    fun2();
}

int main()
{
    try
    {
        fun1();
    }
    catch (int)
    {
        cout << "进入异常处理" << endl;
    }
    return 0;
}

在遇到异常逐层退出的时候c++会会自动调用每一个局部对象的析构函数,释放临时创建的变量、类对象。只要是在抛出异常之前创建的栈对象,全部自动销毁,防止内存泄漏。

抛出异常对象后,会⽣成⼀个异常对象的拷⻉,因为抛出的异常对象可能是⼀个局部对象,所以会
⽣成⼀个拷⻉对象,这个拷⻉的对象会在catch⼦句后销毁。(这⾥的处理类似于函数的传值返
回)
就像我们第一个例子一样

栈展开

1.抛出异常后,程序暂停当前函数的执⾏,开始寻找与之匹配的catch⼦句,⾸先检查throw本⾝是否 在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地⽅进⾏处理
2.如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配,则退出当前函数,继续在外层调⽤函数链中查找,上述查找的catch过程被称为栈展开。
3.如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序。
4.如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。
这个过程就有点类似于我们上面的那个过程

查找匹配的处理代码

⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的
那个。
但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩⼩;允许数组转换成指向数组
元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实
⽤,实际中继承体系基本都是⽤这个⽅式设计的。 我们来说说这个加粗的重点部分,我们来看看下面的代码

cpp 复制代码
class Exception
{
public:
	Exception(const string& str)
		:_errmessage(str)
	{}
	virtual string what() const
	{
		return _errmessage;
	}
protected:
	string _errmessage;
};

class dev:public Exception
{
public:
	dev(double a,double b,const string& errmessage)
		:Exception(errmessage)
		,_a(a)
		,_b(b)
	{}

	double dev_func()
	{
		return _a / _b;
	}
	virtual string what() const
	{
		return _errmessage;
	}

private:
	double _a;
	double _b;
};

void func2()
{
	double x = 0.0;
	double y = 0.0;
		cin >> x >> y;
		if (y == 0 || x == 0)
		{
			throw dev(x, y, "不能除以零");
		}
		else
		{
			dev d(x, y,"除数合法");
			cout << "除数合法" << endl;
			cout << d.dev_func() << endl;
		}
}
int main()
{
	while (1)
	{
		try
		{
			func2();
		}
		catch (const dev& err)
		{
			cout << err.what() << endl;
		}
	}
	return 0;
}

这里我们设计了一个基类和一个计算除法的派生类,当遇到除数为0的时候就抛出异常
还可以设计其他的模块每个模块的继承都是Exception的派⽣类,每个模块可以添加⾃⼰的数据
最后捕获时,我们捕获基类就可以,这样就非常方便
如果到main函数,异常仍旧没有被匹配就会终⽌程序,不是发⽣严重错误的情况下,我们是不期望 程序终⽌的,所以⼀般main函数中最后都会使⽤catch(...),它可以捕获任意类型的异常,但是不知道异常错误是什么。
这个就相当于程序最后一道保险,防止捕获的异常没有处理,而引发报错

cpp 复制代码
catch (...)
{
 cout << "Unkown Exception" << endl;
}

异常重新抛出

有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对 象直接抛出。

cpp 复制代码
class Exception
{
public:
	Exception(const string& str,int id)
		:_errmessage(str)
		,_id(id)
	{}
	virtual string what() const
	{
		return _errmessage;
	}
	int get_id() const
	{
		return _id;
	}
protected:
	string _errmessage;
	int _id;
};

class dev:public Exception
{
public:
	dev(double a,double b,const string& errmessage,int id)
		:Exception(errmessage,id)
		,_a(a)
		,_b(b)
	{}

	double dev_func()
	{
		return _a / _b;
	}
	virtual string what() const
	{
		return _errmessage;
	}

private:
	double _a;
	double _b;
};

void func2()
{
	double x = 0.0;
	double y = 0.0;
		cin >> x >> y;
		if (y == 0 || x == 0)
		{
			throw dev(x, y, "不能除以零",1);
		}
		else
		{
			dev d(x, y,"除数合法",0);
			cout << "除数合法" << endl;
			cout << d.dev_func() << endl;
		}
}
void func3()
{
	while (1)
	{
		try
		{
			func2();
		}
		catch (const Exception& err)
		{
			if (err.get_id()==1)
			{
				throw;
			}
			else
			{
				cout << err.what() << endl;
			}
		}
	}
}
int main()
{
	try
	{
		func3();
	}
	catch (const Exception& err)
	{
		cout << err.what() << endl;
	}
	catch(...)
	{

	}
	return 0;
}

这段代码我们重新设计了一下,我们设计了一个id如果除以0id就设置成1,没有除以0就设置成0,然后如果Id等于1就抛异常不在当前处理重新抛出到main函数处理

  • func3 捕获异常,判断是除零错误(id=1);
  • 执行 throw;,异常不再在 func3 处理;
  • 异常继续向上重新抛出,跳到 main 函数里的 catch 接收。

异常的安全问题

我们知道当程序遇到throw抛出异常时,程序会终止,下面的代码就不会被执行,但如果我们前面申请了资源,没有得到有效的释放就造成了内存泄漏,产生了安全问题,对于这种情况我们需要处理中间我 们需要捕获异常,释放资源后⾯再重新抛出。下面的代码就很好的解释并处理了上述问题

cpp 复制代码
double Divide(int a, int b)
{
  // 当b == 0时抛出异常
  if (b == 0)
  {
    throw "Division by zero condition!";
  }
    return (double)a / (double)b;
}
void Func()
{
   // 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array没有得到释放。
   // 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再
   // 重新抛出去。
     int* array = new int[10];
  try
  {
   int len, time;
   cin >> len >> time;
   cout << Divide(len, time) << endl;
  }
   catch (...)
  {
    // 捕获异常释放内存
   cout << "delete []" << array << endl;
   delete[] array;

    throw; // 异常重新抛出,捕获到什么抛出什么
  }
   cout << "delete []" << array << endl;
   delete[] array;
}
int main()
{
  try
 {
   Func();
 }
   catch (const char* errmsg)
 {
 cout << errmsg << endl;
 }
   catch (const exception& e)
 {
 cout << e.what() << endl;
 }
   catch (...)
 {
 cout << "Unkown Exception" << endl;
 }
 return 0;
}

异常规范

noexcept关键字

noexcept 写在函数尾部,表示:这个函数承诺不会抛出任何异常

这个关键字我们在STL库里面会经常看到

cpp 复制代码
void fun() noexcept;
  1. 普通函数抛异常:异常沿调用链向上寻找 catch。
  2. noexcept 修饰的函数一旦抛出异常:不会再继续传递异常,程序直接终止。

编译器并不会在编译时检查noexcept,也就是说如果⼀个函数⽤noexcept修饰了,但是同时⼜包
含了throw语句或者调⽤的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会
报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调⽤ terminate 终⽌程序。
noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回
false,不会就返回true。