c++学习笔记(18.异常处理)

本节知识点:

1.异常与c语言的异常处理:

    a.异常就是代码中的一些特殊情况,如除法运算中除数为零的情况,还有就是一些函数中出现不符合要求的函数参数的情况,都属于异常情况!
    b.在c语言中对异常的处理通常有三种方式:
       第一种,最常用的就是条件判断
       第二种,goto语句
       第三种,longjmp()函数和setjmp()函数
条件判断的示例代码:
#include <cstdlib>
#include <iostream>

using namespace std;

int MemSet(void* dest, unsigned int length, unsigned char v)
{
    if( dest == NULL )
    {
        return -1;
    }
    
    if( length < 4 )
    {
        return -2;
    }
    
    if( (v < 0) || (v > 9) )
    {
        return -3;
    }
    
    unsigned char* p = (unsigned char*)dest;
    
    for(int i=0; i<length; i++)
    {
        p[i] = v;
    }
    
    return 0;
}

int main(int argc, char *argv[])
{
    int ai[5];
    double ad[4];
    char ac[3];
    int ret;
    
    ret = MemSet(ai, sizeof(ai), 0);
    
    if( ret == 0 )
    {
    }
    else if( ret == -1 )
    {
    }
    else if( ret == -2 )
    {
    }
    else if( ret == -3 )
    {
    }
    
    ret = MemSet(ad, sizeof(ad), 1);
    
    if( ret == 0 )
    {
    }
    else if( ret == -1 )
    {
    }
    else if( ret == -2 )
    {
    }
    else if( ret == -3 )
    {
    }
    
    ret = MemSet(ac, sizeof(ac), 2);
    
    if( ret == 0 )
    {
    }
    else if( ret == -1 )
    {
    }
    else if( ret == -2 )
    {
    }
    else if( ret == -3 )
    {
    }
    
    cout << "Press the enter key to continue ...";
    cin.get();
    return EXIT_SUCCESS;
}
    c.可见条件判断的方法,使得正常逻辑的代码和异常处理的代码混合在一起,导致代码迅速膨胀,难以维护!而goto语句和setjmp()、longjmp()函数则可以将异常处理代码放在统一的地方,与正常逻辑代码分开。

2.c++中的异常处理:

   a.c++中有着自己的一套异常处理方式,没有使用goto语句和setjmp()、longjmp()函数,是因为它们的安全性实在是很低!
   b.c++中异常处理的语法
      try语句块处理正常逻辑
      catch语句块处理异常逻辑
      throw语句引发一个异常
示例代码:
#include <iostream>
#define div_zero_error 1

using namespace std;

double div(double a, double b)
{
	if((-0.0000001<b)&&(b<0.0000001))
	{
		throw div_zero_error;
	}
	return a/b;
}

int main()
{	
	char a = ‘c‘; //try语句块外面依然可以放其他语句 
	try
	{ 
		cout << div(2,3) << endl;
		throw a; //throw后面可以直接 放数据 也可以放变量 
		//throw ‘d‘;
		cout << div(1,0) << endl;
		cout << div(5,2) << endl;
	}
	catch(int a)
	{
		cout << "div_zero_error" << endl;	
	}
	catch(char) //catch语句后面,可以只有变量类型,没有具体变量 
	{
		cout << "test_error" << endl;
	}
	return 0;
}
注意:第一,并不是所有语句都要全部放在try或者catch语句块里面,可以放在try或者catch语句外面!
           第二,throw语句后面,可以直接放数据,也可以放变量
           第三,一个try语句后面可以有多个catch语句块,catch语句后面,可以只有变量类型,没有具体变量!
   c.当throw语句抛出一个异常的时候,首先在当前函数中看,throw语句是否在一个try语句块中,如果是则去判断这个try语句块对应的catch语句块中是否能够处理这个异常类型,如果不能处理或者throw语句不在一个try语句块中,则退出当前函数,返回到调用函数的位置看,看调用函数的语句是否在一个try语句块中,对应的catch是否能处理这个异常,不断重复返回到上层函数,直到遇到可以处理这个异常的catch语句。或者返回到了所有函数后,都无法处理抛出的这个异常,则程序异常终止!!!切记,即使异常在对应的catch语句块中完成了异常处理,程序也不能回到异常发生处或上层函数调用处继续执行了,程序只能接着处理异常的catch语句块后面继续执行!!!
如图:
c++学习笔记(18.异常处理)
   d.c++的这种异常处理方式与常规的条件判断比较,优势在于异常代码块与正常代码块是分开的,便于代码管理,同时也避免了大量的条件判断(因为每种异常都会有一个自己的返回值,当函数结束时,需要判断是那一种异常,所以就存在大量判断,而且是每次调用函数都要重新判断,不能代码复用,导致垃圾代码大量膨胀),条件判断中多种异常的情况(即返回好几个值的情况),可以使用多个catch语句块完成,catch类型多的时候尽量不要使用基本类型,使用自己创建的类类型!
      c++的这种异常处理与goto语句(setjmp函数)的比较,try.....catch语句,比goto语句要安全的多,如果是用goto语句进行跳转,这个过程根本就不考虑函数的建栈和退栈过程,它仅仅就是跳转过去执行而已!而throw语句,则不是!throw抛出异常的时候,是会退栈然后返回上层函数的,这一点要比goto安全多了!起码函数是正常结束的!而且,throw是一层一层的去寻找catch语句,在逻辑性上,也要比goto语句清晰很多!
try.....catch语句的示例代码:
#include <cstdlib>
#include <iostream>

using namespace std;

void MemSet(void* dest, unsigned int length, unsigned char v)
{
    if( dest == NULL )
    {
        throw -1;
    }
    
    if( length < 4 )
    {
        throw -2;
    }
    
    if( (v < 0) || (v > 9) )
    {
        throw -3;
    }
    
    unsigned char* p = (unsigned char*)dest;
    
    for(int i=0; i<length; i++)
    {
        p[i] = v;
    }
}

int main(int argc, char *argv[])
{
    int ai[5];
    double ad[4];
    char ac[3];
    
    try
    {
        MemSet(ai, sizeof(ai), 0);
        MemSet(ad, sizeof(ad), 1);
        MemSet(ac, sizeof(ac), 2);
    }
    catch(int e)
    {
        cout<<e<<endl;
    }
    
    cout << "Press the enter key to continue ...";
    cin.get();
    return EXIT_SUCCESS;
}

注意:上面代码是c++异常处理的常规用法!
比对代码:
#include <cstdlib>
#include <iostream>

using namespace std;

int MemSet(void* dest, unsigned int length, unsigned char v)
{
	int ret = 0;
    if( dest == NULL )
    {
        return  -1;
    }
    
    if( length < 4 )
    {
        return  -2;
    }
    
    if( (v < 0) || (v > 9) )
    {
        return  -3;
    }
    
    unsigned char* p = (unsigned char*)dest;
    
    for(int i=0; i<length; i++)
    {
        p[i] = v;
    }
    return 0;
}

int main(int argc, char *argv[])
{
    int ai[5];
    double ad[4];
    char ac[3];
    
    if(0==MemSet(ai, sizeof(ai), 0))
    {}
    else
    {
		if(-1==MemSet(ai, sizeof(ai), 0))
		{
			cout << -1 << endl;
		}
		else if(-2==MemSet(ai, sizeof(ai), 0))
		{
			cout << -2 << endl;
		}
		else if(-3==MemSet(ai, sizeof(ai), 0))
		{
			cout << -3 << endl;
		}
	}
	if(0==MemSet(ad, sizeof(ad), 1))
	{}
    else
    {
		if(-1==MemSet(ad, sizeof(ad), 1))
		{
			cout << -1 << endl;
		}
		else if(-2==MemSet(ad, sizeof(ad), 1))
		{
			cout << -2 << endl;
		}
		else if(-3==MemSet(ad, sizeof(ad), 1))
		{
			cout << -3 << endl;
		}
	}
	if(0==MemSet(ac, sizeof(ac), 2))
	{}
    else
    {
		if(-1==MemSet(ac, sizeof(ac), 2))
		{
			cout << -1 << endl;
		}
		else if(-2==MemSet(ac, sizeof(ac), 2))
		{
			cout << -2 << endl;
		}
		else if(-3==MemSet(ac, sizeof(ac), 2))
		{
			cout << -3 << endl;
		}
	}
    cout << "Press the enter key to continue ...";
    cin.get();
    return EXIT_SUCCESS;
}

注意:上面就是使用条件判断的异常处理方式,可见代码膨胀的样子!但是使用条件判断可以保证每一个正常逻辑的代码都得到执行,而try.....catch语句就保证不了!因为当发生throw异常的时候,代码就会跳转到catch语句块中,然后从catch语句块往后执行了!会丢失throw语句后面部分的正常逻辑代码!
    e.同一个try语句块可以跟上多个catch语句块,同一个try语句块可以抛出多种不同类型的异常,不同类型的异常由不同的catch语句块负责处理!异常被抛出后会自上而下逐一匹配catch语句块,异常匹配时,不会进行默认类型转换!
示例代码:
#include <cstdlib>
#include <iostream>

using namespace std;

int test(int i)
{
    if( i == 1 )
    {
        throw -1;
    }
    
    if( i == 2 )
    {
        throw "ERROR";
    }
    
    if( i == 3 )
    {
        throw 0.5;
    }
    
    if( i == 4 )
    {
        throw ‘d‘;
    }
    
    return i;
}

int main(int argc, char *argv[])
{
    for(int i=0; i<4; i++)
    {
        try
        {
            cout<<test(i)<<endl;
        }
        catch(int e)
        {
            cout<<"Int: "<<e<<endl;
        }
        catch(const char* e)
        {
            cout<<"const char*: "<<e<<endl;
        }
        catch(double e)
        {
            cout<<"double: "<<e<<endl;
        }
    }
    
    cout << "Press the enter key to continue ...";
    cin.get();
    return EXIT_SUCCESS;
}
注意:第一,catch后面可以只有类型,没有具体变量,但是当没有变量的时候,catch语句块中就不能获得throw语句抛出的异常参数了!
           第二,对于这样一个try语句块和多个catch语句块的时候,基本数据类型往往是不够用的,所以尽量使用自己通过class关键字定义的类类型!

3.深入异常处理:

   a.c++中的catch语句可以使用...捕获所有类型的异常。catch(...)虽然可以捕获所有类型的异常但却无法得到异常信息。catch(...)一般作为最后一个异常处理块出现。看见代码中的catch就要意识到这里在处理异常情况,而异常是在对应的try中产生的。
   b.在catch语句块中,仍然可以使用throw抛出异常
   c.在catch语句块中,可以使用throw a , 也可以只使用throw;  当throw后面没有变量也没有数据的时候,是表示要抛出catch语句块接受的异常类型(即在catch(int a)这个语句块中,throw; 就是throw a; 的简写)。所以throw; 这种情况只能在catch语句块中使用,不能在try语句块中使用。同时即使是catch(int)这样的语句块中,使用throw;  在外层的catch(int e)异常处理中,也可以获得e的值,就是最初的那个throw 抛出的值!应该是编译器自动提供了一个中间变量!
   d.注意在,catch(...)中的throw;  可以直接把接收到的异常再次抛出!应该也是编译器自动提供了中间变量!
示例代码:
#include <cstdlib>
#include <iostream>

using namespace std;

int test(int i)
{
    if( i == 1 )
    {
        throw -1;
    }
    
    if( i == 2 )
    {
        throw "ERROR";
    }
    
    if( i == 3 )
    {
        throw 0.5;
    }
    
    if( i == 4 )
    {
        throw ‘d‘;
    }
    
    return i;
}

int main(int argc, char *argv[])
{
    for(int i=0; i<5; i++)
    {
       	try
	    {
			try
        	{
          	  cout<<test(i)<<endl;
        	}
       		catch(int)
        	{
            //	cout<<"Int: "<<e<<endl;
            	throw;
        	}
        	catch(const char* e)
        	{
            	cout<<"const char*: "<<e<<endl;
            	throw 3;
        	}
        	catch(double e)
        	{
            	cout<<"double: "<<e<<endl;
            	throw;
        	}
        	catch(...)
			{
				cout << "catch(...) " << endl;
				throw;
			} 
		}
		catch(double a)
		{
			cout << "throw catch(double) " << endl;
		}
		catch(int a)
		{
			cout << "throw catch(int) " << endl;
			cout << a << endl;
		}
		catch(char a)
		{
			cout << "throw catch(char) " << endl;
		}
		catch(...)
		{
			cout << "throw catch(...) " << endl;
		}
		
    }
    
    cout << "Press the enter key to continue ...";
    cin.get();
    return EXIT_SUCCESS;
}

4.异常与对象:

   a.切记,千万不要在构造函数中抛出异常,构造函数中可能会申请系统资源(如new int[5]),而在构造函数中抛出异常则会导致对象构造不完全,对于不完全对象的析构函数是不会被调用的,因此可能会造成资源泄漏!(即析构函数不被调用,析构函数中的delet [] 没有执行,造成内存泄漏)
示例代码:
#include <iostream> 

using namespace std;

class test
{
public:
	test()
	{
		cout << "test()....." << endl;
		throw ‘a‘;
	}
	~test()
	{
		cout << "~test()....." << endl;
	}
};
void fun()
{
	try
	{
		test t1;
	}
	catch(char e)
	{
		cout << e << endl;
	}
}

int main()
{
	fun();
	return 0;
}
注意:上面的对象因为是构造不完全的对象,所以对象的析构函数不被调用!

5.工程中的异常应用:

   a.在工程中会定义一系列的异常类,通过继承可以得到一个异常类族。每个类代表工程中可能出现的一种异常类型!
   b.在工程中可以使用标准库中的异常类,可以将标准库中的异常类作为基类派生新的异常类!
   c.标准库中的异常都是从exception类派生而来的,exception类有两个主要分支,logic_error用于描述程序中出现的逻辑错误,如:传递无效参数。runtime_error用于描述无法预料的事件所造成的错误,如:内存耗尽、硬件错误等。
标准库中的异常如图:
c++学习笔记(18.异常处理)
   d.标准库中的异常族包含在<stdexcept>头文件中!
   e.logic_error和runtime_error都提供了一个参数为字符串的构造函数,这样就能够保存异常信息,还可以通过what()成员函数得到异常信息!!!
示例代码:
#include <iostream>
#include <stdexcept> //异常族的头文件 

using namespace std;

double Div(double a, double b)
{
	//invalid_argument p("Divide by zero...");
    	if( (-0.00000001 < b) && ( b < 0.00000001) )
   	 {
		/*
		invalid_argument是一个异常类,invalid_argument("Divide by zero...")
        	是直接调用这个类的构造函数(后面是构造函数的参数,也是异常信息),
		此时编译器产生一个临时对象,throw就是抛出了这个临时对象 
		*/
		throw invalid_argument("Divide by zero..."); 
		//throw p;
    	}
    
   	 return  a / b;
}

int main()
{
	try
	{
		cout<<Div(1, 0)<<endl;
	}
	//这里使用引用是为了避免由于对象构造与拷贝的开销,使用引用就不会再重新创建一个对象进行拷贝了 
	catch(invalid_argument& error)
	{
		cout << error.what() << endl;//通过what成员函数获得异常信息 
	}
	return 0;
}


注意:在catch语句后,可以使用引用参数,使用引用就避免了对象的构造和拷贝的开销,效率会高些!
使用自己创建的异常对象示例代码:
#include <iostream>
#include <stdexcept> //异常族的头文件 

using namespace std;

class div_zero_error : public logic_error
{
public:
	div_zero_error(const char* s) : logic_error(s)//初始化列表~~~ 
	{
		
	} 
};

double Div(double a, double b)
{
	//div_zero_error p("Divide by zero...");
  	if( (-0.00000001 < b) && ( b < 0.00000001) )
 	{
		/*
		div_zero_error是一个异常类,div_zero_error("Divide by zero...")
        	是直接调用这个类的构造函数(后面是构造函数的参数,也是异常信息),
		此时编译器产生一个临时对象,throw就是抛出了这个临时对象 
		*/
		throw div_zero_error("Divide by zero..."); 
		//throw p;
  	 }
    
    	return  a / b;
}

int main()
{
	try
	{
		cout<<Div(1, 0)<<endl;
	}
	//这里使用引用是为了避免由于对象构造与拷贝的开销,使用引用就不会再重新创建一个对象进行拷贝了 
	catch(exception& error) //使用exception类型 是为了接受各种类型的异常 
	{
		cout << error.what() << endl;//通过what成员函数获得异常信息 
	}
	return 0;
}


注意:第一,当希望通过标准库中的异常类族派生出自己的异常类的时候,首先要自己定义类并继承基类,这样自己的异常类中就继承了logic_error类的构造函数,这个带参的构造函数(即参数是字符串,用来保存异常信息的)是对应what()成员函数的。所以就出现了这样的语句,div_zero_error(const char* s) : logic_error(s)  这条语句很有意思,这条语句使用了构造函数的初始化参数列表!当创建div_zero_error类对象的时候,先接收构造函数的参数,且保存在s中,不管是从初始化列表的角度看,还是从父类构造函数的角度看,都是先调用logic_error类的构造函数,再调用div_zero_error类的构造函数,此时在调用logic_error类的构造函数的时候,就完成了对异常信息的保存!!!等于说把子类div_zero_error的构造函数的参数赋值给了父类logic_error的构造函数的参数,用于传递异常信息,即div_zero_error的构造函数的参数就是用来接收异常信息的!对于那些标准库中的其他子类,原理依然相同!
            第二,在throw语句中,throw后面的对象,不管是临时对象,还是事先创建好的对象,这个对象在创建的时候一定是带字符串参数的,如:div_zero_error p("Divide by zero...");和div_zero_error("Divide by zero...");   没有参数是编译不过的(这个参数就是异常信息),因为在创建异常类对象的时候,是需要调用父类logic_error的带字符串参数的构造函数的,所以会报错!当然对于属于标准库中的异常类,依然需要准守这个规则!因为标准库中的异常类的创建过程,跟自己定义的异常类创建的过程是一模一样的!!!只不过一个是库函数帮你写好的,一个是你自己写的,代码是相同的!
             第三,对于工程中的代码,如果已经约定好所有的异常都是基于exception这个基类派生的异常类族,当想接收所有异常的时候,就不用catch(...)这样的语句了,因为catch(...)不能获得what函数的异常信息!可以使用catch(exception& error)这样的语句来捕获所有异常,这里利用的是子类父类之间的赋值兼容性原则(即子类是特殊的父类,exception类可以接收所有他的子类)!

6.函数级try语法:

c++学习笔记(18.异常处理)
注意:其实两段代码没有任何区别,仅仅是多一对括号和少一对括号的区别!右侧的代码主要是,怕try语句块中的代码过多,使程序员忘记这是在try语句块中!仅仅是为了提高代码的可读性和维护性!意义并不是很大,遇到了能够认识就可以了!



c++学习笔记(18.异常处理)

上一篇:C++回顾之友元


下一篇:C语言中随机数的生成