一篇文章带你了解C++中的异常
作者:是小明同学啊
异常
在c语言中,对错误的处理总是两种方法:
1,使用整型的返回值表示错误(有时候用1表示正确,0表示错误;有的时候0表示正确,1表示错误)
2,使用errno宏(可以简单理解为一个全局整形变量)去记录错误。(如果错误,就将被改变的全局整形变量返回)
c++中仍然可以用上面的两种方法,但是有缺点。
(1)返回值不统一,到底是1表示正确,还是0表示正确。
(2)返回值只有一个,通过函数的返回值表示错误代码,那么函数就不能返回其他的值。(输出的值到底是表示异常的值-1,还是最后在那个结果就是-1呢)
抛出异常基本操作
c++处理异常的优点:
异常处理可以带调用跳级。
在C程序中出现了异常,返回值为-1。如果C直接将-1传给B,不进行处理(也不给B报错),那么B收到-1返回值以后就会进行自己的处理,然后返回给A,然后A再进行自己的处理,那么最终程序返回的值肯定是错误的。
所以在c++中,要求必须要处理异常。如果C处理不了允许抛给B处理,B处理不了也允许抛给A处理,如果A也处理不了,那么就直接终止代码报错。
int myDivision(int a, int b) { if (b == 0) { throw -1;//抛出-1 } else return 1; } int main() { int a = 10; int b = 0; try { myDivision(a, b); } catch (int) { cout << "int类型异常捕获" << endl; } return 0; }
如果抛出来的是char类型的数据(异常),那么就需要有个char类型的接收处理代码(catch+类型)。
除了int,char,double以外的抛出类型,可以用...
来接收。
catch (...) { cout << "其他类型异常捕获" << endl; }
如果捕获到了异常,但是不想处理,那么可以继续向上抛出异常。
int myDivision(int a, int b) { if (b == 0) { throw -1; } } void test() { int a = 10; int b = 0; try { myDivision(a, b); } catch (int) { throw; } } int main() { try { test(); } catch (int) { cout << "int类型异常捕获" << endl; } return 0; }
自定义的异常类
注意:类名加()就是匿名对象
class MyException { public: void printError() { cout << "我自己的异常" << endl; } }; int myDivision(int a, int b) { if (b == 0) { throw MyException();//类名加()就是匿名对象,抛出的就是匿名对象。 } } int main() { int a = 10; int b = 0; try { myDivision(a, b); } catch (MyException e) { e.printError();//可以直接用这个对象来调用成员函数 } return 0; }
总结:
1,c++中如果出现异常,不像c中return -1,而是直接throw -1,然后后面再用try catch进行处理。
2,可能出现异常的地方使用try
3,如果与抛出的异常匹配的处理没有找到,那么运行函数terminate将被自动调用,其缺省功能调用abort终止程序。
栈解旋
从try代码行开始 到 throw将代码抛出去之前。所有栈上的数据会被自动的释放掉。
释放的顺序和创建的顺序是相反的。(栈:先进后出)
class Person { public: Person() { cout << "Person的默认构造调用" << endl; } ~Person() { cout << "Person的析构调用" << endl; } }; int myDivision(int a, int b) { if (b == 0) { Person p1; Person p2; throw Person();//匿名对象 } } int main() { int a = 10; int b = 0; try { myDivision(a, b); } catch (Person) { cout << "拿到Person类异常,正在处理" << endl; } return 0; }
输出结果:
Person的默认构造调用
Person的默认构造调用
Person的默认构造调用
Person的析构调用
Person的析构调用
拿到Person类异常,正在处理
Person的析构调用
在throw之前创建了两个对象,并抛出一个匿名对象。发现在抛出去之前,两个对象就被释放了。然后抛出去的对象在程序结束时候释放。这就是栈解旋
异常接口声明
只允许抛出规定类型的异常。
//异常接口的声明 void func() throw(int , double)//只允许抛出int和double类型的异常。 { throw 3.14; } int main() { try { func(); } catch (int) { cout << "int类型异常捕获" << endl; } catch (...) { cout << "其他类型异常捕获" << endl; } return 0; }
throw()的意思就是不允许抛出异常。
这个代码在VS中是不能正确执行的,都不会报错。但是在QT和linux下是可以正确执行的。
异常变量的生命周期
class MyException { public: MyException() { cout << "MyException的默认构造调用" << endl; } MyException(const MyException&e) { cout << "MyException的拷贝构造调用" << endl; } ~MyException() { cout << "MyException的析构调用" << endl; } }; void doWork() { throw MyException();//抛出匿名对象 } int main() { try { doWork(); } catch (MyException e) { cout << "自定义异常的捕获" << endl; } return 0; }
运行的结果:
MyException的默认构造调用
MyException的拷贝构造调用
自定义异常的捕获
MyException的析构调用
MyException的析构调用
throw匿名对象的时候创建了对象,所以用默认构造。
用MyException e来接收对象的时候,是用的值来接收的,所以会调用拷贝构造函数。
然后就打印,并且将两个对象删除掉。
这样效率不高,如果接收对象的时候不用值来接收,而是用引用来接收,这样就能少调用一次的拷贝构造和一次析构函数。
catch (MyException &e) { cout << "自定义异常的捕获" << endl; }
运行结果:
MyException的默认构造调用 自定义异常的捕获 MyException的析构调用
还有一种方式,就是将匿名函数的地址穿进来,这样也不需要调用析构函数。
class MyException { public: MyException() { cout << "MyException的默认构造调用" << endl; } MyException(const MyException&e) { cout << "MyException的拷贝构造调用" << endl; } ~MyException() { cout << "MyException的析构调用" << endl; } }; void doWork() { throw & MyException();//抛出匿名对象 } int main() { try { doWork(); } catch (MyException *e) { cout << "自定义异常的捕获" << endl; } return 0; }
运行结果:(其实没有运行成功)
MyException的默认构造调用
MyException的析构调用
自定义异常的捕获
如果传的是指针,那么匿名对象很快就会释放掉(匿名对象的特点就是执行完就释放掉),最终得到了指针也没有办法进行操作。
但如果匿名对象在=的右边,且左边还给这个对象起名了(如同上面的传对象,引用接收),那么匿名对象的寿命就会延续到左边的变量上。如果传的是指针,给指针起名和给对象起名不一样,所以就会释放。
如果不想被释放掉,还有一种方式,那就是将这个对象创建在堆区,等待着程序员自己去释放。(不会调用析构)
void doWork() { throw new MyException();//抛出匿名对象 }
异常的多态
//异常的基类 class BaseException { public: virtual void printError() = 0;//纯虚函数 }; //空指针异常 class NULLPointerException:public BaseException { public: virtual void printError() { cout << "空指针异常" << endl; } }; //越界异常 class outOfRangeException :public BaseException { public: virtual void printError() { cout << "越界异常" << endl; } }; void doWork() { //throw NULLPointerException(); throw outOfRangeException(); } int main() { try { doWork(); } catch (BaseException &e)//用父类的引用接收子类的对象 { e.printError(); } return 0; }
提供一个基类的异常类,其中有个纯虚函数(有可能是虚函数),然后子类重写。
调用的时候,用父类的引用来接收子类的对象就可以,这样就实现了异常的多态。抛出的是什么类的对象,那么就会调用什么类的函数。
c++的标准异常库
标准库中提供了很多的异常类,它们是通过类继承组织起来的。
如果使用系统提供的标准异常的时候,需要调用规定的头文件
#include <stdexcept>
std:标准 except:异常
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include<string> #include<stdexcept> class Person { public: Person(int age) { if (age < 0 || age>150) { throw out_of_range("年龄必须在0-150之间"); } } int m_age; }; int main() { try { Person p(151); } catch (out_of_range&e) { cout << e.what() << endl;//what函数是获得字符串中的内容 } return 0; //如果使用多态:(异常子类的名字太难记,不好写) //catch (exception &e) }
自己平时不会主动调用系统的标准异常。在写的系统提供的异常后面加()的字符串然后在接收的时候用父类的引用接收,然后用这个引用e的what函数就可以找到这个字符串。
编写自己的异常类
标准异常类是优先的,可以自己编写异常类。
和上面自己写的MyException不太一样。给系统提供的派生类exception提供儿子(需要重写父类的函数等)
ps:在非静态成员函数后面加const,表示成员函数隐含传入的this指针为const指针,决定了在该成员函数中,任意修改它所在的类的成员操作是不允许的。
经过考察上面的有关out_of_range的代码可得:抛出的是out_of_range类的一个对象,接收的时候也是用引用e来接收的这个对象。然后这个引用可以调用what()的函数来返回一个字符串,这个字符串正好是创建out_of_range对象的时候待用有参函数要传入的 字符串。
所以,自己写的out_of_range类一定要有个有参构造,参数就是字符串,然后还有个what的重写函数,需要返回这个字符串,这个字符串作为属性。
ps:注意:const char*可以隐式转换为string,但是反过来就不成立。
所以如果要使得string转换成const char*,需要调用string中的成员函数函数
.c_str()
const char* what() const { string s; return s.c_str(); //返回的就是const char*了。 }
完整代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include<string> #include<stdexcept> class MyOutOfRangeException:public exception//先继承一下这个父亲 { //到底要重写什么呢?点开exception以后,发现有两个virtual的虚函数,一个析构,还有一个what,析构不需要重写 //所以需要重写what函数。 public: MyOutOfRangeException(const char* str) { //const char*可以隐式类型转换为string 反之不可以 this->m_myerrorString = str; } //可以再重载一下这个函数,使得接收的参数改为string类型 MyOutOfRangeException(string str) { this->m_myerrorString = str; } virtual char const* what() const { return m_myerrorString.c_str();//加了.c_str就可以返回const char*了 } string m_myerrorString;//字符串属性 }; class Person { public: Person(int age) { if (age < 0 || age>150) { throw MyOutOfRangeException("年龄必须在0-150之间");//const char* throw MyOutOfRangeException(string("年龄必须在0-150之间"));//string,返回的是string类的匿名对象 } else { this->m_age = age; } } int m_age; }; int main() { try { Person p(1000); } catch (MyOutOfRangeException e)//用exception也可以,证明创建的这个类确实是exception的子类。 { cout << e.what() << endl; } return 0; }
但是最后发现好像没有成功的将MyOutOfRangeException手写异常类变成exception的子类,不知道为啥。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!