C++中异常的详细解释

 2024-01-22 03:01:47  阅读 0

1.什么是异常处理

底线:异常处理是关于处理程序中的错误。

2、为什么需要异常处理以及异常处理的基本思想

C++之父在《The C++》中说:一个库的作者可以检测到发生了运行时错误,但一般不知道如何处理它们(因为这与用户的具体应用有关); 另一方面,库的用户知道如何处理这些错误,但无法检测到它们何时发生(如果可以检测到,可以在用户的​​代码中进行处理,而不是留给库来发现)。

表示:提供例外的基本目的就是为了处理上述问题。 基本思想是让函数在发现无法处理的错误时抛出异常,然后其(直接或间接)调用者可以处理问题。

这个想法是,a 找到 a 它可以处理 an,它的(或)可以是 。

这就是《C++》里说的:问题检测和问题处理分开。

让我们从

一个思想:在所有支持异常处理的编程语言(比如Java)中,都应该认识一个思想:在异常处理过程中,问题检测代码可以向问题处理代码抛出一个对象,通过类型这个对象和内容的交互,实际上完成了两部分之间的沟通。 通信的内容是“发生了什么错误”。 当然,各种语言对异常的实现都或多或少不同,但是通信的思想是不变的。

3. 异常发生前如何处理错误

在C语言的世界中,错误处理总是围绕两种方法:一是使用整数返回值来标识错误;二是使用整数返回值来识别错误。 另一种是使用errno宏(可以简单理解为全局整型变量)来记录错误。 。 当然,这两种方法在C++中仍然可以使用。

这两种方法最大的缺点是不一致问题。 例如,有些函数返回1表示成功,返回0表示错误; 而有些函数返回 0 表示成功,返回非 0 表示错误。

另一个缺点是该函数只有一个返回值。 如果通过函数的返回值来表示错误代码,则该函数不能返回其他值。 当然,你也可以通过指针或C++引用返回另一个值,但这可能会让你的程序稍微晦涩难懂。

4.为什么例外是好的?

使用异常处理的优点如下:

1、函数的返回值可以忽略,但异常不能忽略。 如果程序中出现异常但没有被捕获,程序就会终止,这会在一定程度上鼓励程序员开发更健壮的程序。 而如果在C语言中使用错误宏或函数返回值,调用者可能会忘记检查而无法处理错误。 结果,程序会莫名终止或者出现错误结果。

2. 整数返回值没有任何语义信息。 异常包含语义信息,有时可以从类名中反映出来。

3. 整数返回值缺少相关上下文信息。 作为一个类,异常可以有自己的成员,并且这些成员可以传达足够的信息。

4. 调用过程中可以跳过异常处理。 这是一个编码问题:假设在具有多个函数的调用堆栈中发生错误。 使用整数返回代码需要您在函数的每个级别处理它。 使用异常处理的栈扩展机制只需要在一个地方处理,不需要在每一层函数都处理。

5、C++中使用异常需要注意的问题

任何事物都有两面性,总有优点和缺点。 如果您是一名 C++ 程序员并且希望在代码中使用异常,那么以下问题是您应该注意的。

1.性能问题。 这一般不会成为瓶颈,但如果你正在编写高性能或实时性要求很强的软件,则需要考虑它。

(如果你像我一样,曾经是一个Java程序员,那么下面的事情可能会让你迷惑一阵子,但是没办法,谁叫你现在去学C++了。)

2、指针和动态分配引起的内存回收问题:在C++中,动态分配的内存不会自动回收。 如果遇到异常,需要考虑内存是否被正确回收。 在Java中,基本上不需要考虑这个。 有垃圾回收机制真是太好了!

3、函数的异常抛出列表:在Java中,如果一个函数没有在异常抛出列表中显式指定要抛出的异常,则不允许抛出; 但在C++中,如果你没有在函数的异常抛出列表中指定要抛出的异常,则out列表指定要抛出的异常,意味着你可以抛出任何异常。

4. 在C++中,编译时不检查函数的异常抛出列表。 这意味着,当你编写C++程序时,如果在异常抛出列表中未声明的函数中抛出异常,则编译时不会报错。 java中,提示功能真是强大!

5、Java中,抛出的异常必须是异常类; 但在 C++ 中,你可以抛出任何类型,甚至可以抛出整数。 (当然,在C++中,如果在catch中接收时使用对象而不是引用,那么抛出的对象必须是可复制的。这是语言的要求,而不是异常处理的要求)。

6. C++中没有关键字。 java和java都有关键字。

6. 异常的基本语法 1. 异常的抛出和捕获

这很简单。 要引发异常,请使用 throw,要捕获异常,请使用 try...catch。

捕获异常时需要注意的事项:

1. catch 子句中的异常说明符必须是完整类型,并且不能前向声明,因为在异常处理中经常需要访问异常类的成员。 异常:仅当您的 catch 子句使用指针或引用来接收参数,并且您不在 catch 子句中访问异常类的成员时,您的 catch 子句的异常说明符才可以是前向声明类型。

2.catch的匹配过程是寻找第一个匹配,而不是最佳匹配。

3、渔获的匹配过程中,对种类的要求比较严格。 不允许标准算术转换和到类类型的转换。 (类类型转换有两种:通过构造函数的隐式类型转换和通过转换运算符的类型转换)。

4、与函数参数相同的地方有:

①如果catch中使用基类对象接收子类对象,会导致子类对象被切片到父类对象中(通过调用父类的拷贝构造函数);

②如果catch中使用基类对象的引用来接受子类对象,那么在访问虚成员时,就会发生动态绑定,即发生多态调用。

③ 如果catch中使用了指向基类对象的指针,则必须保证throw语句也抛出该指针类型,并且当catch语句执行时,该指针所指向的对象仍然存在(通常是一个动态分配的对象)对象指针)。

5、与函数参数的区别是:

① 如果在 throw 中抛出一个对象,无论 catch 中使用什么接收者(基类对象、引用、指针或子类对象、引用、指针),编译器都会在将其传递给 catch 之前构造该对象的另一个副本。 也就是说,如果你在 throw 语句中抛出一个对象类型,并在 catch 点通过一个对象接收它,那么该对象就被复制了两次,即复制构造函数被调用了两次。 一次抛出时,将“抛出到对象”复制到一个“临时对象”(这一步是必须的),然后因为catch使用对象接收,所以需要从“临时对象”复制到“catch”中参数变量”; 如果在catch中使用“引用”来接收参数,那么就不需要进行第二次复制,即形参的引用指向临时变量。

② 对象的类型与 throw 语句中反映的静态类型相同。 也就是说,如果在 throw 语句中抛出指向子类对象的父类引用,那么就会发生分裂,即只抛出子类对象的父类部分,而子类对象的类型抛出的对象也是父类。 类类型。 (从实现的角度来看,这是因为当复制到“临时对象”时,使用了 throw 语句中类型(这里是父类的)的复制构造函数)。

③ 不允许标准算术转换和类自定义转换:在函数参数匹配过程中,可以进行很多类型转换。 但在异常匹配的过程中,转换规则必须严格。

④异常处理机制的匹配过程是寻找第一个匹配(first fit),而函数调用过程是寻找最佳匹配(best fit)。

2.异常类型

如上所述,在 C++ 中,您可以抛出任何类型的异常。 (嘿,它可以抛出任何类型。当我第一次看到这个时,我很长一段时间都没有意识到,因为这在Java中是不可能的)。

注意:如上所述,在C++中,如果你在 throw 语句中抛出一个对象,那么你抛出的对象必须是可复制的。 因为需要复制传输,这是语言的要求,而不是异常处理的要求。 (上面“与函数参数的区别”中也提到过,因为需要先复制到临时变量中)

3. 堆栈扩展

栈扩展是指抛出异常后匹配catch的过程。

当抛出异常时,当前函数的执行将被暂停,并开始搜索匹配的catch子句。 搜索函数的嵌套调用链,直到找到匹配的 catch 子句,或者找不到匹配的 catch 子句。

防范措施:

1. 在堆栈展开期间,本地对象被销毁。

① 如果本地对象是类对象,则通过调用其析构函数将其销毁。

②但是对于通过动态分配获得的对象,编译器不会自动删除它们,所以我们必须手动显式删除它们。 (这个问题非常常见和重要,因此使用了一种称为 RAII 的方法。详细信息请参见下文)

2. 析构函数不应该抛出异常。 如果析构函数需要执行可能抛出异常的代码,那么应该在析构函数内部处理异常,而不是抛出异常。

原因:当针对异常展开堆栈时,如果析构函数抛出另一个自己的未处理异常,则会导致标准库函数被调用。 默认函数会调用abort函数强制整个程序异常退出。

3.构造函数中可以抛出异常。 但请注意:如果构造函数因异常而退出,则该类的析构函数将不会被执行。 所以需要手动销毁抛出异常之前已经构造好的部分。

4.异常重新抛出

语法:使用空的 throw 语句。 则写为:扔;

笔记:

①投掷; 语句只能出现在catch 子句中或catch 子句调用的函数中。

② 重新抛出的是原来的异常对象,也就是上面说的“临时变量”,而不是catch参数。

③ 如果想在重新抛出之前修改异常对象,应该在catch中使用引用参数。 如果使用对象接收,那么修改异常对象后,无法通过“重新抛出”来传播修改后的异常对象,因为重新抛出不是catch参数,应该使用throw e; 这里的“e”是在catch语句中接收到的参数。

5.捕获所有异常(匹配任何异常)

语法:在 catch 语句中,使用三个点 (...)。 即写为:catch(...) 这里的三个点是“通配符”,类似于变长形式参数。

常见用法:与“”表达式一起使用,完成catch中的部分工作,然后重新抛出异常。

6. 未捕获的异常

这意味着如果程序中抛出异常,则必须捕获该异常。 否则,如果程序执行过程中抛出异常,而没有找到对应的catch语句,就会和“析构函数在堆栈扩展时抛出异常”一样,会调用该函数,默认函数会调用abort强制整个程序异常退出的函数。

7. 构造函数功能测试块

必须使用 try 块捕获构造函数初始化列表中引发的异常。 语法类型的形式如下:

MyClass::MyClass(int i) 
try :member(i) { 
    //函数体 
} catch(异常参数) { 
    //异常处理代码 
} 

注意:对于函数测试块中捕获的异常,可以在catch语句中执行内存释放操作,然后异常仍会再次抛给用户代码。

8.异常抛出列表(异常描述)

也就是说,在函数的形参列表之后(如果是 const 成员函数,则在 const 之后),使用关键字 throw 来声明一个带括号的、可能为空的异常类型列表。 形式为: throw() 或 throw(, )。

含义:表示该函数只能抛出列表中的异常类型。 例如: throw() 表示不抛出任何异常。 而 throw(, ) 表示只能抛出一到两个异常。

注意:(学过Java的同学要特别注意,和Java中的不一样)

① 如果函数没有显式声明抛出列表,则意味着该异常可以抛出任何列表。 (在java中,如果没有异常抛出列表,那么就不能抛出异常)。

② C++的“throw()”相当于Java的未声明的抛出列表。 all 表示不会抛出异常。

③ 在C++中,编译时,编译器不会检查异常抛出列表。 换句话说,如果你声明了一个抛出列表,那么即使你的函数代码抛出了抛出列表中没有指定的异常,你的程序仍然可以被编译,并且在运行时会出现错误。 对于这样的异常,在C++中称为“意外异常”()。 (这一点和Java不同,在Java中需要严格检查)。

处理意外异常:

如果程序中出现意外的异常,程序就会调用()。 该函数的默认实现是调用该函数,默认情况下该函数最终会终止程序。

虚函数重载方法时异常抛出列表的限制:

在子类中重载时,函数的异常描述必须与父类中的异常描述一样严格或更严格。 也就是说,子类中对应函数的异常描述中不能添加新的异常。 或者换句话说:父类中的异常抛出列表是子类重载版本的虚函数可以抛出的异常列表的超集。

函数指针中异常抛出列表的限制:

异常抛出列表是函数类型的一部分,异常抛出列表也可以在函数指针中指定。 不过,在初始化或者赋值函数指针时,除了检查返回值和形参之外,还必须注意异常抛出列表的限制:源指针的异常描述至少要和的目标指针。 真是一口。 也就是说,声明函数指针时指定的异常抛出列表必须是实际函数的异常抛出列表的超集。 如果定义函数指针而不提供异常抛出列表,则可以指向可以抛出任何类型异常的函数。

抛出一个列表有用吗:

Scott 在《More C++》第 14 条中指出“谨慎使用异常规范”(Use)。 “异常描述”就是我们所有的“异常抛出列表”。 谨慎的根本原因是C++编译器不会检查异常抛出列表,因此抛出列表中未指定的异常可能会在函数代码中或被调用的函数中抛出,导致程序调用函数,导致程序提前终止。 同时,他提出了三点需要考虑的问题:

① 不要在模板中使用异常抛出列表。 (原因很简单,你甚至不知道实例化模板所用的类型,因此无法确定函数是否应该抛出异常以及抛出什么异常)。

② 如果函数 B 在函数 A 内调用,且函数 B 没有声明异常抛出列表,则函数 A 本身不应设置异常抛出列表。 (原因是函数B可能会抛出未在函数A的异常抛出列表中声明的异常,从而导致unex函数被调用);

③通过函数指定新的函数,捕获函数中的异常,并抛出统一类型的异常。

另外,《C++》第4篇中指出,虽然异常描述的应用范围有限,但如果可以确定函数不会抛出异常,那么显式声明它不抛出任何异常是有好处的。 通过语句:“throw()”。 这样做的好处是,对于程序员来说,调用这样的函数时无需担心出现异常。 编译器可能会执行因抛出异常的可能性而受到抑制的优化。

7.标准库中的异常类

与Java一样,标准库也提供了许多异常类,这些异常类是通过类继承来组织的。 标准例外分为八种。

异常类继承层次结构如下:

每个类所在的头文件在图下方标识。

标准异常类的成员:

① 在上面的继承系统中,每个类都提供了构造函数、复制构造函数和赋值运算符重载。

② 类及其子类、类及其子类、它们的构造函数接受一种类型的形参,用于异常信息的描述;

③ 所有异常类都有一个what()方法,该方法返回一个const char*类型(C风格字符串)的值来描述异常信息。

标准异常类的具体描述:

异常名称

描述

所有标准异常类的父类

new和new[]时,请求分配内存失败

这是一个特殊的例外。 如果在函数的异常抛出列表中声明了异常,并且在函数内部抛出了不在抛出列表中的异常,则这就是如果在调用函数中抛出了异常,无论是什么类型,都会被抛出。替换为类型

使用运算符操作NULL指针,并且该指针是带有虚函数的类,则抛出异常。

当使用转换引用失败时

::

io操作时发生错误

逻辑错误,运行前可以检测到的错误

运行时错误,只能在运行时检测到的错误

子类:

返回 ap 保存的指针

ap.重置(p)

如果p和ap的值不同,则删除ap指向的对象,并将ap绑定到p

ap.()

返回 ap 持有的指针并使 ap 解除绑定

ap.get()

返回 ap 保存的指针

类的用法:

1. 用于保存指向对象类型的指针。 注意,它必须是指向动态分配对象的指针(即使用new但未分配)。 既不能是动态分配的数组(使用 new[])指针,也不能是非动态分配的对象指针。

2. 惯用的初始化方法:在用户代码中,使用new表达式作为构造函数的参数。 (注意:类的构造函数接受指针参数,因此必须显式初始化)。

3.行为特征:与普通指针行为类似。 它存在的主要原因是为了防止动态分配的对象指针导致的内存泄漏。 由于它是一个指针,因此它有“*”运算符和“->”运算符。 所以主要目的是:第一,保证引用的对象被自动删除,并支持正常的指针行为。

4、对象的复制和赋值是破坏性的。 ① 会导致右操作数变为未绑定状态,导致对象无法放入容器中; ② 赋值时,将一个运算符改为非绑定就意味着修改了右边的操作数,所以这里的赋值操作必须保证。 运算符的右操作数是可以修改的左值(但是,在普通赋值运算符中,右操作数不一定是左值); ③ 和普通赋值运算符一样,如果是自赋值,则没有任何作用; ④ 导致物体无法放入容器中。

5、如果初始化时使用了默认构造函数,变成了未绑定的对象,可以通过reset操作绑定到一个对象上。

6、如果想测试是否已经绑定到某个对象,可以使用get()函数的返回值与NULL进行比较。

缺陷:

1. 不能使用对象保存指向静态分配对象的指针,也不能保存指向动态分配数组的指针。

2、不能说两个对象指向同一个对象。 因为一个对象被销毁后,另一个对象指向被释放的内存。 造成这种情况的两个常见原因是: ① 使用同一个指针来初始化或重置两个不同的对象; ② 使用一个对象的get函数的返回值来初始化或重置另一个对象。

3. 容器内不能放置物品。 因为它的复制和赋值操作都是破坏性的。

11.常见异常处理问题

动态内存分配错误

① new和new[]运算符用于分配动态内存。 如果它们分配内存失败,则会在新的头文件中抛出异常,因此应该在我们的代码中捕获这些异常。 常见的代码形式如下:

try{//其他代码 ptr =[];//其他代码 }catch( &e) {//这里常见的处理方式是:先释放分配的内存,然后结束程序,或者打印错误信息继续执行}

② 可以用类似C语言的方式处理,只不过此时要使用的版本采用“new()”的形式来分配内存。 此时,如果分配不成功,将返回NULL指针,而不是抛出异常。

③ 可以自定义内存分配失败行为。 C++ 允许指定新的 () 回调函数。 默认情况下没有新的处理程序。 如果我们设置了一个新的处理程序,那么当new和new[]分配内存失败时,我们设置的新处理程序将被调用,而不是直接抛出异常。 通过函数设置回调函数。 被回调的函数要求没有返回值,也没有形式参数。

12. C++之父的建议

摘自《The C++》——C++之父

1.不要在当地人较多的地方使用; 当可以处理本地控制时不要使用异常;

2.使用“is”来; 采用“资源分配即初始化”技术来管理资源;

3.使用try-。 使用代码的“is”; 尽量少使用try-catch语句块,而是使用“资源分配即初始化”技术。

4.在a中添加to; 如果构造函数中发生错误,请通过抛出异常来指示。

5.避免; 避免在析构函数中抛出异常。

6.保存代码和错误代码; 将正常程序代码和异常处理代码分开。

7. 发生泄漏的情况; 注意,异常发生时,通过new分配的内存可能会导致内存泄漏。

8.凡是可以通过意志实现的; 如果一个函数可能会抛出某种异常,那么当我们调用它时,我们必须假设它肯定会抛出异常,即必须对其进行处理。

9.难道每一个都来自; 请记住,并非所有异常都继承自类。

10.阿塔。 ,抛出一个并让一个; 写给别人调用的程序库不应该结束程序,而应该让调用者通过抛出异常来决定如何处理(因为调用者必须处理抛出的异常异常)。

11. 错误——早期的; 如果开发一个项目,那么“错误处理策略”必须在设计阶段确定。

标签: 异常 抛出 函数

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码