异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。
异常检测: 当程序的某部分检测到一个它无法处理的问题时,需要用到异常处理。此时,检测出问题的部分应该发出某种信号以表明程序遇到了故障,无法继续下去了,而且信号的发出方无须知道故障将在何处得到解决。一旦发出异常信号,检测出问题的部分也就完成了任务。
异常处理: 如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。异常处理部分就是用来对捕获的异常进行处理的。
异常处理机制: 为程序中异常检测和异常处理这两部分的协作提供支持。
在C++中,异常处理包括:
throw表达式,异常检测部分使用throw表达式来表示它遇到了无法处理的问题。throw引发了异常
try-catch语句块,异常处理部分使用try-catch语句块处理异常。try{}后以一个或多个catch(){}子句结束,try{}中的代码抛出的异常通常会被某个catch子句处理。catch子句通常被称为异常处理代码。catch(…)意为捕获所有异常,如果有多个catch子句,catch(…)必须放在最后,否则后面的catch语句不可能得到执行。
异常类,用于在throw表达式和相关的catch子句之间传递异常的具体信息。
所需头文件
#include<exception> //定义了最通用的异常类exception,只报告异常的发生,不提供任何额外信息
#include<stdexcept> //定义了几种最常用的exception类
抛出异常
void fun(){
···;
throw runtime_error("xx error!");//抛出异常
···;
}
捕获异常
try{
//try语句块中调用的fun()函数抛出异常
fun();
}catch(runtime_error& e){
//捕获runtime_error异常
cout<<e.what()<<endl;//打印异常错误信息
}
异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。异常能使得我们能够将问题的检测与解决过程分离开来。程序的一部分负责检测问题的出现,另一部分负责问题的解决。
C++中通过throw表达式来引发(raised)异常,被抛出的表达式的类型(即异常类型)以及当前函数调用链共同决定了哪段处理代码(handler,即哪个try-catch块)将被用来处理该异常。
当执行一个throw时,跟在throw后面的语句将不再被执行。程序的控制权从throw转移到与之最先匹配到的catch模块。这会引发两个后果:
抛出一个异常后,程序将暂停当前函数的执行过程,并寻找与之匹配的catch子句。将会沿着函数调用链从内到外逐步检查,如果throw语句或调用该throw语句的函数或在该throw语句的调用链上的函数在try语句块内,且该try子句对应的catch子句中存在对该异常的处理,则程序进入该catch子句并从这里开始继续执行。这个过程被称为栈展开,即从最内层函数逐步向外层展开,直到找到匹配的catch语句或到最外层还没有找到匹配。如果一直到main函数还没有找到匹配的catch子句,程序将会调用标准库中的terminate函数,终止程序的执行。
在栈展开的过程中,将沿着函数调用链逐步向外,内层的函数将会退出,这些函数中的局部对象将会被随之销毁。对于内置对象(int,double等),编译器无需任何操作,因为找到匹配的catch块后程序的函数栈将会退栈,调用链上的栈帧将会被销毁,这些对象自然失去了意义,也不会再被访问到。对于类对象而言,将会自动执行析构函数,具体原理后面将会介绍。对于动态分配的指针,退栈后无法再得到这些指针,因此没有办法找到相应的动态分配内存,会造成内存泄漏的问题。因此对于动态分配相关的资源管理问题,最好用类来管理,例如用构造函数new,析构函数delete,或直接使用智能指针。
以下是一个例子,可以看出异常处理链的执行情况以及类对象析构情况。
#include<iostream>
#include<stdexcept>
#include<memory>
using namespace std;
//A调用B,B调用C,C抛出异常,B catch所有异常并全部抛出,A捕获异常
void funA();
void funB();
void funC();
class ClassA
{
public:
ClassA():pi(new int[len]) {
cout << "ClassA building" << endl; }
~ClassA() {
cout << "ClassA deconstruction" << endl; }
};
void funA()
{
try {
funB();
}catch (exception& e) {
cout << "funA() -- catch --> ";
cout << e.what() << endl;
}
}
void funB()
{
try {
funC();
}catch (runtime_error& e) {
cout << "funB() -- catch --> ";
cout << e.what() << endl;
}
}
void funC()
{
//该函数用于抛出异常,函数会提前退出,动态分配内存无法被释放,但类对象会被成功析构
cout << "funC() -- throw之前" << endl;
ClassA ca;
int* a = new int(10);
cout << "函数体内 new 分配内存" << endl;
throw runtime_error("xx异常"); //抛出异常,之后的代码得不到执行
cout << "funC() -- throw之后" << endl;
delete a;
cout << "函数体内 delete 释放内存" << endl;
}
int main()
{
funA();
}
程序运行结果如下:
funC() -- throw之前
ClassA building
函数体内 new 分配内存
ClassA deconstruction
funB() -- catch --> xx异常
可以看到,throw语句后面的代码全部没有执行,因此指针a所指向的动态内存空间永远也得不到释放,造成了内存泄漏,同时也能看到,ClassA的构造、析构均得到了执行,析构应该是在栈展开的过程中完成的。
不能让异常逃离析构函数,由于抛出异常后的栈展开过程将会执行析构函数来销毁对象,因此析构函数内部如果存在异常,是不能让异常逃离析构函数的,必须在析构函数内部就将异常捕获。否则,由于已经存在了一个异常,析构函数又向外抛出一个,异常调用链将会被破坏,程序将被终止。
异常声明
catch(exception e) //√
catch(exception& e) //√
catch(exception&& e)//×,不允许右值引用
如果catch的参数是基类类型,可以用派生类类型对其进行初始化。如果catch参数是引用类型,将会动态绑定;如果是非引用类型,异常对象将被切割为基类对象。
重新抛出
在catch语句或catch语句调用的函数中,使用空的throw语句可以将异常重新抛出,交给更上层的函数处理异常。
void funA()
{
try {
funB();
}catch (exception& e) {
//捕获funB重新抛出的异常
cout << "funA() -- catch --> ";
cout << e.what() << endl;
}
}
void funB()
{
try {
funC();
}catch (runtime_error& e) {
cout << "funB() -- catch --> ";
cout << e.what() << endl;
throw;//重新抛出
}
}
捕获所有异常
使用catch(…)来捕获所有异常,catch(…)通常与throw重新抛出语句一起使用
try{
···
}catch(...){
···do something
throw;
}
由于构造函数分为初始化列表和函数体这两个部分。如果在函数体中使用try-catch语句块,是无法捕获初始化列表中可能出现的异常的。需要function try block形式的try-catch语句来处理构造函数初始值抛出的异常。
如下:
class ClassA
{
public:
//函数try语句块,函数体内的try无法处理构造函数初始值列表中抛出的异常
ClassA(long long len)try:pi(new int[len]) {
cout << "ClassA building" << endl; }
catch(bad_alloc& e){
cout << "动态内存分配失败: ";
cout << e.what() << endl;
}
~ClassA() {
cout << "ClassA deconstruction" << endl; delete[] pi; }
private:
int* pi;
};
其中try出现在构造函数初始值列表的冒号前,catch出现在函数体之后。
noexcept用于说明一个函数不会抛出异常。
void fun1() noexcept {
cout << "fun1()" << endl;
}
void fun2() throw() //throw()与noexcept等价
{
fun1();
cout <<"noexcept(fun1()) -> "<<noexcept(fun1()) << endl;;
}
void fun3() noexcept(true); //不抛出异常,等价于noexcept
void fun4() noexcept(false);//默认情况
如果一个函数提供了noexcept说明,却违反了它,仍然会编译通过,但是编译器将会调用terminate终止程序。
noexcept运算符
用于表示给定的表达式是否会抛出异常。
noexcept(fun1());//true
noexcept(funA());//false
函数指针的异常说明
1.函数指针与该指针指向的函数必须有一致的异常说明
2.做出不抛出异常声明的的指针只能指向不抛出异常的函数
3.可能抛出异常的指针可以指向不论是否抛出异常的函数
//fun1与fun2定义如上
void (*pf1)() noexcept = fun1; //√
void (*pf2)() = fun1; //√
pf2 = funC; //√
pf1 = funC; //不兼容的异常规范×
继承体系中的异常说明
对于存在虚函数的继承体系而言
如果一个虚函数承诺了它不抛出异常,派生的虚函数也必须做出同样的承诺
如果基类的虚函数允许抛出异常,派生类既可以抛出也可以不允许抛出异常
class Base {
public:
virtual double f1(double) noexcept;
virtual int f2() noexcept(false);
virtual void f3();
};
class Derived : public Base {
public:
double f1(double) override;
//“double Derived::f1(double)”: 重写虚函数的限制性异常规范比基类虚成员函数“double Base::f1(double) noexcept”少
int f2()noexcept(false);
void f3() noexcept;
};
异常类继承体系
标准异常类继承体系
exception
|_________bad_cast(类型转换异常:例如将派生类指针指向基类)
|
|_________bad_alloc(内存分配异常:例如new分配空间时堆区没有足够的空间)
|
| _____overflow_error(上溢)
|_________runtime_error(运行时错误) |_____underflow_error(下溢)
| |___________________|_____range_error(生成的结果超出了有意义的值域范围)
|
|_________logic_error(逻辑错误)
|________________________domain_error(域错误)
|____invalid_argument(无效参数)
|____out_of_range(使用一个超出有效范围的值,例如访问超出容器范围的变量)
|____length_error(试图创建一个超出该类型最大长度的对象)
这里可以查看各个异常的作用(http://www.cplusplus.com/reference/stdexcept/),例如对于domain_error,解释如下:
This class defines the type of objects thrown as exceptions to report domain errors.
此类定义作为异常引发的对象类型,以报告域错误。
Generally, the domain of a mathematical function is the subset of values that it is defined for. For example, the square root function is only defined for non-negative numbers. Thus, a negative number for such a function would qualify as a domain error.
一般来说,数学函数的域是定义它的值的子集。例如,平方根函数只为非负数定义。因此,这样一个函数的负数将被视为域错误。
No component of the standard library throws exceptions of this type. It is designed as a standard exception to be thrown by programs.
标准库的任何组件都不会抛出这种类型的异常。它被设计成程序抛出的标准异常。
C++的异常处理,改变了栈帧的结构,相比传统的无异常处理代码或C函数代码,栈帧中多出了包含piPrev、piHandler、nStep等部分的结构体EXP。异常处理部分采用链式结构,piPrev用于指向前一个栈帧中的异常处理部分,nStep用于定位try块的位置,piHandler指向完成异常捕获和栈展开所必须的数据结构(包括try块表和栈展开表)。是不是感觉一头雾水?关于这些奇怪的变量更加详细的解释可以在参考文献中找到原文,这里就讲点简单的。
我们知道,函数中的局部变量、返回地址、寄存器中不够存储的参数等等都是存储在栈中的,每个函数中的这些信息组合起来称为一个栈帧(stack frame)。函数调用完后,栈指针将会指向上一个栈帧,而调用完的函数所在的栈帧将会消亡,其中所有的局部变量都失效,如果有指针指向其中的局部变量,对该指针的使用将会造成未定义的行为。
对于C++中的类,其中的数据成员类似结构体,成员函数类似普通的函数,唯一的不同就是需要传入this指针。哪怕是含有virtual函数的类,其结构中多了vptr虚表指针,这样的类也能一如常规的方式存放在栈帧里。
而存在异常处理的函数的栈帧与传统栈帧不同。它在栈帧中存在一个保存异常处理相关信息的结构体EXP,这个结构体是编译器生成的。假如存在如下的函数调用栈funA->funB->funC(a->b表示a调用b),由于EXP是链式存储的,而且异常捕获的原则是调用栈更近的catch块优先,因此funC.EXP.pre->funB.EXP(子函数的EXP指向调用它的函数的EXP)。EXP中有一个指针指向了一个处理异常相关的结构体EHDL,EHDL中保存了两个表:
捕获:当程序抛出异常后,首先在栈捕获表tblTryBlocks中,对其中每一个保存的try块信息,查看抛出异常的位置是否在try块的覆盖范围内。如果在,查看try块对应的catch块表,是否有匹配的catch块;如果不在,查看下一个try块;如果该栈帧的try块或catch块遍历完了还没有找到匹配的catch块,则说明该函数未能捕获异常,异常将交给调用它的函数来解决。因此当前函数后面的内容将不会得到执行,而且局部类变量将被析构,这时需要用到栈展开来析构类变量。
栈展开:在栈展开表tblUnwind中,对其中保存的每个局部 类对象(内置类型无需析构)通过保存的析构函数指针调用析构函数。这也是不能让异常逃离析构函数的原因,发生异常会进行栈展开,栈展开时会调用析构函数,如果这时候再遇到异常,异常处理的结构便会被破坏,程序将会终止。所有局部变量都被成功析构后,异常处理结构体利用指针指向上一个节点,处理上一个函数。
异常处理的基础知识主要参考了C++ primer第5版。
异常处理的实现机制主要参考了这篇文章:C++异常机制的实现方式和开销分析,里面介绍了异常机制的实现细节,写得非常好,看完后对异常将会有更深刻的认知。
1.DOM2.获取元素document.getElementById(id)document.getElementsByTagName('标签名') document.querySelector('.box');3.事件常用事件:onclickonmouseover 鼠标经过onmouseout 鼠标离开onfocus 获取焦点触发onblur 失去焦点onmousemove 移动触发onmouseup 弹起触发onmousedown 按下触发 var btn = do
原标题:华为手机连上WiFi后显示不可上网?其实很简单,用这个解决就好了一直以来都有很多的小伙伴反应,华为手机经常会出现WiFi连接不上,以及WiFi连接之后不可上网的情况,并且是大家都用同一个网,别人都可以,就是自己的手机不行,这是什么原因呢?可能是你没注意这些吧,赶紧来看看是怎么回事吧! 首先我们要确定我们路由器搭建是否有问题,如果路由器有问题的话,那相应的你的手机就不可能会连上网。我们可以使...
问题背景Visual Studio启动winform项目时提示:由于缺少调试目标,Visual Studio 无法开始调试。 解决方法解决方案配置 → 配置管理器 → 勾选【生成】选项 End C#文章导航 菜单加载中......
后台daemon非法窃取用户iTunesstore信息本人郑重声明:并不鼓励窃取用户隐私等行为,一切hack学习都只是为了研究如何防御。OK,进入正题。开机自启动在iOS安全攻防(一):Hack必备的命令与工具中,介绍了如何编译自己的C程序并手动启动。今天介绍如何使程序变为开机自启动。1.首先打开Xcode创建一个plist属性文件,如下图所示:...
python read()函数
一、多进程 进程就是一堆资源的集合,进程中至少包含一个线程。多进程的使用方法和线程类似,来看代码:#!/usr/bin/env python3# -*- coding:utf-8 -*-import multiprocessing,time,threadingdef threading_run(): print(threading.get_id...
这段日子工作不忙,心情也不错,为了娱乐和干点什么,把网上的一篇贴《微软等数据结构+算法面试100题》做了一遍。感觉不错,大部分是比较基础。除了个别语法题由于对该语言不熟无法下手之外,其余的连查带想地全部搞定!边做的同时也做了些记录工作,大都是写出了思路或算法。觉得个别有必要编码验证的也实验了,当然,有一部分是编码验证计划中的,回来有时间慢慢完成。此刻,想起前辈的句话:先了解一些XXX、掌握XXX等知识,一般的题就不在话下了。什么叫一般的题?这些算吗?虽然略有些小成就感,但自知对某些知识掌握还是不透。有时瞪大
版权声明:Davidwang原创文章,严禁用于任何商业途径,授权后方可转载。 在VS工程构建完成后,使用VS 2019打开该工程(可以直接通过双击.sln格式项目文件打开),然后就可以生成MR应用并部署到HoloLens2设备上(确保HoloLens2设备系统已开机并处于非待机休眠状态)。我们可以通过USB方式或者WiFi两种方式将MR应用部署到HoloLens2设备上。(一)、VS环境USB部署方式 确保HoloLens2设备通过USB连接到开发计算机,设置VS生成方式为Debug/Rele
题目地址:https://www.icourse163.org/learn/PKU-1001941004?tid=1450230441#/learn/hw?id=1222245416下面的程序可以下载多个网页文件(download方法已写好),请将它改成多线程进行下载(评分占7分),如果可能, 显示计算全部下载完成程序所用的时间(提示:new Date().getTime()可以得到当前时间的...
历届试题 最大子阵 时间限制:1.0s 内存限制:256.0MB 问题描述 给定一个n*m的矩阵A,求A中的一个非空子矩阵,使这个子矩阵中的元素和最大。 其中,A的子矩阵指在A中行和列均连续的一块。输入格式 输入的第一行包含两个整数n, m,分别表示矩阵A的行数和列数。 接下来n行,每行m个整数,表示矩阵A。
作为Java程序员,我想很多人都知道日志对于一个程序的重要性,尤其是Web应用。很多时候,日志可能是我们了解应用程序如何执行的唯一方式。所以,日志在Java Web应用中至关重要,但是,很多人却以为日志输出只是一件简单的事情,所以会经常忽略和日志相关的问题。在接下来的几篇文章中,我会来介绍介绍这个容易被大家忽视,但同时也容易导致故障的知识点。Java语言之所以强大,就是因为他很成熟的生态体系。...
在Java的实际开发中,经常会遇填写一个文件的相对路径或者是绝对路径的问题,对于初学者来说,经常犯难的是到底是用\ 还是用/的问题,本文将彻底解决这个问题的困扰.先来看要下路径符号在windows系统和再Linux系统写的显示使用的区别: Windows下的路径: Linux下的路径: 对比可以发现:windows使用的是\ linux使用的是/ 然而在java的代码开发中\...