C++ 异常处理(try catch)

版权声明:作者:weizu_cool https://blog.csdn.net/qq_26460841/article/details/88352736

在任何一门语言中都有异常的解释,这里就不做介绍了。

C++ 异常处理机制会涉及 try、catch、throw 三个关键字。

程序错误

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

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

异常处理基本思想

C++之父Bjarne Stroustrup在《The C++ Programming Language》中讲到:一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关);另一方面,库的用户知道怎样处理这些错误,但却无法检查它们何时发生(如果能检测,就可以再用户的代码里处理了,不用留给库去发现)。

        Bjarne Stroustrup说:提供异常基本目的就是为了处理上面的问题。基本思想是:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。 
The fundamental idea is that a function that finds a problem it cannot cope with throws an exception, hoping that its (direct or indirect) caller can handle the problem.

        也就是《C++ primer》中说的:将问题检测问题处理相分离。 
Exceptions let us separate problem detection from problem resolution

C语言中的处理错误方式

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

这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。

        还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值。当然,你也可以通过指针或者C++的引用来返回另外的值,但是这样可能会令你的程序略微晦涩难懂。

异常为什么好

    在如果使用异常处理的优点有以下几点:

        1. 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。

        2. 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。

        3. 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。

        4. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。

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

    任何事情都是两面性的,异常有好处就有坏处。如果你是C++程序员,并且希望在你的代码中使用异常,那么下面的问题是你要注意的。

        1. 性能问题。这个一般不会成为瓶颈,但是如果你编写的是高性能或者实时性要求比较强的软件,就需要考虑了。

       2. 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存。在java中,就基本不需要考虑这个,有垃圾回收机制。

        3. 函数的异常抛出列表:java中是如果一个函数没有在异常抛出列表中显式指定要抛出的异常,就不允许抛出;可是在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常

        4. C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。而在java中会检查。

        5. 在java中,抛出的异常都要是一个异常类;但是在C++中,你可以抛出任何类型,你甚至可以抛出一个整型。(当然,在C++中如果你catch中接收时使用的是对象,而不是引用的话,那么你抛出的对象必须要是能够复制的。这是语言的要求,不是异常处理的要求)。

        6. 在C++中是没有finally关键字的。而java和python中都是有finally关键字的。

捕获异常

抛出异常用throw,捕获用try……catch

我们可以借助 C++ 异常机制来捕获上面的异常,避免程序崩溃。捕获异常的语法为:

try{
    // 可能抛出异常的语句
}catch(exceptionType variable){
    // 处理异常的语句
}
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;
}

第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。

第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是第 17 行的 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。

C++ 标准的异常

C++ 提供了一系列标准的异常,定义在 <exception> 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

下表是对上面层次结构中出现的每个异常的说明:

异常 描述
std::exception 该异常是所有标准 C++ 异常的父类。
std::bad_alloc 该异常可以通过 new 抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid 该异常可以通过 typeid 抛出。
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 当发生数学下溢时,会抛出该异常。

案例:

1. 除0

#include <iostream>
using namespace std;
double division(int a, int b){
	if( b == 0 ){
		throw "Division by zero condition!";//const char*
	}
	return (a/b);
}
int main (){
	int x = 50;
	int y = 0;
	double z = 0;
	try {
		z = division(x, y);
		cout << z << endl;
	}catch (const char* msg) {
		cerr << msg << endl;
	}
	return 0;
}

由于我们抛出了一个类型为 const char* 的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char*。

2. 定义新的异常

您可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:

例2:依照C++标准实现自定义异常类myException并将throw语句封装到函数check()中

涉及到的更改正如标题所述,(1)重写基类的what()函数,返回错误信息;(2)将throw myException()封装到check()函数中;(3)允许check()函数抛出myException类型的异常。代码如下:C++函数声明后面加throw()的作用!

 #include<exception>  
#include<iostream>  
using namespace std;  
   
class myException:public exception{  
    public:  
       const char* what()const throw(){ //throw () 表示不允许任何异常产生 
            return "ERROR! Don't divide a number by integer zero.\n";  
       }      
};  
void check(int y) throw(myException){ //throw (myException)表示只允许myException的异常发生 
     if(y==0) throw myException();  
}  
  
int main()  
{  
    int x=100,y=0;  
    try{  
        check(y);  
        cout<<x/y;  
    }catch(myException& me){  
        cout<<me.what();  
    }  
    system("pause");  
    return 0;  
}  

下面的编译也能通过: 

void check(int y) throw() 
{  
     if(y==0) throw myException();  // // 程序会在这里崩溃.(编者注:如果该异常被处理,不会崩溃)
} 

成员函数声明后面跟上throw(),表示告诉类的使用者:我的这个方法不会抛出异常,所以,在使用该方法的时候,不必把它至于 try/catch 异常处理块中。

声明一个不抛出异常的函数后,你有责任保证在你的函数的实现里面不会抛出异常。

函数后面声明 throw() 只是接口的提供者和接口的使用者间的默契或称协议。


作者:无涯明月

上篇: C++函数声明后面加throw()的作用!


猜你喜欢

转载自blog.csdn.net/qq_26460841/article/details/88352736