异常规格

本文探讨了C++中异常规格的使用及其潜在问题,包括编译器检测限制、模板中的问题、与旧代码集成的挑战及如何避免unexpected异常。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 
异常规格是一个引人注目的特性。它使得代码更容易理解,因为它明确地描述了一个函数可以抛出什么样的异常。但是它不只是一个有趣的注释。编译器在编译时有时能够检测到异常规格的不一致。而且若一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。异常规格既可以做为一个指导性文档同时也是异常使用的强制约束机制。

  函数unexpected缺省的行为是调用函数terminate,而terminate缺省的行为是调用函数abort,所以一个违反异常规格的程序其缺省的行为就是halt(停止运行)。在激活的stack frame中的局部变量没有被释放,因为abort在关闭程序时不进行这样的清除操作。对异常规格的触犯变成了一场并不应该发生的灾难。

  但是我们很容易就能够编写出导致发生这种灾难的函数。编译器仅仅部分地检测异常的使用是否与异常规格保持一致。一个函数调用了另一个函数,并且后者可能抛出一个违反前者异常规格的异常,(A函数调用B函数,因为B函数可能抛出一个不在A函数异常规格之内的异常,所以这个函数调用就违反了A函数的异常规格)编译器不对此种情况进行检测,并且语言标准也禁止它们拒绝这种调用方式(尽管可以显示警告信息)。

  例如函数f1没有声明异常规格,这样的函数就可以抛出任意种类的异常:
  extern void f1(); // 可以抛出任意的异常
  假设有一个函数f2通过它的异常规格来声明其只能抛出int类型的异常:
  void f2() throw(int);
  f2调用f1是非常合法的,即使f1可能抛出一个违反f2异常规格的异常:
  void f2() throw(int)
  {
   ...
   f1(); // 即使f1可能抛出不是int类型的异常,这也是合法的。
   ...
  }

  当带有异常规格的新代码与没有异常规格的老代码整合在一起工作时,这种灵活性就显得很重要。  因为编译器允许调用一个函数其抛出的异常与发出调用的函数的异常规格不一致,并且这样的调用可能导致程序执行被终止,所以在编写软件时采取措施把这种不一致减小到最少。一种好方法是避免在带有类型参数的模板内使用异常规格。例如下面这种模板,它好像不能抛出任何异常:

  // a poorly designed template wrt exception specifications
  template<class T>
  bool operator==(const T& lhs, const T& rhs) throw()
  {
   return &lhs == &rhs;
  }

  这个模板为所有类型定义了一个操作符函数operator==。对于任意一对类型相同的对象,如果对象有一样的地址,该函数返回true,否则返回false。这个模板包含的异常规格表示模板生成的函数不能抛出异常。但是事实可能不会这样,因为opertor&能被一些类型对象重载。如果被重载,当调用从operator==函数内部调用opertor&时,opertor&可能会抛出一个异常,这样就违反了我们的异常规格,使得程序控制跳转到unexpected。

  上例是一种更一般问题的特例,这个问题也就是没有办法知道某种模板类型参数抛出什么样的异常。我们几乎不可能为一个模板提供一个有意义的异常规格。,因为模板总是采用不同的方法使用类型参数。解决方法只能是模板和异常规格不要混合使用。

  能够避免调用unexpected函数的第二个方法是如果在一个函数内调用其它没有异常规格的函数时应该去除这个函数的异常规格。这很容易理解,但是实际中容易被忽略。比如允许用户注册一个回调函数:
  // 一个window系统回调函数指针,当一个window系统事件发生时
  typedef void (*CallBackPtr)(int eventXLocation,int eventYLocation,void *dataToPassBack);
  //window系统类,含有回调函数指针,该回调函数能被window系统客户注册  class CallBack {
  public:
   CallBack(CallBackPtr fPtr, void *dataToPassBack): func(fPtr), data(dataToPassBack) {}
   void makeCallBack(int eventXLocation,int eventYLocation) const throw();
  private:
   CallBackPtr func; // function to call when callback is made
   void *data;      // data to pass to callback
  };                   // 为了实现回调函数,我们调用注册函数,事件的作标与注册数据做为函数参数。
  void CallBack::makeCallBack(int eventXLocation, int eventYLocation) const throw()
  {
   func(eventXLocation, eventYLocation, data);
  }
  这里在makeCallBack内调用func,要冒违反异常规格的风险,因为无法知道func会抛出什么类型的异常。  通过在程序在CallBackPtr typedef中采用更严格的异常规格来解决问题:
 
 typedef void (*CallBackPtr)(int eventXLocation,
                int eventYLocation,
                  void *dataToPassBack) throw();  这样定义typedef后,如果注册一个可能会抛出异常的callback函数将是非法的:  // 一个没有异常规格的回调函数
  void callBackFcn1(int eventXLocation, int eventYLocation,void *dataToPassBack);
  void *callBackData;
  ...
  CallBack c1(callBackFcn1, callBackData);
  //错误!callBackFcn1可能抛出异常

  //带有异常规格的回调函数  void callBackFcn2( int eventXLocation,
                     int eventYLocation,
                     void *dataToPassBack) throw();
  CallBack c2(callBackFcn2, callBackData);
  // 正确,callBackFcn2 没有异常规格
  传递函数指针时进行这种异常规格的检查,是语言的较新的特性,所以有可能有的编译器不支持这个特性。如果它们不支持,那就依靠自己来确保不能犯这种错误。
  避免调用unexpected的第三个方法是处理系统本身抛出的异常。这些异常中最常见的是bad_alloc,当内存分配失败时它被operator new 和operator new[]抛出。如果在函数里使用new操作符,必须为函数可能遇到bad_alloc异常作好准备。
  常说预防胜于治疗(即做任何事都要未雨绸缪),但是有时却是预防困难而治疗容易。也就是说有时直接处理unexpected异常比防止它们被抛出要简单。例如写一个软件,精确地使用了异常规格,但是必须从没有使用异常规格的程序库中调用函数,要防止抛出unexpected异常是不现实的,因为这需要改变程序库中的代码。

 
 虽然防止抛出unexpected异常是不现实的,但是C++允许用其它不同的异常类型替换unexpected异常,能够利用这个特性。例如希望所有的unexpected异常都被替换为UnexpectedException对象。就能这样编写代码:
  class UnexpectedException {}; // 所有的unexpected异常对象被替换为这种类型对象
  void convertUnexpected() // 如果一个unexpected异常被抛出,这个函数被调用
  {                      
   throw UnexpectedException();
  }
  通过用convertUnexpected函数替换缺省的unexpected函数,来使上述代码开始运行。  set_unexpected(convertUnexpected);
  当这么做了以后,一个unexpected异常将触发调用convertUnexpected函数。Unexpected异常被一种UnexpectedException新异常类型替换。如果被违反的异常规格包含UnexpectedException异常,那么异常传递将继续下去,好像异常规格总是得到满足。(如果异常规格没有包含Unexpected-Exception,terminate将被调用,就好像没有替换unexpected一样)
  另一种把unexpected异常转变成知名类型的方法是替换unexpected函数,让其重新抛出当前异常,这样异常将被替换为bad_exception。可以这样编写:
  void convertUnexpected() // 如果一个unexpected异常被抛出,这个函数被调用
  {            
   throw;         // 它只是重新抛出当前异常
  }                      
  set_unexpected(convertUnexpected);
  // 安装 convertUnexpected做为unexpected的替代品异常

  如果这么做,应该在所有的异常规格里包含bad_exception(或它的基类,标准类exception)。不必再担心如果遇到unexpected异常会导致程序运行终止。任何不听话的异常都将被替换为bad_exception,这个异常代替原来的异常继续传递。

  现在应该理解异常规格能导致大量的麻烦。编译器仅仅能部分地检测它们的使用是否一致,在模板中使用它们会有问题,一不注意它们就很容易被违反,并且在缺省的情况下它们被违反时会导致程序终止运行。异常规格还有一个缺点就是它们能导致unexpected被触发即使一个high-level调用者准备处理被抛出的异常,比如下面这个例子:

  class Session { // for modeling online sessions
  public:    
   ~Session();
   ...
  private:
   static void logDestruction(Session *objAddr) throw();
  };
  Session::~Session()
  {
   try {
    logDestruction(this);
   }
   catch (...) { }
  }

  session的析构函数调用logDestruction记录有关session对象被释放的信息,它明确地要捕获从logDestruction抛出的所有异常。但是logDestruction的异常规格表示其不抛出任何异常。现在假设被logDestruction调用的函数抛出了一个异常,而logDestruction没有捕获。不会期望发生这样的事情,凡是正如我们所见,很容易就会写出违反异常规格的代码。当这个异常通过logDestruction传递出来,unexpected将被调用,缺省情况下将导致程序终止执行。这是一个正确的行为,这是session析构函数的作者所希望的行为么?作者想处理所有可能的异常,所以好像不应该不给session析构函数里的catch块执行的机会就终止程序。如果logDestruction没有异常规格,这种事情就不会发生。(一种防止的方法是如上所描述的那样替换unexpected)

  以全面的角度去看待异常规格是非常重要的。它们提供了优秀的文档来说明一个函数抛出异常的种类,并且在违反它的情况下,会有可怕的结果,程序被立即终止,在缺省时它们会这么做。同时编译器只会部分地检测它们的一致性,所以他们很容易被不经意地违反。而且他们会阻止high-level异常处理器来处理unexpected异常,即使这些异常处理器知道如何去做。综上所述,异常规格是一个应被审慎使用的公族。在把它们加入到函数之前,应考虑它们所带来的行为是否就是所希望的行为。
 
 
资源下载链接为: https://pan.quark.cn/s/d9ef5828b597 在本文中,我们将探讨如何通过 Vue.js 实现一个带有动画效果的“回到顶部”功能。Vue.js 是一款用于构建用户界面的流行 JavaScript 框架,其组件化和响应式设计让实现这种交互功能变得十分便捷。 首先,我们来分析 HTML 代码。在这个示例中,存在一个 ID 为 back-to-top 的 div 元素,其中包含两个 span 标签,分别显示“回到”和“顶部”文字。该 div 元素绑定了 Vue.js 的 @click 事件处理器 backToTop,用于处理点击事件,同时还绑定了 v-show 指令来控制按钮的显示与隐藏。v-cloak 指令的作用是在 Vue 实例渲染完成之前隐藏该元素,避免出现闪烁现象。 CSS 部分(backTop.css)主要负责样式设计。它首先清除了一些默认的边距和填充,对 html 和 body 进行了全屏布局,并设置了相对定位。.back-to-top 类则定义了“回到顶部”按钮的样式,包括其位置、圆角、阴影、填充以及悬停时背景颜色的变化。此外,与 v-cloak 相关的 CSS 确保在 Vue 实例加载过程中隐藏该元素。每个 .page 类代表一个页面,每个页面的高度设置为 400px,用于模拟多页面的滚动效果。 接下来是 JavaScript 部分(backTop.js)。在这里,我们创建了一个 Vue 实例。实例的 el 属性指定 Vue 将挂载到的 DOM 元素(#back-to-top)。data 对象中包含三个属性:backTopShow 用于控制按钮的显示状态;backTopAllow 用于防止用户快速连续点击;backSeconds 定义了回到顶部所需的时间;showPx 则规定了滚动多少像素后显示“回到顶部”按钮。 在 V
资源下载链接为: https://pan.quark.cn/s/9e7ef05254f8 以下是简化后的内容: 程序集变量 计数器:整数型 文本发送计次:整数型 子程序 __启动窗口_创建完毕 _手动发送数据_被单击 停止发送 发送预处理 判断端口是否启动成功,失败则提示并返回 根据组合框选择的进制类型,将编辑框内容转换后发送 发送失败则提示并返回 进制转换(被转换文本,被转换进制,转换的进制) 检查进制范围,错误则返回提示 规范参数,逐字符检查是否符合进制要求,不符合则返回错误提示 若进制相同直接返回原文本 否则进行进制转换并返回结果 _退出_被单击销毁 _组合框_端口号_列表项被选择 停止发送 设置端口号 _组合框_波特率_列表项被选择 停止发送 设置波特率 _组合框_数据位_列表项被选择 停止发送 设置数据位数 _组合框_校验_列表项被选择 停止发送 设置奇偶校验方案 _组合框_停止位_列表项被选择 停止发送 设置停止位数 发送预处理 停止发送 设置波特率、端口号、数据位数、奇偶校验方案、停止位数 根据奇偶校验方案设置校验标志 _选择框_DTR_被单击 根据选中状态设置信号操作 _选择框_RTS_被单击 根据选中状态设置信号操作 _选择框_Break_被单击 根据选中状态设置信号操作 _编辑框_发送周期_内容被改变 若时钟标志选中,设置时钟周期 _选择框_时钟标志_被单击 若选中,设置发送方式为时钟模式,启动发送并设置时钟周期 否则,停止发送,设置时钟周期为0 _组合框_发送方式_列表项被选择 根据选择设置时钟标志和时钟周期 _端口_发送数据_收到信号 _端口_接收数据_收到信号 _端口_接收数据_数据到达 根据接收数据的进制选择,将数据转换后显示在编辑框中 _时钟1_周期事件 根据发送方式和进制选择,周期性发送数据 打开并读入文件 打开文件,读取内容到编辑框 _打开
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值