如何处理C++构造函数中的错误

2023-05-16

用C++写代码的时候总是避免不了处理错误,一般来说有两种方式,通过函数的返回值或者抛出异常。C语言的错误处理一律是通过函数的返回值来判断的,一般是返回0NULL或者-1表示错误,或者直接返回错误代码,具体是哪种方式没有统一的规定,各种API也各有各的偏好。譬如fopen函数,当成功时返回文件指针,失败时返回NULL,而POSIX标准的open函数则在成功时返回0或者正数,失败时返回-1,然后需要再通过全局变量errno来判断具体错误是什么,配套的还有一系列perrorstrerror这样的函数。

C++的错误处理方式

C++号称向下兼容C语言,于是就将C语言通过返回值的错误处理方式也搬了进来。但C++最大的不同是引入了异常机制,可以用throw产生一个异常,并通过trycatch来捕获。于是就混乱了,到底是什么时候使用返回值表示错误,什么时候使用异常呢?首先简单谈论一下异常和返回值的特点。


异常的优点

  1. 错误信息丰富,便于获得错误现场
  2. 代码相对简短,不需要判断每个函数的返回值

异常的缺点

  1. 使控制流变得复杂,难以追踪
  2. 开销相对较大

返回值的优点

  1. 性能开销相对小
  2. 避免定义异常类

返回值的缺点

  1. 程序员经常「忘记」处理错误返回值
  2. 每个可能产生错误的函数在调用后都需要判断是否有错误
  3. 与「真正的」返回值混用,需要规定一个错误代码(通常是0-1NULL

使用异常还是返回值

我的观点是,用异常来表示真正的、而且不太可能发生的错误。所谓不太可能发生的错误,指的是真正难以预料,但发生了却又不得不单独处理的,譬如内存耗尽、读文件发生故障。而在一个字符串中查找一个子串,如果没有找到显然应该是用一个特殊的返回值(如-1),而不应该抛出一个异常。

一句话来概况就是不要用异常代替正常的控制流,只有当程序真的「不正常」的时候,才使用异常。反过来说,当程序真正发生错误了,一定要使用异常而不是返回一个错误代码,因为错误代码总是倾向于被忽略。如果要保证一个以返回值来表示错误代码的函数的错误正确地向上传递,需要在每个调用了可能产生错误的函数后面都判断一下是否发生了错误,一旦发生了不可解决的错误,就要终止当前函数(并释放当前函数申请的资源),然后向上传递错误。这样一来错误处理代码会被重复地写好几遍,十分冗杂,譬如下面代码:

int func(int n) {
  int fd = open("path/to/file", O_RDONLY);
  if (fd == -1) {
     return ERROR_OPEN;
  }
  int* array = new[n];
  int err;
  err = do_something(fd, array);
  if (err != SUCCESS) {
     delete[] array;
     return err;
  }
  err = do_other_thing();
  if (err != SUCCESS) {
     delete[] array;
     return err;
  }
  err = do_more_thing();
  if (err != SUCCESS) {
     delete[] array;
     return err;
  }
  delete[] array;
  return SUCCESS;
}


对使用异常容易增加函数出口的指控其实是不成立的,因为即使使用返回值,这些出口也是免不了的,除非程序员有意或无意忽略掉,但异常是不可忽略的。如果你认为可以把判断错误的if语句缩写到一行使代码变得「更清晰」,那么我只能说是自欺欺人。

有些错误几乎总是可以被立即恢复(譬如前面所说的查找一个字符串不存在的子串,甚至都不能说这是一个「错误」),而且返回值本身就传递一定信息,就不需要使用异常了。

鉴于C++没有统一的ABI,并不建议在模块的接口上使用异常。如果要使用,就要把可能曝露给用户的异常全部声明出来,不要把其他类型的异常丢给用户去处理,尤其是内部状态——模块的使用者通常也不会关心模块内部具体是哪条语句发生错误了。

构造函数中的错误

有一个相当实际的问题是,如何处理构造函数的错误?我们都知道构造函数是没有返回值的,怎么办呢?通常有三种常见的处理方法,标记错误状态使用一个额外的initialize函数来初始化,或者直接抛出异常

合格的C++程序员都知道C++的析构函数中不应该抛出异常,一旦析构函数中的异常没有被捕获,整个程序都要被中止掉。于是许多人就对在构造函数中抛出异常也产生了对等的恐惧,宁可使用一个额外的初始化函数在里面初始化对象的状态并抛出异常(或者返回错误代码)。这样做违背了对象产生和初始化要在一起的原则,强迫用户记住调用一个额外的初始化函数,一旦没有调用直接使用了其他函数,其行为很可能是未定义的。

使用初始化函数的惟一好处可能是避免了手动释放资源(释放资源的操作交给析构函数来做),因为C++的一个特点是构造函数抛出异常以后析构函数是不会被调用的,所以如果你在构造函数里面申请了内存或者打开了资源,需要在异常产生时关闭。但想想看其实并不能完全避免,因为有些资源可能是要在可能产生错误的函数调用过后才被申请的,还是无法完全避免手工的释放。

标记错误状态也是一种常见的形式,譬如STL中的ifstream类,当构造时传入一个无法访问的文件作为参数,它不会返回任何错误,而是标记的内部状态为不可用,用户需要手工通过is_open()函数来判断是否打开成功了。同时它还有good()fail()两个函数,同时也重载了bool类型转换运算符用于在if语句中判断。标记状态的方法在实践中相当丑陋,因为在使用前总是需要判断它是否「真的创建成功了」。

最直接的方法还是在构造函数中抛出异常,它并不会向析构函数中抛出异常那样有严重的后果,只是需要注意的是抛出异常以后对象没有被创建成功,析构函数也不会被调用,所以应该自行把申请的资源全部都释放掉。

如何在构造函数中捕获异常

构造函数与普通函数有一个很不一样特性,就是构造函数可以有初始化列表,例如下面的代码:

class B {
 public:
  B(int val) : val_(val * val) {
  }
 private:
  int val_;
};

class A {
 public:
  A(int val) : b_(val) {
    a_ = val;
  }
 private:
  int a_;
  B b_;
};


以上的代码中A的构造函数的函数体的语句在执行之前会先调用B的构造函数,这时候问题在于,如果B的构造函数抛出了异常,A该如何捕获呢?一个迂回的做法是在A中把B的实例声明为指针,在构造函数和析构函数中分别创建和删除,这样就能捕获到异常了。不过,实际上是有更简单的做法的。下面我要介绍一个C++的很不常见的语法:函数作用域级别的异常捕获。

class B {
 public:
  B(int val) : val_(val * val) {
    throw runtime_error("wtf from B");
  }
 private:
  int val_;
};

class A {
 public:
  A(int val) try : b_(val) {
    a_ = val;
  } catch (runtime_error& e) {
    cerr << e.what() << endl;
    throw runtime_error("wtf from A");
  }
 private:
  int a_;
  B b_;
};


注意上面A的构造函数,在参数列表后和初始化列表前增加了try关键字,然后构造函数就被分割为了两部分,前面是初始化,后面是初始化时的错误处理。需要指出的是,catch块里面捕获到的异常不能被忽略,即catch块中必须有一个throw语句重新抛出异常,如果没有,则默认会将原来捕获到的异常重新抛出,这和一般的行为是不同的。例如下面代码运行可以发现A会将捕获到的异常原封不动抛出:

class A {
 public:
  A(int val) try : b_(val) {
    a_ = val;
  } catch (runtime_error& e) {
    cerr << e.what() << endl;
  }
 private:
  int a_;
  B b_;
};


这种语法是C++的标准,而且目前已经被所有的主流C++编译器支持(VS2010、g++ 4.2、clang 3.1),所以几乎不存在兼容性问题,大可放心使用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何处理C++构造函数中的错误 的相关文章

随机推荐

  • linux cmake分别指定编译/运行时动态库链接路径

    1 背景 在树莓派上开发执行程序 xff0c 需要用到opencv curl jsoncpp等库函数支持 xff0c apt get安装好后 xff0c 编译执行正常 但是执行程序挪到别的板子上 xff0c 都要apt get安装这些库 x
  • C语言函数库之字符串连接函数(string.h)

    1 字符串连接函数strcat 函数定义 xff1a char strcat char str1 const char str2 函数功能 xff1a 把str2 包括 39 0 39 拷贝到str1的尾部 连接 xff0c 并返回str1
  • foc学习笔记3——电流环

    foc学习笔记3 电流环 电流环的作用 前文不断强调 xff0c 进行磁场定向控制需要控制的是电流而非电压 xff0c 只是因为我们没有办法直接去控制电流才暂时退而求其次地去控制电压 虽然电压控制的效果也还不错 xff0c 但由于电机不是单
  • ESP32之蓝牙配网blufi

    概览 BluFi 是一款基于蓝牙通道的 Wi Fi 网络配置功能 xff0c 适用于 ESP32 它通过安全协议将 Wi Fi 配置和证书传输到 ESP32 xff0c 然后 ESP32 可基于这些信息连接到 AP 或建立 SoftAP B
  • 机器人学习之项目- Project2 :Where Am I?

    1 项目概述 欢迎来到 Where Am I 我在哪里 定位项目 在这个项目中 xff0c 将学习如何利用ROS AMCL包在Gazebo模拟环境的地图中准确地定位移动机器人 在完成本项目的过程中 xff0c 涉及机器人软件工程的几个方面
  • 将不同类对象指针存放QVector中

    需求 xff1a 将一系列操作步骤放在链表中按需调用 我将每个操作步骤用一个类实现 xff0c 将各类继承于一个基类 xff0c 再将指向各子类的基类指针放在QVector Vector xff0c 即可通过C 43 43 的多态实现调用各
  • 体感摄像头 realsense 系列硬件资料

    一 体感摄像头 Intel的体感摄像机是具有深度图像采集能力的摄像机 xff0c 目前已经出到了400系列 与kinect 2 xff0c ZED xff0c leap motion比较 xff0c 属于比较中庸 手势识别方面不如leap
  • C语言进度条的实现

    C语言进度条的实现 buff N xff1a 进度条状态字符串 xff0c N大小决定进度条长度 xff08 自行调整 xff09 xff0c 使用前先将所有字符初始化置零 xff0c 我这里使用的是memset函数 xff1b label
  • numpy下的随机数

    版权声明 xff1a 本文为博主原创文章 xff0c 未经博主允许不得转载 https blog csdn net m0 38061927 article details 75335069 在使用Python进行数据处理时 xff0c 往往
  • 宏函数 可变参数 C/C++

    span class token macro property span class token directive hash span span class token directive keyword include span spa
  • 排查ingress 404报错的方法--附:在容器中使用tcpdump抓包

    出现这个问题 xff0c 一般是由于路由不通导致的 xff0c 需要进行抓包排查 首先要解决的问题是 xff0c 如何在 ingress controller 容器中安装抓包工具 1 由于 ingress controller 的mando
  • fastApi介绍与重要版本更新细节

    fastApi介绍 FastAPI 是用于使用 Python 构建 API 的现代 Web 框架 它建立在 Starlette 框架之上 xff0c Starlette 框架是一个轻量级且可扩展的 ASGI xff08 异步服务器网关接口
  • 3. fastApi查询参数详解

    当声明的参数不是路径参数时 xff0c 路径操作函数会把该参数自动解释为查询参数 如下 路径操作函数将参数skip limit解释为查询参数 query parameter span class token decorator annota
  • 4. fastApi请求体详解

    简单介绍网络请求 网络请求是指客户端 xff08 例如浏览器 移动应用程序等 xff09 向服务器发送请求 xff0c 以获取特定资源或执行特定操作的过程 HTTP请求是一种常见的网络请求协议 xff0c 它通过互联网连接客户端和服务器 x
  • python多线程与多进程简略介绍

    GIL介绍 在python中GIL的限制导致不论是在单核还是多核条件下 xff0c 同时刻都只能运行 一个线程 xff0c 这使得Python多线程无法发挥多核并行的优势 GIL全称为Global Interpreter Lock意思是全局
  • 高并发场景下,python各web框架的优劣对比与示例分析

    高并发场景下 xff0c python各个web框架的优劣对比与示例分析 Python有许多适合高并发场景的Web框架 xff0c 下面是其中几个 xff1a Flask Flask是一个轻量级的Web框架 xff0c 由Werkzeug和
  • 6. fastApi文件上传请求处理示例

    需求 xff1a 开发文件上传功能接口 接口路径 files 请求类型 post 响应结果 返回文件的大小 单位bytes 实现方案 使用post类型处理方法 xff0c 指定参数类型为bytes或UploadFile 使用File 方法处
  • 7. fastApi表单数据处理详解与示例

    需求 前端通过表单数据的形式发送用户名与密码到后端 xff0c 后端通过用户信息校验 xff0c 过滤出合法用户 xff0c 并为用户设置cookie 超时时间为24h xff09 接口路径 login 请求类型 post方法发送的文件 响
  • C语言结构体字节对齐规则

    C语言结构体字节对齐规则 基本规则 规则1 xff1a 结构体 xff08 struct xff09 的数据成员 xff0c 第一个数据成员放在offset为0的地方 xff0c 以后每个数据成员存放在offset为该数据成员大小的整数倍的
  • 如何处理C++构造函数中的错误

    用C 43 43 写代码的时候总是避免不了处理错误 xff0c 一般来说有两种方式 xff0c 通过函数的返回值或者抛出异常 C语言的错误处理一律是通过函数的返回值来判断的 xff0c 一般是返回0 NULL 或者 1 表示错误 xff0c