异常的介绍
1.C语言传统的处理错误的方式
今天来看看异常,祖师爷实在难以接受C语言处理错误的方式,所以就弄出了异常。C语言处理错误的方式有1.终止程序,如assert、内存错误、退出函数,这些方式C++觉得没问题可以继续用;2.返回错误码,出错了告诉我们是几号错误。但问题是怎么知道是哪个模块出错了?比如项目100w行代码分了10个模块,10个组实现。每个组5人。此时进行错误编号,如把错误码1-100分给第一组,101-200分给第2组……只有这样规范管理,出错了才知道是哪一块问题。但万一有人不规范胡乱返别的组的错误码,这样查错时容易被误导。而且比如规范的情况下报了5号错误,那是什么意思呢?所以还需要一个错误码表,出错了拿这个编号查表。因此错误码有两大问题:1.要层层返回 2.告诉几号错误后还要去查错误信息。
2.C++异常概念
于是C++直接使用异常的方式来解决问题:异常首先是要try catch,只有包含在try catch里才能进行捕获;还有可能try catch的Func,Func抛异常,Func的下一层也可能抛异常,都可以捕:

上面是如果发生除0错误就会抛异常。调式可以观察到:如果没有没有错误和正常程序走没有差别,如果抛了异常会直接跳动catch的地方,异常是通过throw发生的。
下面来看看异常的相关特性:异常是由抛出对象引发的,也就是throw时会有异常,出错了就throw,throw可以抛任意类型对象(整型、字符串、string、自定义类型等)。如果类型不匹配会发生什么?

比如抛的是字符串,捕了一个int,发现类型不匹配时捕获不到的(可理解为实参和形参那样)。抛出的异常如果没有被捕获,编译器就会报错。下面看看检查原则:

它会一层层去检查,抛出过程有个栈展开的过程:比如是多层调用,main调Func1,Func1调Func2,现在Func2里有throw。Func2会先看自己本身有没有try catch,如果在就看当前栈有没有匹配的catch,匹配就跳到catch那里。如果没有匹配就退出当前栈去上一次看,如果没有再到上一层……如果到main函数依旧没有匹配就终止程序。抛了异常(throw)没有被捕获可能有这些情况:类型不匹配、没有捕获(catch)、throw不在try块内部。如果捕获了,它会沿着catch子句继续执行:

了解上述后看一个问题:

如果Func中也try catch了,会跳到谁?会跳到func,因为前面说了是不断去上一层看:

在看下面有什么问题:

语法上没有报错,但是throw 1没有意义,它执行不了。
前面说了可以抛任意类型的异常,那现在抛个string类型的异常出来。因为string出作用域就析构了,这时会出现野指针什么的吗?

发先没有错误,说明抛异常其实和返回值非常像,它会进行拷贝,返回的是一个拷贝,这个拷贝对象直到被捕获后才销毁。可以理解为抛出的对象不是string,是string的拷贝,catch接收的也是,所以string该销毁就销毁,不会有任何影响。
有时候还会有这样一种情况,有人可能不注意抛出了一个我们没有捕获的异常;虽然是个小事,但这样会导致程序直接终止。为了防止有人不小心抛了异常我们又没有捕获,异常捕获机制里给了...方式。三个点意思是可以捕获任意类型异常,但捕获了任意类型的问题是也不知道是什么样的错误,没法打印,所以一般输出未知异常:

它是为了防止抛出一些不规范的异常出来使程序终止。再看下面问题:

现在既有捕获任意类型,又有捕获string,报错了。所以捕获未知异常一般放在最后,放前面的话后面的catch就没有机会执行了,因此前面的都走完了再进行相关捕获。它是最后一道防线,抛了它意味着有人没按照规范走。
日常项目里捕获异常会记录一下日志,其次捕获异常还可能进行处理。比如写一个SendMsg函数,就是把某个消息发送给你,但有可能消息发送失败,发送失败一般有很多情况(C++机制用try catch)。错误信息一般不能随便抛,比如抛个整型和错误码没有区别,抛string只知道描述不能区分错误类型(如信息说文件出错,但不知道是权限问题还是文件不存在等问题,无法很好的处理)。因此通常会抛自定义类型来解决问题,自定义类型通常包含两个信息:1.错误信息 2.错误id:

为啥要有id呢?因为发送消息失败可能有很多错误类型:1.没有权限、2.网络故障等。比如看了id说是没有权限,那到底什么没有权限?描述中说文件错误,这样就知道文件没有权限,两者配合才能准确解决问题。再比如:

比如catch后看id发现如果是2号错误网络故障,那么就尝试不断的发送消息;如果是其它类型的错误,就记录在日志里面,然后退出。所以异常可包含更丰富的信息来支撑我们去做各种应用,对不同场景做不同处理。因此虽然C++支持抛任意异常,但是实际不能随便抛,一般要抛一个自定义类型,这个自定义类型怎么定义都行,但至少包含id(错误编号,很好的分辨错误)和errmsg(错误描述,描述谁的错误)。
3.自定义异常体系
但是实践中异常需求比上述还要复杂,比如写一个服务端会分为网络、缓存、数据模块这些组,现在捕获异常都是exception的话到底找哪个组的人来看异常?有些模块还希望带一些信息出来,这样若每个组按各自需求定义各自类,捕获的地方就很多很麻烦。祖师爷也想到了这样的问题,觉得这些异常类都要具有共性,都肯定有id和errmsg。祖师爷给了个例外,可以抛派生类对象,用基类捕获,因为派生类对象可以切片,赋值兼容给父类,这个过程是天然的,没类型转换发生。

实践中公司会定义一个规范,如异常叫exception,里面有id和errmsg,大家想怎么抛就怎么抛,但大家必须抛基类或派生类,也就是要抛一个根据需求自定义类没问题,前提是要继承基类。下面看看实践中项目跑的方法再来理解一下(模拟服务器出错过程):

现在规定了一个Exception,普通的错误可以直接抛Exception,catch后可以通过what来直到错误信息是什么。但是上面三个模块里面出了相关错误直接抛Exception,外部都拿到权限不足也不清楚谁的权限不足。若每个模块自己定义一个类抛出,外部接收就特别乱,于是每个模块要想自定义异常类就需要先继承规范,然后添加自己需求:

完整代码:
class Exception
{
public:Exception(string errmsg, int id):_errmsg(errmsg),_id(id){}virtual string what() const{return _errmsg;}protected:string _errmsg;int _id;
};class SqlException : public Exception
{
public:SqlException(const string& errmsg, int id, const string& sql):Exception(errmsg, id), _sql(sql){}virtual string what() const{string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}
private:const string _sql;
};class CacheException : public Exception
{
public:CacheException(const string& errmsg, int id):Exception(errmsg, id){}virtual string what() const{string str = "CacheException:";str += _errmsg;return str;}
};class HttpServerException : public Exception
{
public:HttpServerException(const string& errmsg, int id, const string& type):Exception(errmsg, id) //调用基类构造函数, _type(type){}virtual string what() const{string str = "HttpServerException:";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};void SQLMgr()
{srand(time(0));if (rand() % 7 == 0){throw SqlException("权限不足", 100, "select * from name = '张三'");}//throw "xxxxxx";
}void CacheMgr()
{srand(time(0));if (rand() % 5 == 0){throw CacheException("权限不足", 100);}else if (rand() % 6 == 0){throw CacheException("数据不存在", 101);}SQLMgr();
}void HttpServer()
{// ...srand(time(0));if (rand() % 3 == 0){throw HttpServerException("请求资源不存在", 100, "get");}else if (rand() % 4 == 0){throw HttpServerException("权限不足", 101, "post");}CacheMgr();
}int main()
{while (1){Sleep(1000);try {HttpServer();}catch (const Exception& e){//多态cout << e.what() << endl;}catch (...){cout << "UnKnown Exception" << endl;}}return 0;
}这里what函数是虚函数,派生类继承后可以对虚函数重写,里面重新构造错误信息等;错误信息在throw时传进去,what里组织起来进行返回。并且catch时是多态,可以做到指向谁调用谁。假如有人不小心忘了继承,此时Exception不能匹配,因为没继承是两个独立的类,此时走catch(...)。
4.C++标准库的异常体系
C++标准库也定义了一套异常,C++标准库异常体系基类叫exception:

所以捕C++标准异常就捕exception,它实现了一个叫what的虚函数,下面的派生类都重写了what。还发现它的析构函数定义成虚函数,重写后可以根据对象实际类型调用相应析构函数。下面是C++标准库中定义的主要异常类型,以及它们的简要描述和常见抛出场景,简单看看就行:

5.其它特性
再来看一些异常的其它特性,第一个叫异常规范,比如:

看到函数后面有throw是什么意思呢?异常的缺点是导致执行流乱跳:

比如从上到下调好几个函数声明,我们也不知到这几个函数会不会抛异常,要不要捕获?这样非常难受,所以祖师爷定义了一套规范,如果不抛异常就用throw加一个括号 throw()。总结一下:1.异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。2.函数的后面接throw(),表示函数不抛异常。3.若无异常接口声明,则此函数可以抛掷任何类型的异常。还有一些情况(抛多个类型异常要用逗号进行分割):

C++11进行了简化,不抛异常加noexcept,标记出不抛异常的函数就行(异常规范在函数声明的后面加)。如果说了没有异常还抛异常会报错:

下面谈谈异常安全的问题,异常最好不要在构造函数、析构函数抛异常。如果抛异常对象可能只初始化一半就走了,或者析构函数只清理了一半。看一个场景:

这里正常new和delete,没异常时只看两个匹配就行。有异常时Division抛异常会直接到catch地方,导致内存泄漏。可以这样解决:

catch以后后面的句子是正常运行的。假设要求最外面统一处理错误,所以捕获完后可以再重新抛出:

意味着这里捕获异常仅仅是为了拦截下来释放了再抛。但还是有坑:

万一还有个func函数,func函数抛了异常还有考虑catch func的情况。所以可改为上图右边,捕什么抛什么。再看个问题:

new1那里抛了异常没事,new2那里抛了异常new1就释放不了了。
6.异常的优缺点
C++异常的优点:
1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。
3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如 T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
C++异常的缺点:
1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用func() throw();的方式规范化。
