C语言处理错误的范围性
C语言处理错误经常会用到assert和打印错误码这两种方式。assert可以在检测到错误之后立即终止步伐报错,但是assert只会在debug版才会见效,release版就不会产生结果了,C语言期望步伐员在代码测试阶段就完成对全部错误的排查,太过于抱负。别的,终止步伐的方式也太过于暴力,试想一下生活中我们使用的步伐或网站因为一个小掉线就直接报错终止,未免太过于大惊小怪,使用体验很糟糕。而对于打印错误码的方式,起首打不会直接终止步伐,也不会在release版失效,这两点看起来很好。c语言错误码是通过全局变量的方式实现的,步伐有异常,就会将错误码传给errno(C语言规定的错误码,是一个全局变量),这时可以通过调用perror函数等方式打印错误码,但是这种方式打印的错误码可读性很差,且error作为全局变量必要实时打印,不然下一个错误出来就会被覆盖,这样就要写很多if语句打印错误码,倘若想要使用函数返回统一处理也要一层一层的返回很麻烦,忘记返回也不会报错,就会出现纰漏。总而言之,c语言的这套处理错误的方式还是很麻烦的,所以c++引入了异常来替换这种方式。
异常的界说
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
- #define _CRT_SECURE_NO_WARNINGS 1
- #include<iostream>
- using namespace std;
- void func()
- {
- throw "func() error";
- }
- int main()
- {
- try
- {
- func();
- }
- catch (const char* x)
- {
- cout << x << endl;
- }
- return 0;
- }
复制代码 这就是抛异常的一个简朴的使用场景。场景中有三个异常中会使用到的关键字,分别是:
1.throw:判定当前步伐出现异常时用来抛出异常所使用的。
2.try:try块中的代码会标识将要被激活的特定异常,它后面通常跟着一个或多个 catch 块。try块中的代码被称为掩护代码。
3.catch: 在想要处理问题的地方,通过异常处理步伐捕获异常,catch 关键字用于捕获异常,可以有多个catch进行捕获。
异常的具体使用细则
异常的抛出与捕获
异常使用throw关键字抛出,c++支持抛出恣意类型的异常,而抛出的异常的类型会与对应try块的catch块的参数所匹配(参数只用来匹配的话可以只写类型),匹配成功实验流就会进入该catch块内部,由于抛出的对象只有一个,所以catch块的参数也就只会有一个。
一个try块是可以有多个catch块的,全部抛出的异常会与对应try块的全部catch块自上而下按照次序匹配,找到第一个匹配的参数就会进入,当抛出的异常没有被catch捕获时就会报错。
抛出异常的对象的传递类似函数的返回值,会拷贝生成一个临时对象,这个临时对象的生命周期会持续到对应的catch块实验结束,而且这个对象不像一样寻常的临时变量,这个变量是不具有常形的(const,可以被普通引用接收),因为抛出的对象如果具有常性后续对于抛出对象的修改操作就没法进行了。
catch(…)可以捕获恣意类型的异常,但是也没法知道捕获的是什么类型,一样寻常不会把它放在中间,而是会将其放在末了,以防出现未知错误时实时兜底。
catch语句中的参数匹配规则并不都是要求类型完全匹配。起首,是支持非const对象向const对象转换的;再者,也是支持数组名转数组指针和函数名转函数指针的;然后,const void*指针可以接受任何类型的指针;末了同样也是最重要的,c++支持抛出派生类对象 / 指针 / 引用被基类对象 / 指针 / 引用来接受,这非经常用,在现实中的现实项目中经常采取以基类为基础继续出各个板块的派生类,再由catch块基类类型接受,再利用多态达到接受体系中各种错误的结果,这个后面会详细解说。
在函数调用链中异常栈睁开匹配原则
对于throw出的异常,起首要检查throw本身是否在try块中,在的情况下就会自上而下查找与try块对应的catch块,由匹配的就会到匹配的catch块运行。
如果throw不在当前的函数栈中的try块中大概在try块中但是没有与之匹配的catch块,就会退出当前的栈到调用这个函数的栈中看是否在try块中以及是否有与之对应的catch块。
如果到达main函数栈也依旧没有找到与之匹配的,就会报错终止步伐。上述的这个沿着调用链查找匹配的catch子句的过程称之为栈睁开。一样寻常来说,末了都要加上catch(…)兜底,防止因为疏忽没有捕获导致步伐直接终止。
- #define _CRT_SECURE_NO_WARNINGS 1
- #include<iostream>
- #include<string>
- using namespace std;
- void test()
- {
- throw string();
- }
- int main()
- {
- try
- {
- test();
- }
- catch(...)//接受任何类型
- {
- }
- }
复制代码 在throw出异常后,步伐就不会在按照正常的流程去跑了,先是按照栈睁开(调试中栈睁开这个过程是看不到的,编译器在throw后会直接跳转到匹配的catch块,大概没有匹配的直接报错,不会一个栈一个栈地退,这是编译器优化的结果)进行回退,回退时会消除没有匹配的函数栈帧,到达catch块处理完后会继续实验catch子句后面的语句。
异常的重新抛出
有大概单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
- #define _CRT_SECURE_NO_WARNINGS 1
- #include<iostream>
- #include<string>
- using namespace std;
- void test_2()
- {
- string str("test_2 error");
- throw str;
- }
- void test_1()
- {
- int* a = new int(1);
- test_2();
- cout << "Deletion successful" << endl;
- delete a;
- }
- int main()
- {
- try
- {
- test_1();
- }
- catch (string x)
- {
- cout << x << endl;
- }
- return 0;
- }
复制代码 像上面这种情况,如果自己向堆上动态申请了内存,而且想要在main函数中统一处理异常,此时就会因为throw出的异常导致跳过了堆的内存释放,这样就会导致内存泄漏,
- #define _CRT_SECURE_NO_WARNINGS 1
- #include<iostream>
- #include<string>
- using namespace std;
- void test_2()
- {
- string str("test_2 error");
- throw str;
- }
- void test_1()
- {
- int* a = new int(1);
- try
- {
- test_2();
- }
- catch(string x)
- {
- cout << "Deletion successful" << endl;
- delete a;
- throw;
- }
- cout << "Deletion successful" << endl;
- delete a;
- }
- int main()
- {
- try
- {
- test_1();
- }
- catch (string x)
- {
- cout << x << endl;
- }
- return 0;
- }
复制代码 这时我们就可以先捕获一下异常,在catch块中释放掉申请的内存,然后重新抛出异常给后面的catch块接受统一处理。重新抛出的方法也很简朴,直接在catch块中写throw; 就表示将捕获到的异常再次抛出。
重新抛出的方式可以解决一些简朴场景的内存释放,但对于多次内存开辟加多次函数调用,因为内存开辟也是会开辟失败抛异常的,所以会要求捕获多种类型,就会比力麻烦,必要搭配智能指针来使用。
异通例范
throw(类型)
1.异通例格说明的目标是为了让函数使用者知道该函数大概抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数大概抛掷的全部异常类型。
2. 函数的后面接throw(),表示函数不抛异常。
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
- void test_1() throw()//不会抛异常
- {
- //...
- }
- void test_2() throw(int, double)//只会抛int和double类型的异常
- {
- //...
- }
- void test_3() //可以抛任何类型的异常
- {
- //...
- }
复制代码 这套体系看起来严格规定了函数异常抛出的类型,但是即使不遵守,函数也不会报错,这是c++为了兼容旧代码导致的结果。所以这就象是一种口头承诺了,防君子而不防小人。其实,从现实使用的角度上来说,这样的计划也是不好的,每次写函数表明大概会抛出的异常类型会非常麻烦,而且现实开辟时就连开辟者自己也不会清楚自己这个函数会抛出多少种异常,也许我写的这个函数调用的接口是项目组中的其他人所写的,也有大概未来还要对项目标功能进行扩展调用更多的接口,这些都是不确定因素,全部写明是非常困难且麻烦的。而且throw()是在运行时生成隐式try-catch代码块,动态检查异常类型是否规范,会有一定的性能开销。这种计划本意希望资助调用者预判异常类型,简化错误处理逻辑,但在现实开辟中没什么用处,而且在c++的新版本中也已经渐渐被废弃,throw仅作为提示作用兼容老版本代码,运行时不会强制验证,所以不保举使用。
noexcept
- void test_1() noexcept//不会抛异常
- {
- //...
- }
- void test_2() //会抛各种类型的异常
- {
- //...
- }
复制代码 noexcept是c++11新增的关键字,用来替换throw()用的。函数后面加noexcept表示这个函数不会抛出异常,编译器会严格检查声明函数是否抛出了异常。相较于throw()的运行时的动态检查 ,noexcept是在编译时就确定的静态检查,编译器还进一步优化了noexcept的异常抛出,noexcept的异常抛出不会进行栈睁开一个个释放函数栈帧,而是直接报错,所以noexcept比throw的服从高很多。noexcept声明过的函数表示不会抛出异常,编译器也会对此做出优化,进一步提升函数服从。
综上,现实写代码时如果遇到确定不会抛异常的函数,加一个noexcept优化一下就行了,throw因为各种汗青包袱,现在已经被废弃,没什么人用了。
成熟的异常体系
固然c++在语法上支持我们抛出恣意类型的异常,但是在成熟的项目开辟中是有一套成熟的异常体系的,一个好的异常体系可以大大提高项目开辟服从,减少项目维护成本。
- #define _CRT_SECURE_NO_WARNINGS 1
- #include<iostream>
- #include<string>
- using namespace std;
- class My_Exception
- {
- protected:
- int err_id;
- string err_msg;
- public:
- My_Exception(const string& errmsg, int errid):
- err_id(errid),
- err_msg(errmsg)
- {
- }
- virtual string what() const
- {
- return err_msg;
- }
- };
- class A : public My_Exception
- {
- string A_err_mesg;
- public:
- A(int x, string y, string z):
- My_Exception(y, x),
- A_err_mesg(z)
- {
- }
- virtual string what() const
- {
- string str("A : ");
- str += err_msg;
- str += ",";
- str += A_err_mesg;
- return str;
- }
- };
- class B : public My_Exception
- {
- string B_err_mesg;
- public:
- B(int x, string y, string z) :
- My_Exception(y, x),
- B_err_mesg(z)
- {
- }
- virtual string what() const
- {
- string str("B : ");
- str += err_msg;
- str += ",";
- str += B_err_mesg;
- return str;
- }
- };
- void test_A()
- {
- A x(1, "错误描述", "A组的错误信息");
- throw x;
- }
- void test_B()
- {
- B x(2, "错误描述", "B组的错误信息");
- throw x;
- }
- int main()
- {
- try
- {
- test_A();
- }
- catch (My_Exception& x)
- {
- cout << x.what() << endl;
- }
- try
- {
- test_B();
- }
- catch (My_Exception& x)
- {
- cout << x.what() << endl;
- }
- return 0;
- }
复制代码 上面是一个简朴的一场体系,现实的项目中肯定会更成熟以及复杂。但通过上面的例子可以看出一场体系的关键地点。即界说一个类专门用来记录最根本的异常信息,这里就给出了错误id和错误描述,错误id可以表示哪个模块发生了错误,这个我们可以事先界说好,比如数据库模块的id为001,网络模块的id为002,等等;错误描述可以是一些最根本的错误描述,比如权限不足,数据不存在,等等一些通用的错误描述。这些异常信息必须是全部异常都会有的,然后再由这个类衍生出各种派生类,这些派生类记录现实项目中各个模块的错误信息,他们可以在继续父类的最根本的错误信息的基础之上,再界说一些自己独有的错误信息,比如数据库模块的错误就可以给出具体的sql语句,网络模块错误就给出错误码等等,这些东西的自由度很高,我们可以给出很详细的信息极大的方便我们在代码错误之后的错误查找。然后就是这些错误的捕获,我们之所以使用派生类来作为记录异常的类就是因为c++允许用catch子句用基类的对象 / 指针 / 引用接受派生类的对象 / 指针 / 引用,所以我们就可以界说一个what虚函数,体系中每个类的what虚函数都返回自己整理好的独特的错误信息,这样通过多态达到以一种类型接受整个异常体系中全部异常类的结果,如果不使用这套思绪,面对大型项目标多个模块,光写catch子句就得写累死,所以这套体系还是强烈保举的。
c++自己的异常体系
c++有自己的异常体系,也是通过父子类来完成的。
这些错误我们在一样平常写代码时肯定都遇到过一些。
我们可以用try块包裹住大概会出现错误的位置,使用exception类型就能捕获这些错误发生时会抛出的异常,当然,我们也能自动抛出这些异常,
- #define _CRT_SECURE_NO_WARNINGS 1
- #include<iostream>
- #include<string>
- using namespace std;
- int main()
- {
- try
- {
- throw bad_alloc();//new会抛出的异常
- }
- catch (const exception& e)
- {
- cout << e.what() << endl;
- }
- return 0;
- }
复制代码 c++固然有自己的一套异常体系,但是不敷好用,很多公司都会有自己的一套异常体系。
异常的优缺点
长处
(1)异常对象界说好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,以致可以包含堆栈调用的信息,这样可以资助更好的定位步伐的bug。
(2)返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,具体看下面的详细表明。
(3)很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也必要使用异常。
(4)部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常大概终止步伐处理,没办法通过返回值表示错误。
缺点
(1) 异常会导致步伐的实验流乱跳,并且非常的杂乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析步伐时,比力困难,偶然间打断点都大概断不住。
(2)异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响根本忽略不计。
(3)C++没有垃圾采取机制,资源必要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个必要使用RAII来处理资源的管理问题。学习成本较高。
(4)C++尺度库的异常体系界说得不好,导致大家各自界说各自的异常体系,非常的杂乱。
(5)异常只管规范使用,否则结果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异通例范有两点:一、抛出异常类型都继续自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。
总的来说,异常还是利大于弊,一样平常代码中可以使用来对代码中的各种错误进行检查。c++的异常也被厥后的一些面向对象语言所鉴戒。
异常安全
(1)构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则大概导致对象不完整或没有完全初始化。
(2)析构函数重要完成资源的清理,最好不要在析构函数内抛出异常,否则大概导致资源泄漏(内存泄漏、句柄未关闭等)
(3)C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|