C++异常

1、什么是异常

程序运行时常会碰到一些异常情况,例如:

  • 做除法的时候除数为 0;
  • 用户输入年龄时输入了一个负数;
  • 用 new 运算符动态分配空间时,空间不够导致无法分配;
  • 访问数组元素时,下标越界;打开文件读取时,文件不存在。
    这些异常情况,如果不能发现并加以处理,很可能会导致程序崩溃

2、如何处理异常

所谓“处理”,可以是给出错误提示信息,然后让程序沿一条不会出错的路径继续执行;也可能是不得不结束程序,但在结束前做一些必要的工作,如将内存中的数据写入文件、关闭打开的文件、释放动态分配的内存空间等。
一发现异常情况就立即处理未必妥当,因为在一个函数执行过程中发生的异常,在有的情况下由该函数的调用者决定如何处理更加合适。尤其像库函数这类提供给程序员调用,用以完成与具体应用无关的通用功能的函数,执行过程中贸然对异常进行处理,未必符合调用它的程序的需要。
此外,将异常分散在各处进行处理不利于代码的维护,尤其是对于在不同地方发生的同一种异常,都要编写相同的处理代码也是一种不必要的重复和冗余。如果能在发生各种异常时让程序都执行到同一个地方,这个地方能够对异常进行集中处理,则程序就会更容易编写、维护。

鉴于上述原因,C++ 引入了异常处理机制。其基本思想是:函数 A 在执行过程中发现异常时可以不加处理,而只是“拋出一个异常”给 A 的调用者,假定为函数 B。

拋出异常而不加处理会导致函数 A 立即中止,在这种情况下,函数 B 可以选择捕获 A 拋出的异常进行处理,也可以选择置之不理。如果置之不理,这个异常就会被拋给 B 的调用者,以此类推。

如果一层层的函数都不处理异常,异常最终会被拋给最外层的 main 函数。main 函数应该处理异常。如果main函数也不处理异常,那么程序就会立即异常地中止。

3、C++异常处理方法

C++ 异常处理涉及到三个关键字:throwcatchtry

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
用法:
	throw 表达式;
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
用法:
try
{
   // 可能会引发异常的代码
}catch( 异常类型 e1 )
{
   // catch 块
}catch( 异常类型 e2 )
{
   // catch 块
}catch( 异常类型 eN )
{
   // catch 块
}

catch 可以有多个,但至少要有一个。
把 try 和其后{}中的内容称作“try块”
把 catch 和其后{}中的内容称作“catch块”。

try…catch 语句的执行过程是:

  1. 执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
  2. 如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个 catch 块的后面继续执行。
示例:
double chufa(double &a,double &b)
{
    if(fabs(b-0)<1e-6)  //浮点数涉及精度问题,比大小时指定精度,这里精度为1e-6
        throw "除数为0";    //抛出异常,后面的值(字符串/对象)指出了异常的特性
    else
    {
        return a/b;
    }    
}

int main(int argc, char const *argv[])
{
    double a = 20,b = 0,result = 0;
    /*try{}:表示注意这些代码可能会引发异常*/
    try
    {
        result = chufa(a,b);
        cout<<"a/b = "<<result<<endl;
    }
    /*catch:捕获异常,会根据异常的类型,决定执行那块异常分支*/
    catch(const char *str)/*本例:抛出异常的数据类型为char*,所以匹配的catch块就是该块*/
    {
        cout<<str<<endl;
    }
    catch(int a)
    {
        cout<<a<<endl;
    }
    cout<<"finsh"<<endl;
 
    return 0;
}
/*执行结果
如果b = 0,会引发异常。执行结果为:除数为0 finsh
如果b = 2,不会引发异常。执行结果为:除数为a/b = 10 finsh
*/

除法函数的除数不能为0,如果除数为0就可能会引起程序出错奔溃,因此将除法函数放在try块中,在chufa()函数中,由于除数不能为0,因此在做运算前对除数进行判断,如果除数为0的话,我们抛出一个异常(异常后面的表达式可以是任意类型的)。抛出异常后try块立即停止执行,然后根据抛出异常的类型,选择进入到与异常类型匹配的catch语句中,该 catch 块执行完毕后,程序继续往后执行,直到正常结束。

4、捕获任何异常类型的catch语句

前面提到,捕获异常时会根据异常的类型选择不同的catch语句执行,怎么让catch语句捕获任意类型的异常呢?

使用下面该方式
catch(...) {
    ...
}
示例:
int main(int argc, char const *argv[])
{
    int a = 10 ,b = 20;
    try
    {
        if(a = 10)
        throw -1;
        else if(b = 20)
        throw "b = 20";
    }
    catch(const int par)
    {
        cout<<par<<endl;
    }
    catch(...)
    {
        cout<<"捕获到了异常"<<endl;
        
    }
    cout<<"finshed"<<endl;
    
    return 0;
}
a = 10会抛出异常 -1, 类型为int型,catch语句会从第一个按顺序匹配,刚好第一个catch语句匹配到了int型,
所以就执行了第一个catch语句。因此虽然第二个catch语句能捕获任意类型的异常,但是第一个已经捕获到了,显然轮不到它了。
a = 10,抛出异常后整个try块就执行结束,b = 20就执行不到。

5、将异常抛给调用者处理

如果一个函数在执行过程中拋出的异常在本函数内就被 catch 块捕获并处理,那么该异常就不会拋给这个函数的调用者(也称为“上一层的函数”);如果异常在本函数中没有被处理,则它就会被拋给上一层的函数。

void A(int a);
void B(int b);

int main(int argc, char const *argv[])
{
    try
    {
        B(0);
        A(0);
        cout<<"begin"<<endl;
    }
    catch(const char *p)
    {
        cout<<p<<endl;
    }
    cout<<"end"<<endl;
    return 0;
}
void A(int a)
{
    if(a == 0)
        throw "a = 0"; //抛出异常,自己A函数不处理,传递给A的调用者
    else
    {
        cout<<"fun A a = "<<a<<endl;
    } 
}
void B(int b)
{
    try
    {
        if(b ==0)
            throw "b = 0";
        else
        {
            cout<<"b = "<<b<<endl;
        }
        
    }
    catch(const char *p)    //函数自身抛出的异常,自身处理
    {
        cout<<p<<endl;
    }     
}

//输出
b = 0
a = 0
end
这里调用函数B时,函数B抛出了异常但是自身处理了,因此这个异常不会丢给调者。
然后后面的函数A可以继续调用,而函数A也抛出了异常,但是自身并没有处理该异常
将异常抛给了主函数,主函数try块立即结束,“cout<<"begin"<<endl”这句代码就不会
执行。然后使用主函数的catch捕获刚产生的异常。

6、自身处理了异常,但还是想通知调用者产生了异常

有时,虽然在函数中对异常进行了处理,但是还是希望能够通知调用者,以便让调用者知道发生了异常,从而可以作进一步的处理。在 catch 块中拋出异常可以满足这种需要。
还是上面的例子函数B抛出了异常,自身了有处理异常的代码,但还是想通知调用者(即主函数)产生了异常。只需要在异常捕获(catch)处理最后再加一句throw

void B(int b)
{
    try
    {
        if(b ==0)
            throw "b = 0";
        else
        {
            cout<<"b = "<<b<<endl;
        }
        
    }
    catch(const char *p)    //函数自身抛出的异常,自身处理
    {
        cout<<p<<endl;
        throw;	//自身已经处理了异常,但还是想通知给调用者。
    //未指明拋出什么样的异常,因此拋出的就是 catch 块捕获到的异常
    //这个异常会被B的调用者 main 函数中的 catch 块捕获。
    } 
    
}
//将第5小节的B函数的代码换成上述代码,执行后的结果为:
b = 0	//自身的异常处理
b = 0	//调用者的异常处理
end
由于B函数抛出了异常,虽然自身处理了,但是传递给了调用者,因此
try
{
     B(0);
     A(0);
     cout<<"begin"<<endl;
 }
 try块中的B(0)执行结束后,立即执行到异常处理代码处,因此下面的A(0)和cout<<"begin"<<endl;不会再被执行。

7、异常声明列表

为了增强程序的可读性和可维护性,使程序员在使用一个函数时就能看出这个函数可能会拋出哪些异常,C++ 允许在函数声明和定义时,加上它所能拋出的异常的列表,具体写法如下:

void func() throw (int, double, A, B, C); 或者
void func() throw (int, double, A, B, C){...}; 
上面的写法表明 func 可能拋出 int 型、double 型以及 A、B、C 三种类型的异常。
异常声明列表可以在函数声明时写,也可以在函数定义时写。如果两处都写,则两处应一致。

一个函数如果不交待能拋出哪些类型的异常,就可以拋出任何类型的异常。函数如果拋出了其异常声明列表中没有的异常,在编译时不会引发错误,运行时可能会报错。

7、c++标准异常类

C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来的。
在这里插入图片描述
关于各个异常的说明,参考该文章
下面拿几个举举例子

①、bad_cast

在用 dynamic_cast 进行从基类对象(或引用)转到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常。程序示例如下:

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

class A
{
public:
	virtual void printf()
	{
		cout << "class A" << endl;
	}
	void fun1() { cout << "Class A fun1" << endl; };
};
class B : public A
{
public:
	virtual void printf()
	{
		cout << "class B" << endl;
	}
	void fun1() { cout << "Class B fun1" << endl; };
};

int main(int argc, char const *argv[])
{
	A a;
	try
	{
		B &b = dynamic_cast<B&>(a);	//将基类的对象转换成派生类,会转换失败,抛异常
	}
	catch (const std::exception& e)
	{
		std::cerr << e.what() << '\n';
	}
	
	return 0;
}

②、bad_alloc

在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常。程序示例如下:

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


int main(int argc, char const *argv[])
{
	try
	{
		char *p = new char[0x7fffffff];	//无法分配这么大空间,抛异常
		delete p;
		cout << "SUCCESS";

	}
	catch (const bad_alloc &obj)
	{
		cout << obj.what() << endl;
		cout << "Faile";
	}
	

	return 0;
}
//输出
bad allocation
Faile
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值