部分转载自:https://www.cnblogs.com/MrYuan/p/4800257.html
catch 的数据类型需要与throw出来的数据类型相匹配
#include "stdafx.h"
#include <iostream>
#include <exception>
using namespace std;
int main()
{
try
{
throw 1; //抛出异常后,之后的代码将不会执行
throw "error";
}
catch (const char* str) //需要加const 不然编译不通过
{
cout << str << endl;
}
catch (int i)
{
cout << i << endl;
}
}
try中通过throw来抛出异常,而catch则负责捕获这个异常并进行相应的处理,一个try可以对应多个catch来处理不同的异常,但throw抛出第一个异常后,之后的代码将不会执行,当选中了某个catch子句处理异常之后,执行与之对应的块。catch一旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问。
自定义异常类来进行处理
#include <iostream>
#include <exception>
using namespace std;
//可以自己定义Exception
class myexception: public exception
{
virtual const char* what() const throw()
{
return "My exception happened";
}
}myex;
int main () {
try
{
if(true) //如果,则抛出异常;
throw myex;
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
函数后面throw()关键字的作用
C++函数后面加关键字throw(something)限制,是对这个函数的异常安全作出限制,throw()它是函数提供者和使用者之间的一种默契或称协议,提供者告诉使用者此函数会抛出哪个异常或者标明该函数不抛出任何异常,之所以说是协议,是因为实际上内部实现是需要人为的确保,这种协议不影响正常的异常处理流程,所以,在使用该方法的时候,不必把它至于 try/catch 异常处理块中,但如果一个标明throw()的函数内部发生了throw:
(1).如果内部直接 throw something 编译器会发现并指出
(2).如果是内部调用了一个可能 throw something的函数,编译器无法发现运行时一旦这个内部的函数throw,程序会abort崩溃
这是异常规范,只会出现在声明中,表示这个函数可能抛出任何类型的异常
void GetTag() throw(type); //表示只抛出type类型的异常
void GetTag() throw(int); //表示只抛出int类型异常
void GetTag() throw(int,char); //表示抛出in,char类型异常
void GetTag() throw(exception); //表示只抛出exception类型异常
void GetTag() throw(); //表示不会抛出任何类型异常
void GetTag() throw(...); //表示抛出任何类型异常
如void GetTag() throw(int)表示只抛出int类型异常,并不表示一定会抛出异常,但是一旦抛出异常只会抛出int类型,如果抛出非int类型异常,调用unexception()函数,退出程序。假如你加了一个throw()属性到一个永远不会抛出异常的函数中,编译器会非常聪明的知道此代码的意图和对此函数决定优化的方式。
#include "stdafx.h"
#include <iostream>
#include <exception>
using namespace std;
void test() throw()
{
throw 5;
}
int main()
{
try
{
test();
}
catch (int i) //如果没有catch捕获对应int类型的异常,则会崩溃
{
cout << i << endl;
}
return 0;
}
如果该异常未被上层的异常过滤器捕捉的话,会引发运行时的错误。
异常抛出后的处理
当异常被抛出时,首先搜索抛出该异常的函数,如果没找到匹配的catch子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的catch子句,这个新的函数也被终止,继续搜索调用它的函数。以此类推,沿着程序的执行路径逐层回退,直到找到适当的类型的catch子句为止,如果最终还是没能找到任何匹配的catch子句,程序转到名为terminate的标准库函数,该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。
#include "stdafx.h"
#include <iostream>
#include <exception>
using namespace std;
double fuc(double x, double y)
{
if (y == 0)
{
throw y; //除数为0,抛出异常
}
return x / y;
}
int main()
{
double res;
try
{
res = fuc(2, 3);
cout << "The result of x/y is : " << res << endl;
res = fuc(4, 0); //出现异常
cout << "这里不会执行" << endl;
}
catch (double b) //捕获并处理异常
{
cerr << "error of dividing zero.\n";
cout << b << endl; //打印出异常信息
exit(1); //异常退出程序
}
return 0;
}
catch(...)的作用
catch (…)能够捕获多种数据类型的异常对象,所以它提供给程序员一种对异常对象更好的控制手段,使开发的软件系统有很好的可靠性。因此一个比较有经验的程序员通常会这样组织编写它的代码模块,如下:
注意下面try block中可能抛出的DataType1、DataType2和DataType3三种类型的异常对象在前面都已经有对应的catch block来处理。但为什么还要在最后再定义一个catch(…) block呢?这就是为了有更好的安全性和可靠性,避免上面的try block抛出了其它未考虑到的异常对象时导致的程序出现意外崩溃的严重后果,而且这在用VC开发的系统上更特别有效,因为catch(…)能捕获系统出现的异常,而系统异常往往令程序员头痛了,现在系统一般都比较复杂,而且由很多人共同开发,一不小心就会导致一个
指针变量指向了其它非法区域,结果意外灾难不幸发生了。catch(…)为这种潜在的隐患提供了一种有效的补救措施。
void Func()
{
try
{
// 这里的程序代码完成真正复杂的计算工作,这些代码在执行过程中
// 有可能抛出DataType1、DataType2和DataType3类型的异常对象。
}
catch (DataType1& d1)
{
}
catch (DataType2& d2)
{
}
catch (DataType3& d3)
{
}
catch (...)
{
}
}
构造器和析构器中的异常抛出
#include <iostream.h>
#include <stdlib.h>
class ExceptionClass1
{
char* s;
public:
ExceptionClass1()
{
cout<<"ExceptionClass1()"<<endl;
s=new char[4];
cout<<"throw a exception"<<endl;
throw 18;
}
~ExceptionClass1()
{
cout<<"~ExceptionClass1()"<<endl;
delete[] s;
}
};
void main()
{
try
{
ExceptionClass1 e;
}
catch(...)
{}
}
打印结果为:
ExceptionClass1()
throw a exception
在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的)。应该说这符合实际现象,因为对象没有完整构造。为了避免这种情况,我想你也许会说:应避免对象通过本身的构造函数涉及到异常抛出。即:既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出。但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr。那么,在析构函数中的情况呢?我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到。
标准C++异常类
class exception
{
public:
exception() throw();
exception(const exception& rhs) throw();
exception& operator=(const exception& rhs) throw();
virtual ~exception() throw();
virtual const char *what() const throw();
};
C++有很多的标准异常类:
namespace std
{
//exception派生
class logic_error; //逻辑错误,在程序运行前可以检测出来
//logic_error派生
class domain_error; //违反了前置条件
class invalid_argument; //指出函数的一个无效参数
class length_error; //指出有一个超过类型size_t的最大可表现值长度的对象的企图
class out_of_range; //参数越界
class bad_cast; //在运行时类型识别中有一个无效的dynamic_cast表达式
class bad_typeid; //报告在表达试typeid(*p)中有一个空指针p
//exception派生
class runtime_error; //运行时错误,仅在程序运行中检测到
//runtime_error派生
class range_error; //违反后置条件
class overflow_error; //报告一个算术溢出
class bad_alloc; //存储分配错误
}
标准库异常类定义在以下四个头文件中
1、exception头文件:定义了最常见的标准异常类,其类名为exception。只通知异常的产生,但不会提供更多的信息
2、stdexcept头文件定义了以下几种常见异常类
函数 功能或作用
exception 最常见的问题
runtime_error 运行时错误:仅在运行时才能检测到的问题
range_error 运行时错误:生成的结果超出了有意义的值域范围
overflow_error 运行时错误:计算上溢
underflow_error 运行时错误:计算下溢
logic_error 逻辑错误:可在运行前检测到的问题
domain_error 逻辑错误:参数的结果值不存在
invalid_argument 逻辑错误:不合适的参数
length_error 逻辑错误:试图生成一个超出该类型最大长度的对象
out_of_range 逻辑错误:使用一个超出有效范围的值
3、new头文件定义了bad_alloc异常类型,提供因无法分配内存而由new抛出的异常
4、type_info头文件定义了bad_cast异常类型(要使用type_info必须包含typeinfo头文件)
C++异常参数传递
catch里的参数可以是值类型,引用类型,指针类型。如:
try
{
.....
}
catch(A a)
{
}
catch(B& b)
{
}
catch(C* c)
{
}
尽管表面它们是一样的,但是编译器对二者的处理却又很大的不同。调用函数时,程序的控制权最终还会返回到函数的调用处,但是抛出一个异常时,控制权永远不会回到抛出异常的地方,当我们抛出一个异常对象时,抛出的是这个异常对象的拷贝。当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。此时对象会丢失RTTI信息。异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样:
catch (A& w) // 捕获异常
{
// 处理异常
throw; // 重新抛出异常,让它继续传递
}
catch (A& w) // 捕获Widget异常
{
// 处理异常
throw w; // 传递被捕获异常的拷贝
}
直接调用throw;表示重新抛出异常,继续传递,去寻找相应的catch,第一个块中重新抛出的是当前异常(current exception),无论它是什么类型(有可能是A的派生类),第二个catch块重新抛出的是新异常,失去了原来的类型信息。
一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。
看看以下这三种声明:
catch (A w) ... // 通过传值
catch (A& w) ... // 通过传递引用,一个被异常抛出的对象(总是一个临时对象)可以通过普通的引用捕获
catch (const A& w) ... //const引用
回到异常对象拷贝上来。我们知道,当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝,并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的,当我们这样声明一个catch子句时:
catch (A w) ... // 通过传值捕获
会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中。实际上,编译器会优化掉一个拷贝。同样,当我们通过引用捕获异常时:
catch (A& w) ... // 通过引用捕获
catch (const A& w) ... //const引用捕获
这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。话虽如此,但是不是所有编译器都如此。另外,通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。另外一个重要的差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。在函数传递参数时,如果参数不匹配,那么编译器会尝试一个类型转换,如果存在的话。而对于异常处理的话,则完全不是这样。见一下的例子:
void func_throw()
{
CString a;
throw a; //抛出的是a的拷贝,拷贝到一个临时对象里
}
try
{
func_throw();
}
catch(const char* s)
{
}
抛出的是CString,如果用const char*来捕获的话,是捕获不到这个异常的。尽管如此,在catch子句中进行异常匹配时可以进行两种类型转换。第一种是基类与派生类的转换,一个用来捕获基类的catch子句也可以处理派生类类型的异常。反过来,用来捕获派生类的无法捕获基类的异常。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常:catch (const void*) ... //可以捕获所有指针异常,另外,你还可以用catch(...)来捕获所有异常,注意是三个点。传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的catch子句捕获,这叫异常截获,一般的编译器会有警告
class A
{
public:
A()
{
cout << "class A creates" << endl;
}
void print()
{
cout << "A" << endl;
}
~A()
{
cout << "class A destruct" << endl;
}
};
class B : public A
{
public:
B()
{
cout << "class B create" << endl;
}
void print()
{
cout << "B" << endl;
}
~B()
{
cout << "class B destruct" << endl;
}
};
void func()
{
B b;
throw b;
}
try
{
func();
}
catch (B& b) //必须将B放前面,如果把A放前面,B放后面,那么B类型的异常会先被截获。
{
b.print();
}
catch (A& a)
{
a.print();
}