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

程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:
- 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
- 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
- 运行时错误是指程序在运行期间发生的错误,例如除数为 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;
}
运行结果: 可以看出,第一个 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是一个标准的异常处理
发生异常的位置
- 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;
}
- 下面的例子演示了 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 都没有被执行。
- 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++ 提供了一系列标准的异常,它们是以父子类层次结构组织起来的,如下所示:

下面是对异常类的说明:
使用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标例子准中已经被废弃)
异常规范可以分为两类:
- 动态异常规范(Dynamic Exception Specification):使用关键字 throw() 或 noexcept 来指定函数不会抛出任何异常或只会抛出特定类型的异常。例如:
void myFunction() throw(); // myFunction 不会抛出任何异常
void myFunction() noexcept; // myFunction 不会抛出任何异常
void myFunction() throw(std::bad_alloc); // myFunction 只会抛出 std::bad_alloc 异常
- 静态异常规范(Static Exception Specification):使用关键字 throw(...) 来指定函数可能会抛出任何类型的异常。例如:
void myFunction() throw(...); // myFunction 可能会抛出任何类型的异常
(异常安全有时间再看,先知道怎么用)
C++中使用异常时应注意的问题/总结
- 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存。
- try块中如果抛出异常,会立即跳转到与异常相匹配的catch块中(throw后的语句不执行),执行完会再跳转到最后一个catch的下一条语句。
- try和catch必须成对出现。catch通过()中的异常声明来匹配throw抛出的异常类型(也就是throw后接的表达式类型)
- catch的匹配是逐层向外匹配。即当前函数没有catch,则在调用它的函数里找catch
- 若throw抛出的异常最终没有匹配到catch或则根本就没有catch,系统调用terminate后退出程序
- 。
- 函数的异常抛出列表:在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常。
- C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。
- catch的匹配过程中,对类型的要求比较严格。不允许标准算术转换和类类型的转换。(类类型的转化包括两种:通过构造函数的隐式类型转化和通过转化操作符的类型转化)