(1)在函数中返回一个错误信息。比如Linux经常返回-1代表错误。
也可以设定一个全局的变量,比如errno
(2)用信号函数signal和raise捕捉信号
(3)用setjmp和longjmp两个非局部跳转函数,这种方法很困难,耦合度也很高,因为它和goto不一样,他会跳转到其他地方,而不是局部地点。
以上三种方法都有自己的问题。第一个显得代码过于冗长,第二个信号处理标准不一致就会出很大问题,第三个问题更大,因为它会跳到栈区的其他地方,不会自动调用析构函数,行为危险,结果未知。
try代码块负责运行正常代码,在代码中的错误会通过throw抛出,catch负责接受抛出的错误,做出相应的处理。
(1)throw:抛出异常
#include
#include
//抛出一个异常
//设置一个错误类
class MyError{
private:
const char* const data;
public:
MyError(const char* const msg = 0):data(msg){}//构造函数
};
void f()
{
throw MyError("something bad happened");
}
int main()
{
f();
}
//f()函数抛出一个错误,生成了MyError对象,这是程序创建的一个对象的拷贝,f()函数返>回了这个对象。
catch(type value)
{
some code
}
在try块中编写的代码,所产生的错误都会被根据程序员所设计的throw所抛出,程序会返回到错误开始的地方,如果不对错误做出处理,程序到此就结束了。
try的使用方法为
try{
代码
}
处理抛出的异常的地方就叫异常处理器,他会接住throw抛出的异常,异常处理对继承和多态同样有效
使用方法:
try{
代码
}
catch(类型1 id1)
{
处理方法1
}
catch(类型2 id2)
{
处理方法2
}
……
异常抛出后,会一一匹配catch,如果类型符合,就能够执行catch块的语句
异常被抛出以后,会按照顺序匹配异常处理器,一旦找到能够匹配的,就会开始处理,不会继续匹配下一个,即使下面的更加合适。
匹配一个异常,并不要求异常和处理器之间完全相关,一个对象或者是指向派生类对象的引用都会与其基类处理器匹配。如果不是引用或者指针,而是派生类对象和基类相匹配,
则会发生截断,派生类对象会被截断,与基类相匹配。
#include
class X{
public:
class Trouble{};
class Small:public Trouble{};
class Big:public Trouble{};
void fun(){throw Big();}
};
int main(){
X x;
try{
x.fun();
}
catch(X::Trouble&)
{
std::cout << "捕获到Trouble"<
输出:捕获到Trouble
Small,Big都是Trouble的派生类,抛出Big异常处理的时候,却被第一个异常处理器捕获,不会被第三个捕获。
所以我们在处理异常的时候,可以把基类处理器放在最后面捕获一些未定位的错误。
注意:异常匹配过程不会发生类型转化,以下面的程序为例子
定义了两个类,Except1和Except2,其中Except2定义了一个自动转换类型的函数。
在抛出异常后,异常处理器接受到抛出的错误类型,会匹配到Except1而不是Except2,它不会自动把Except1转化为Except2。
#include
using std::cout;
class Except1{};
class Except2{
public:
Except2(const Except1&){}//类型转换,将Except1转换为其他类型
};
void fun()
{
throw Except1();//抛出一个异常,类型是Except1
}
int main()
{
try{
fun();
}
catch(Except2&)
{
cout << "inside catch(except2)\n";
}
catch(Except1&)
{
cout << "inside catch(except1)\n";
}
return 0;
}
运行结过将会是:inside catch(except1)
catch(…)
{
some code
}
这个表示捕获所有的异常,但是不会接受任何参数,可以把它放在异常处理器的最后面。
抛出的异常不能被忽略,需要被捕获,否者就会出错。
如果没有异常处理器可以捕获抛出的异常,则会进行下面的处理
(1)terminate()函数
如果没有可以匹配异常的异常处理器,则会调用这个函数,这个函数调用了C语言标准库中的abort()函数,程序不会被正常终止,不会调用析构函数
除了异常不会被捕获会调用terminate()函数,还有另外两种情况会调用这个函数
第一种情况:局部对象的析构函数抛出异常
第二种情况:静态对象的构造函数或者析构函数抛出异常
(2)set_terminate()函数
程序员可以使用set_terminate定义自己的terminate()函数,但是在调用结束后,仍然会调用默认的terminate()函数。
使用方法如下。
#include
#include
using namespace std;
void terminator()
{
cout << "terminator 回来了\n";
}
//设置一个函数指针,指向set_terminator
void (*Myterminator)(void) = set_terminate(terminator);
class Botch
{
public:
class Fruit{};
void fun(){
cout << "Botch::f()"<
set_terminate函数是一个这样的函数 void set_terminate(void(*func)(void));
其中func是自己定义的
析构函数抛出异常,catch获取了异常,先调用了自己定义的set_terminate()。但是运行结果和我们预料的不一致。
输出:
Botch::f()
terminator 回来了
Aborted (core dumped)
第一次调用fun函数的时候,确实调用了自定义的terminate函数,但是析构函数所产生的异常却仍然调用了默认的terminate函数,终止了程序。
说明,析构函数抛出的异常,只会调用默认的terminate函数。我们在编写代码的时候,应该避免让构造函数抛出异常。
有时候异常发生了,我们需要对资源进行清理,比如在构造函数中抛出异常,由于构造函数并没有完成,异常发生后,它不会自动调用析构函数清理资源
#include
using namespace std;
class Cat{
public:
Cat(){ cout << "cat()" <
运行结果:
UseResource()
cat()
cat()
cat()
Dog模拟内存分配
n = 44
inside hander
cat的构造函数被调用,Dog却在分配内存的时候抛出一个异常,导致UseResource的构造函数没有结束,所以最后的析构函数没有调用,cat分配的资源仍然没有被释放
为了防止资源泄露,可以使用两种办法来来分配资源
(1)在构造函数里捕获异常,用于释放资源
(2)在对象的构造函数分配资源,并且在对象的析构函数中释放资源
#include
#include
using namespace std;
template
class PWrap
{
private:
T* ptr;
public:
class RangeError{};//异常类
PWrap()
{
ptr = new T[sz];
cout << "PWrap constructor"<= 0 && i < sz)
return ptr[i];
throw RangeError();
}
};
class Cat{
public:
Cat(){cout << "Cat()" << endl;}
~Cat(){cout << "~Cat()"< cats;
PWrap dog;
};
int main()
{
try{
UseResource ur;
}
catch(int)
{
cout << "inside handler" << endl;
}
catch(...)
{
cout << "inside catch(...)" << endl;
}
return 0;
}
这种方法的好处在于在创建UseResource对象之前,指针类就已经被创建了,所需要创建的对象嵌入到了指针对象中。当Dog出现异常时,cat的析构函数被调用了,就不会有
内存泄露。
输出:
Cat()
Cat()
Cat()
PWrap constructor
动态创建Dog对象
~Cat()
~Cat()
~Cat()
PWrap destructor
inside handler
这样的方法实际上也用于智能指针。
2.3 函数级的try块
#include
using namespace std;
int main()try
{
throw "主函数抛出异常";
}
catch(const char* msg){
cout << msg << endl;
}
在函数后面使用try,然后紧跟着函数使用catch接受抛出的异常,这种方法也可以用在类的内部,比如用在构造函数里面。
C++标准中规定了异常,程序员可以直接使用他们,在构造自己的异常类的时候可以用他们当作基类
所有的标准异常都是从exception类派生出来,定义在头文件
派生出来的这两个类,都有构造函数,使用了string,他们会把错误消息用string保存,我们能够通过函数what()获取到这个信息。
而logic_error和runtime_error也派生出来了其他的异常类,可以查看相关资料
有时候我们需要知道函数抛出的异常类型,这时我们可以遵循这个规格提示程序员。
规格说明写在函数后面,用关键字throw标注。比如:
void fun() throw(int,float,const char*)
{
}
这说明这个函数如果抛出异常,会抛出这三种类型的异常
void fun();意味这可能会抛出任何类型的异常
void fun() throw();意味不会抛出任何类型的异常
(1)如果抛出的异常不在说明的列表中,则系统会自动调用unexcept()函数,而这个函数会调用之前提到的terminate()函数。
(2)set_unexcepted()函数的原型是这样的: void set_unexcepted(void (*) (void))
程序员可以用它调用自己设计的异常函数,而不必使用系统默认的unexcepted()函数。
使用自己设定的unexcepted()函数,可以重新抛出一个异常,让下面的代码接受。
如果依然没有可以匹配的异常处理器,则会发生两种情况:
(a).如果异常规则列表中含有bad_exception类,怎会把异常转化为bad_exception对象,然后程序回到调用的位置重新开始匹配异常
(b).如果什么都匹配不到,则调用默认的unexpected()函数。
#include
#include
using namespace std;
//定义两个异常类
class A{};
class B{};
//定义一个自己的terminate
void my_terminate()
{
cout << "my_terminate" << endl;
}
//定义自己的unexcept()
void my_unexpectedA()
{
cout << "my_unexpectedA"<
输出:
第一个try
f2执行
f1执行
my_unexpectedA
catch A from f2()
f3执行
f1执行
my_unexpected
catch bad_exception from f3
f2执行
f1执行
my_unexpected
my_terminate
Aborted (core dumped)
继承中使用标准异常说明
派生类的异常列表需要比父类的列表更窄。
写累了,这一段略过