- 程序消亡
程序消亡有几种形式:
(1)程序正常结束—main—return 0;
(2)自杀(自己将自己结束),例如打开一个文件,如果为空,exit(1);
(3)他杀(被别人给杀死), - C语言传统的处理错误的方式
1、暴力法: 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,就会终止程序。
2、返回错误码,例如:
//异常
#include <windows.h>
void TestFunc()
{
FILE* pf = fopen("111.txt", "rb");
if (nullptr == pf)
{
size_t errID = GetLastError();//最近发生的错误
cout << errID << endl;
cout << errno << endl;//最近发生的错误
return;
}
fclose (pf);
}
int main()
{
TestFunc();
return 0;
}
GetLastError这个方法会返回对应的错误码,它的缺陷是需要程序员自己去查找对应的错误。系统的很多库的接口函数都是通过把错误码放到errno中,表示错误
3、setjmp和longjmp组合,但是这个不是很常用,因为他们的限制条件比较多,第一个限制条件就是在调用longjmp之前必须先调用setjmp;还有一个goto语句进行跳转,但是只能在函数中跳转,出了函数就不能跳转了。
例如:
void TestFunc()
{
https://www.baidu.com;
FILE* pf = fopen("111.txt", "rb");
if (nullptr == pf)
{
goto L;
}
L:cout << "EndFunc" << endl;
}
int main()
{
return 0;
}
分析:使用goto语句只能跳到当前函数中,注意:其中www.baidu.com;是一个注释。
再比如:
#include <setjmp.h>
jmp_buf buff;//结构体变量
void Func1()
{
FILE* pf = fopen("111.txt", "rb");
if (nullptr == pf)
{
longjmp(buff, 1);
//return;//不清楚是正常结束还是异常结束
}
//文件操作
fclose(pf);
}
void Func2()
{
int *p = (int*)malloc(0x7ffffff);//没有使用new,new如果申请失败就会抛异常
if (nullptr == p)
{
longjmp(buff, 2);
}
}
int main()
{
//1.记录跳转点:将当前位置的寄存器信息保存到buff中
//2.setjmp第一次返回值一定是0
int state = setjmp(buff);//setjmp第一次返回值一定是0
if (state == 0)
{
//进行正常操作
Func1();
Func2();
}
else
{
switch (state)
{
case 1:
cout << "打开文件失败" << endl;
break;
case 2:
cout << "malloc申请空间失败" << endl;
break;
default:
cout << "未知错误" << endl;
}
}
return 0;
}
分析:以此方式来实现跳转,最终的结果就是打开文件失败
- C++中异常的概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误
(1)throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
(2)catch: 在想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
(3)try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
例如:
void Func1()
{
FILE* pf = fopen("111.txt", "rb");
if (nullptr == pf)
{
throw 1;
}
fclose(pf);
}
void Func2()
{
int *p = (int*)malloc(0x7fffffff);
if (nullptr == p)
{
throw '1';
}
}
int main()
{
try
{
//Func1();
//Func2();
Func2();
Func1();
}
catch (int err)//一个catch只能捕获一种类型的异常
{
cout << err << endl;
}
catch (char err)
{
cout << err << endl;
}
//执行后面的语句
return 0;
}
分析:catch一次只捕获一个异常,按照try关键字中哪个函数先调用,就捕获哪个函数的异常,例如上述代码如果先调用Func1,就捕获Func1中的异常,抛出1,即catch捕获整形类型的异常;如果先调用Func2,就捕获Func2中的异常,抛出字符1,即catch捕获字符类型的异常。一般不会进行类型转换
- 异常的使用
异常的抛出和匹配原则
1、 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
2、被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
例如:
void Func1()
{
FILE* pf = fopen("111.txt", "rb");
if (nullptr == pf)
{
throw 1;
}
fclose(pf);
}
void Func2()
{
int *p = (int*)malloc(10*sizeof(int));
if (nullptr == p)
{
throw '1';
}
try
{
Func1();
}
catch (int err)
{
cout << err << endl;
}
free(p);
}
int main()
{
try
{
Func2();
}
catch (int err)
{
cout << err << endl;
}
catch (char err)
{
cout << err << endl;
}
//执行后面的语句
return 0;
}
在调用Func2时,在Func2中先捕获Func1的异常,否则就无法执行下面的释放malloc申请的空间,主函数中的捕获Func1就不需要了。
3、 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
例如:
class A
{};
void TestFunc()
{
A a;
cout << &a << endl;
throw a;//实际抛出的是a的一份拷贝,因为a是函数中的临时对象
}
int main()
{
try
{
TestFunc();
}
catch (const A& a)
{
cout << &a << endl;
cout << "A Exception" << endl;
}
return 0;
}
分析:发现输出两个a的地址不同,说明抛出的是一个临时对象,临时对象在函数退出时一定会销毁,否则会内存泄漏,只是一个异常对象的拷贝。
而拷贝的临时对象会在catch以后销毁,例如:
class A
{
public:
A()
{
cout << "A::A()" << this << endl;
}
A(const A& a)
{
cout << "A::A(const A& a)" << this << endl;
}
~A()
{
cout << "A::~A()" << this<< endl;
}
};
void TestFunc()
{
A a;
cout << &a << endl;
throw a;//实际抛出的是a的一份拷贝,因为a是函数中的临时对象
}
int main()
{
try
{
TestFunc();
}
catch (const A& a)//将临时的拷贝对象引用
{
cout << &a << endl;
cout << "A Exception" << endl;
}
return 0;
}
分析:当代码运行时,进入TestFunc函数,先构造一个a对象,然后对a对象进行拷贝构造,抛出,这时代码的显示结果如图:
出了函数作用域后,先将构造的对象(地址为008FFA83)析构,这时代码才走到catch,然后代码继续往下走,如图
此时catch以后才析构拷贝的临时对象(地址为008FF9B7)。
4、catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么。这种情况的使用一般有两种情况,第一种是在主函数的最后给一个catch(…)是为了防止代码有类似于第三方库抛出的异常,而我们前面已经将所有能抛出的异常已经全部捕获,在这里是以防万一代码还有别的异常,因为主函数是最后一个对代码进行捕获的函数,所以最好给主函数最后加上catch(…),因此:
函数调用链中异常栈展开匹配原则
(1)首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
(2) 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
(3)如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
(4)找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
第二种就是类似于以下的例子:即异常的重新抛出
//异常的重新抛出,即Func2中不得不对Func1中的异常进行捕获,否则就无法释放空间,内存泄漏,但是没有对Func1中的异常进行处理,可以使用catch(...),重新抛出Func1的异常
void Func1()
{
FILE* pf = fopen("111.txt", "rb");
if (nullptr == pf)
{
throw 1;
}
fclose(pf);
}
void Func2()
{
int *p = (int*)malloc(10 * sizeof(int));
if (nullptr == p)
{
throw '1';
}
try//Func2没有义务为Func1处理异常,但是他对Func1的异常进行了捕获
{
Func1();
}
catch (...)
{
free(p);
throw;//将Func1中的异常接着往出抛,但是不知道是什么异常,因此是catch(...)
}
free(p);
}
int main()
{
try
{
Func2();
}
catch (int err)
{
cout << err << endl;
}
catch (char err)
{
cout << err << endl;
}
//执行后面的语句
return 0;
}
5、 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获
异常安全
1、构造函数进行对象的构造以及初始化,如果在构造函数中抛异常,函数中途退出,可能导致对象不完整或者没有完全初始化
2、析构函数完成资源的清理,如果在析构函数内抛异常,可能导致资源泄漏
3、C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常会导致死锁,C++经常使用RAII来解决以上问题(关于RAll后面会介绍)
异常规范
1、异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
例如:
void Testfunc()throw(int)
{
throw 1.0;
}
在vs下这个会给出一个警告,即忽略了C++异常规范
2、函数的后面接throw(),表示该函数不抛异常。
3、若无异常接口声明,则此函数可以抛掷任何类型的异常
注意:一般采用异常继承体系来捕获异常,例如:
#include <string>
class Exception
{
public:
Exception(size_t errID, const string& errInfo)
:_errID(errID)
, _errInfo(errInfo)
{}
virtual void What()const = 0;//具体不知道异常
protected:
size_t _errID;
string _errInfo;
};
class DBException:public Exception
{
public:
DBException(size_t errID, const string& errInfo)
:Exception(errID, errInfo)
{}
virtual void What()const
{
cout << _errID << "--->" << _errInfo << endl;
}
};
void Func1()
{
DBException e(404,"网络不通");
throw e;
}
void Func2()
{
DBException e(504, "数据库未打开");
throw e;
}
int main()
{
try
{
Func1();
Func2();
}
catch (const Exception& e)//采取基类引用的方式来捕获
{
e.What();
}
catch (const exception& e)//库里的捕获
{
e.what();
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
分析:先给一个基类Exception,给两个成员变量,一个表示异常错误号,一个表示具体是什么异常,给出一个纯虚函数,因为不知道是什么异常,然后公有继承它,派生类DBException对纯虚函数进行重写,将错误号和具体错误打印出来,然后一个使用自己定义的异常继承体系来抛出异常,一个使用标准库中的异常体系,最后进行捕获异常。这样既可以捕获异常,也可以实现多态。
异常的优缺点
C++异常的优点:
1、 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug,提高调试的效率。
2、 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,得层层返回错误,最外层才能拿到错误
3、 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常。
4、 很多测试框架都使用异常,这样能更好的使用单元测试等进行白盒的测试。
5、 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T&operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
C++异常的缺点:
1、 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
2、 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
3、 C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
4、 C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,会造成混乱。
5、 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func()throw();的方式规范化。
源代码(github):
https://github.com/wangbiy/C-/tree/master/test_2019_9_29/test_2019_9_29