首页 > 编程语言 >C++初学者指南-3.自定义类型(第一部分)-异常

C++初学者指南-3.自定义类型(第一部分)-异常

时间:2024-07-06 20:58:56浏览次数:15  
标签:std 函数 自定义 抛出 noexcept C++ 初学者 catch 异常

C++初学者指南-3.自定义类型(第一部分)-异常

文章目录

简介

什么是异常?

可以在调用层次结构中向上抛出的对象。

  • 抛出将控制权转移回当前函数的调用方
  • 它们可以通过try…catch块捕获/处理
  • 如果不处理,异常会向上传播,直到它们到达 main
  • 如果main中没有处理异常,将会调用std::terminate
  • std::terminate 的默认行为是中止程序
    在这里插入图片描述

第一个示例

异常的最初动机是报告构造函数未能正确初始化对象,即未能建立所需的类不变量(构造函数没有可用于错误报告的返回类型)。

#include <stdexcept>  // standard exception types
class Fraction {
  int numer_;
  int denom_;
public: 
  explicit constexpr
  Fraction (int numerator, int denominator): 
    numer_{numerator}, denom_{denominator}
  {
    if (denom_ == 0) 
      throw std::invalid_argument{"denominator must not be zero"};
  }
  …
};
int main () {
  try {                              
    int d = 1;
    std::cin >> d;
    Fraction f {1,d}; 
    …
  } 
  catch (std::invalid_argument const& e) {
    // deal with / report error here
    std::cerr << "error: " << e.what() << '\n';
  }
  …
}

运行上面代码

用途:报告违反规则的行为

  1. 前提条件违规
  • 前提条件 = 关于输入的期望(有效函数参数)
  • 违规示例: 越界容器索引/平方根为负数
  • 宽契约函数在使用其输入值之前执行前置条件检查
    在性能关键的代码中,如果传入的参数已经知道是有效的,那么人们不想支付输入有效性检查的成本,因此通常不会使用这些方法。
  1. 未能建立/保持不变量
  • 公共成员函数无法设置有效的成员值
  • 内存不足,向量vector增长失败
  1. 后置条件违规
  • 后置条件 = 关于输出的期望值(返回值)
  • 违规=函数未能产生有效的返回值或损坏全局状态
  • 例子:
    • 构造函数失败
    • 无法返回除以零的结果

异常的优点和缺点
优点1:将错误处理代码与业务逻辑分离
优点2:错误处理集中化(在调用链的更高层)
优点3:现在,当没有抛出异常时,性能影响可以忽略不计
缺点1:但是,抛出异常 时通常会影响性能
缺点2:由于额外的有效性检查而影响性能
缺点3:容易产生资源/内存泄漏(更多见下文)

异常的替代方案

输入值无效(违反前提条件)

  • 窄契约函数:在传递参数之前确保参数有效
  • 使用可以排除无效值的参数类型
  • 如今这是首选以获得更好的性能

未能建立/保留不变量

  • 错误状态/标志
  • 将对象设置为特殊的无效值/状态

无法返回有效值(违反后置条件)

  • 通过单独的输出参数(引用或指针)返回错误代码
  • 返回特殊的无效值
  • 使用特殊的词汇类型,可以包含有效结果,也可以什么都不包含,就像C++17的std::optional或Haskell的Maybe

标准库异常

异常是 C++ 标准库使用继承的少数地方之一:
所有标准异常类型都是std::exception的子类型。

std::exception
  ↑ logic_error
  |  ↑ invalid_argument
  |  ↑ domain_error
  |  ↑ length_error
  |  ↑ out_of_range
  |  …
  ↑ runtime_error
    ↑ range_error
    ↑ overflow_error
    ↑ underflow_error
    …
try {
  throw std::domain_error{
    "Error Text"};
}
catch (std::invalid_argument const& e) {
  // 仅仅处理 'invalid_argument'异常
  …
}
// 捕捉其它所有异常
catch (std::exception const& e) {
  std::cout << e.what()
  // prints "Error Text"
}

一些标准库容器提供了宽契约函数,通过抛出异常来报告无效的输入值:

std::vector<int> v {0,1,2,3,4};
// 窄契约:不检查以获取最大性能
int a = v[6];     //  未定义行为
// 宽契约:检查是否超范围
int b = v.at(6);  // throws std::out_of_range

处理

重新抛出异常

try {   
   // potentially throwing code
} 
catch (std::exception const&) {  
  throw;  // re-throw exception(s)
}

捕获所有异常

try {                              
  // potentially throwing code
} 
catch (...) {  
  // handle failure
}

集中异常处理!

  • 如果同样的异常类型在许多不同的地方被抛出,可以避免代码重复。
  • 对于将异常转换为错误代码很有用
void handle_init_errors () {
  try { throw;  // re-throw! } 
  catch (err::device_unreachable const& e) { … } 
  catch (err::bad_connection const& e) { … } 
  catch (err::bad_protocol const& e) { … }
}
void initialize_server (…) {
  try {
    …
  } catch (...) { handle_init_errors(); }
}
void initialize_clients (…) {
  try {
    …
  } catch (...) { handle_init_errors(); }
}

问题和保证

资源泄露

几乎任何一段代码都可能抛出异常导致对 C++ 类型和库的设计产生重大影响。
如果与以下内容一起使用,则可能是资源/内存泄漏的潜在来源

  • 进行自己的内存管理的外部 C 库
  • (设计不佳)不使用 RAII 进行自动资源管理的 C++ 库
  • (设计不佳)在销毁时不清理资源的类型

示例:由于 C 风格的资源处理而导致的泄漏
即,两个单独的函数用于资源初始化(连接)和资源终止(断开连接)。

void add_to_database (database const& db, std::string_view filename) {
  DBHandle h = open_dabase_conncection(db);  
  auto f = open_file(filename);
  // 如果 "open_file"抛出异常,则链接不会断开!
  // do work…
  close_database_connection(h);
  // ↑ 如果"open_file"抛出了异常不会执行上面代码
}

使用 RAII 避免内存泄漏!

RAII 又是什么?

  • 构造函数:资源获取
  • 析构函数:资源释放/终结

如果抛出异常:

  • 局部作用域中的对象被销毁:被调用的析构函数
  • 使用 RAII:正确释放资源
class DBConnector {
  DBHandle handle_;
public:
  explicit
  DBConnector (Database& db): 
    handle_{make_database_connection(db)} {}
  ~DBConnector () { close_database_connection(handle_); }
  // 使connector不能复制:
  DBConnector (DBConnector const&) = delete;
  DBConnector& operator = (DBConnector const&) = delete;
};
void add_to_database (database const& db, std::string_view filename) {
  DBConnector(db);
  auto f = open_file(filename);
  // 如果 'open_file' 抛出异常 ⇒ 连接关闭!
  // do work normally…
} // 连接关闭了!

如果你需要使用一个(比如来自C语言的)库,这个库采用独立的初始化和资源释放函数,那么就编写一个RAII包装器。
通常,如果无法控制引用的外部资源,将包装器设为不可复制(删除复制构造函数和复制赋值运算符)也是有意义的。

析构函数:不要让异常逃脱!

… 否则资源可能会泄露!

class E {
public:
  ~E () { 
    // throwing code ⇒ BAD!
  }  
  …
};
class A {
  // some members:
  G g;  F f;  E e;  D d;  C c;  B b;
  …
};

在这里插入图片描述
如果对象e析构时抛出异常的话会导致 f 和 g 对象的析构函数没有被调用。

在析构函数中: 捕获可能引发异常的代码!

class MyType {
public:
  ~MyType () { …
    try {
      // y throwing code…
    } catch ( /* … */ ) {
      // handle exceptions…
    } …
  }
};

异常保证

如果引发异常:
不能保证
任何 C++ 代码都应该默认做出这个假设,除非它的文档另有说明:

  • 操作可能会失败
  • 资源可能泄露
  • 可能会破坏不变性(= 成员可能包含无效值)
  • 部分执行失败的操作可能会导致副作用(例如输出)
  • 异常可能会向外传播

基本保正

  • 不变量被保留,没有资源泄漏
  • 所有成员都将包含有效值
  • 执行失败操作的部分可能会导致一些副作用(例如,值可能已写入文件)

这是你最起码的目标!

强保证(提交或回滚语义)

  • 操作可能会失败,但不会产生明显的副作用
  • 所有成员都保留其原值

内存分配容器应该提供这一保证,即,如果在增长过程中内存分配失败,容器应保持有效和不变。

无抛出异常保证(最强)

  • 保证操作成功
  • 外部看不到任何异常(要么没有抛出异常,要么在内部被捕获了)
  • 使用 noexcept 关键字进行记录和强制执行

在高性能代码和资源受限的设备上,首选此功能。

无抛出异常保证:noexcept (C++11)

void foo () noexcept { … }
  • ‘foo’ 承诺永远不会抛出异常或让任何异常逃逸
  • 如果一个异常从一个 noexcept 函数中逃逸了,程序会被终止

好好想想,你能不能遵守不抛出异常的承诺!

  • noexcept是函数的接口的一部分(甚至是自C++17函数类型的一部分)
  • 稍后将不抛出异常的函数更改为抛出异常的函数可能会破坏那些依赖不必处理异常的调用代码

有条件noexcept

A noexcept( expression )如果表示式为真则声明A不抛出异常
A noexcept( noexcept( B ) )如果B为不抛出异常则声明A也不抛出异常

默认情况下为 noexcept(true)

都是隐式声明的特殊成员

  • 默认构造函数
  • 析构函数
  • 复制构造函数, 移动构造函数
  • 复制赋值运算符、移动赋值运算符
  • 继承的构造函数
  • 用户定义的析构函数

以上这些都是默认noexcept(true)

除非

  • 他们需要调用 noexcept(false) 的函数
  • 明确的声明另有说明

附上原文地址
如果文章对您有用,请随手点个赞,谢谢!^_^

标签:std,函数,自定义,抛出,noexcept,C++,初学者,catch,异常
From: https://blog.csdn.net/silencestarsky/article/details/140161575

相关文章

  • C++(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例
    C++(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例目录C++(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例1、概述2、实现效果3、主要代码4、源码地址更多精彩内容......
  • c++ u7-02-高精度乘法
    本节课作业:链接:https://pan.baidu.com/s/13-FC86jSHGziRDA8lqzimg?pwd=owv1提取码:owv1   高精度乘法             #include<iostream>#include<cstdio>#include<cstring>usingnamespacestd;stringx,y;inta......
  • C++算法实践04-寻找两个正序数组的中位数
    一、题目:给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为 O(log(m+n)) 。示例1:输入:nums1=[1,3],nums2=[2]输出:2.00000解释:合并数组=[1,2,3],中位数2示例2:输入:nu......
  • Windows防火墙 日志 自定义 以记录被丢弃的数据包和成功的连接日志。以下是一个示例.r
     配置注册表,以记录被丢弃的数据包和成功的连接日志 WindowsRegistryEditorVersion5.00;WindowsDefender防火墙日志记录设置[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy];以下是针对不同配置文件的设置,例如......
  • 使用c++实现图形化文件浏览
       代码中使用了SDL2库,需要先安装并正确配置相关的开发环境。还需要添加字体加载和处理的代码,为图方便,省略。#include<iostream>#include<SDL2/SDL.h>#include<SDL2/SDL_image.h>#include<vector>#include<string>#include<filesystem>constintSCREEN......
  • C++题解(3) 信息学奥赛一本通: 1013:温度表达转化 洛谷:B2013 温度表达转化 土豆编程:M
    【题目描述】利用公式 C=5×(F−32)÷9C=5×(F−32)÷9(其中CC表示摄氏温度,FF表示华氏温度)进行计算转化,输入华氏温度FF,输出摄氏温度CC,要求精确到小数点后55位。【输入】输入一行,包含一个实数FF,表示华氏温度。(F≥−459.67)(F≥−459.67)【输出】输出一行,包含一个......
  • day01 初学c++第一章
    目录一、前置代码以及cout打印两条预处理代码:count打印语句:二、符号常量 三、标识符的命名规范四、数据类型 c++中整数类型的表现形式:在c++中,数字存在有符号和无符号之分的c++中实型的表现形式:代码所用函数:c++中字符型的表现形式:基础运算总结:转义字符c++中字......
  • Spring Boot 自定义 starter 启动器
    前言:SpringBoot为我们提供了自动配置的功能,我们可以像使用插件一样,对各个组件自由组合装配,只需引入定义好的starter即可,有点类似于Java的SPI机制,SPI机制是为了解决项目与项目之间的解耦,而SpringBootstarter方式实现了模块化的解耦,前文我们从SpringBooot源......
  • C++开发调试工具:GDB调试,windebug调试,adb调试
    我们在C++开发过程中时常避免不了要调试追踪,一下介绍最主流的三种调试工具:一.GDB调试1.coredump文件:coredump文件是程序异常时系统产生的错误日志文件,即核心转储文件;编译一个debug程序,必须是debug版本,否则无法产生coredump文件;编译命令:g++test.cpp-omytest-g,必须要......
  • C++中的设计模式
    要搞清楚设计模式,首先得要了解UML中的类的一些关系模型。一.UML图中与类的层次关系UML关系:继承关系(泛化关系);组合关系;聚合关系;关联关系;依赖关系;以上关系强度依次减弱。1.继承关系继承关系是最直接的父子关系,如麻雀和老鹰都继承自鸟类,属于子类继承自父类,所以UML中子类实......