首页 > 编程语言 >掌握 C++ 异常艺术:构建健壮程序的秘诀与实战策略「一」

掌握 C++ 异常艺术:构建健壮程序的秘诀与实战策略「一」

时间:2024-07-20 21:29:08浏览次数:16  
标签:健壮 对象 抛出 秘诀 C++ 捕捉 catch 异常 MyException

以下内容为本人的烂笔头,如需要转载,请全文无改动地复制粘贴,原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/WC8CThJ77oHMsCSH0CBzsQ

在过去几十年的编程历史中,异常处理的演变仿佛一场文明的进化史,它不仅仅是技术的革新,更是编程思想与哲学的深刻体现。

从古早的错误码时代,程序员们在代码的荒野中艰难跋涉,每一个错误都需要手动检查,仿佛在茫茫大海中寻找失落的宝藏。

那时,程序的健壮性如同脆弱的瓷器,一触即碎。

直到 C++ 及其它现代编程语言的兴起,异常处理机制如同一场技术革命,为虚拟世界带来了前所未有的韧性与优雅。它借鉴了古老的战争哲学——“让情报流动”,如同古代战争中传递烽火信号,迅速将问题通知给能处理它的机构。

这不仅是对错误处理方式的革新,更是编程思维的一次飞跃,从被动防御转向了主动管理复杂性。

想象一下中世纪的城堡,坚固的城墙(try 块)围护着核心区域,当外来侵扰(异常)发生时,哨兵(catch 块)立即响应,或直接抵御,或发出警报,甚至调用援军(再次抛出异常)。

这便是 C++ 异常处理机制的形象比照,它不仅是一种技术手段,也是编程智慧与人类文明的结晶。

就如同罗马帝国修建的庞大道路网,让信息与物资得以高效流通,C++ 的异常处理机制确保程序在遇到不可预知的挑战时,能灵活应对,排除错误,确保核心逻辑的顺畅执行。

每一次异常的抛出与捕获,都是对程序健壮性的一次检验。正如历史长河中,文明在一次次危机中涅槃重生,变得更加坚强。

现在,让八戒带领大家一同踏入这场编程史上的伟大征程,从如何优雅地抛出第一个异常,到如何智慧地布局 try 与 catch,再到如何巧妙地减少 try 块的冗余,每一步都是对过往智慧的致敬,也是对未来编程艺术的探索。

这个系列的主题会分为多篇文章推送,感兴趣的读者朋友记得点赞收藏,如能多多转发让更多朋友受益,笔者不胜感激!

如何抛出异常?

抛出异常信号时,对信号的要求不多,比较灵活,一般建议抛出对象,无论是内置的数据类型,还是自定义类,最佳建议是基于 std::exception 的实例(包括派生类的实例)。

std::exception 是 C++ 标准库中提供的一个异常信号基类,需要包含 头文件。它是所有标准异常类的基类。这个类的目标是提供一个统一的异常处理接口,因此捕获标准库各种异常都可以基于这个类。

抛出信号前,直接创建一个对象吗?比较好的实践是,抛出一个临时对象。下面举个例:

class MyException
    : public std::runtime_error {
public:
    MyException() : std::runtime_error("MyException") { }
};

void fun() {
    // ...
    throw MyException();
}

如何捕获异常?

C++ 捕捉异常信号有好几种方式:

  1. 值捕捉(catch-by-value)
  2. 引用捕捉(catch-by-reference)
  3. 指针捕捉(catch-by-pointer)

这几种捕捉异常信号的方式和函数调用时的参数传递极为相似。

最佳实践是,推荐使用引用捕捉,除非有充足的理由不这么做,比如特殊情况下可采用指针来捕捉异常信号。但是要尽量避免采用值捕捉,因为值捕捉会导致异常对象的复制,并且复制品可能会展现出与原抛出对象不同的行为。

值捕捉(catch-by-value)

捕捉异常时,值捕捉是不推荐的使用方式,看看下面的例子:

class MyException
    : public std::runtime_error
{
public:
    MyException()
      : std::runtime_error("MyException") { }
};

void fun() {
    try {
        throw MyException("exception message");
    } catch (MyException e) {
        std::cout << "Caught exception by value: "
          << e.what() << std::endl;
    }
}

通过值捕捉异常有几个方面的表现糟糕:

  1. 效率低

异常对象被抛出后,如果通过值捕捉,编译器需要创建被抛出对象的副本,也就是执行拷贝构造函数。拷贝构造函数可能是浅拷贝也可能是深拷贝,如果异常对象内部包含大量数据(比如容器、内存缓冲等)或者复杂数据结构,一旦启动深拷贝,将可能带来极大的性能开销。

  1. 行为不一致

当异常对象中包含指针或引用成员变量时,一旦拷贝执行的是浅拷贝,比如只复制指针而非指针所指的缓冲,延伸出资源竞争的问题,复制品与原抛出对象在行为上就会变得不一致。

  1. 资源管理混乱

如果异常类包含有某些资源,如文件句柄、数据库连接、锁等,复制可能会导致资源被重复管理。比如,复制品和原抛出对象都可能尝试释放同一个资源,资源将被重复释放(double-free),触发程序崩溃。还有,如果修改复制品中的这些资源,可能会影响到原异常对象的状态,进而促使对象进入不决定的状态,引发漏洞。

引用捕捉(catch-by-reference)

引用捕捉不会导致对象的拷贝,和原抛出对象行为一致,这是最推荐的捕捉方式,能满足绝大部分的使用场景,形式如下:

try {
    throw MyException("An exception message");
} catch (const MyException& e) {
    std::cout << "Caught exception by reference: "
        << e.what() << std::endl;
} catch (...) {
    std::cerr << "Caught an unknown exception"
        << std::endl;
}

catch (const MyException& e) 语句块参数声明中添加了修饰符 const 用于表示不会修改原抛出异常对象,& 表示以引用方式捕捉异常。

catch (...) 语句块是为了捕捉剩余所有未被匹配到的异常信号,加强程序的健壮性。

指针捕捉(catch-by-pointer)

通过指针来捕捉异常对象在特殊场景下是有意义的,比如在捕捉异常对象之后还需要重新抛出去,方便其它处理块再次捕捉,又或者,为了需要处理多态异常类型时,特别适合使用这种方式捕获异常。

下面以处理多态异常类型为例:

class BaseException
    : public std::exception {
public:
    virtual const char* what()
        const noexcept override {
        return "BaseException occurred";
    }
};

class DerivedException
    : public BaseException {
public:
    virtual const char* what()
        const noexcept override {
        return "DerivedException occurred";
    }
};

int main() {
    try {
        throw new DerivedException();
    } catch (BaseException* e) {
        std::cerr << "Caught exception: "
            << e->what() << std::endl;
        delete e;
    } catch (...) {
        std::cerr << "Caught an unknown exception"
            << std::endl;
    }
    return 0;
}

从上面的代码来看,想要通过指针来捕捉异常信号对象,那么在抛出的时候也需要抛出对应的指针,然后捕捉的时候可声明为基类的对象指针,实现多态捕捉。

但是,相应的,通过指针捕获异常后,还需要手动管理异常对象的生命周期,所以这种方式可能带来内存资源的泄漏,又或者悬空指针的问题。

除此之外,通过指针来捕捉异常的方式还有个令人疑惑的场景,到底应不应该由 catch 语句块负责指针指向对象的释放?下面来看几个例子:

MyException x;
void f()
{
  MyException y;
  try {
    int num = rand() % 3;
    switch () {
      case 0: throw new MyException;
      case 1: throw &x;
      case 2: throw &y;
    }
  }
  catch (MyException* p) {
    // delete p; ?
  }
}

上面这个例子中,如果随机数计算结果返回 0,那么在 catch 语句块中释放异常对象是合适的,因为异常对象是通过 new 在 try 语句块中动态实例化的。

相反,如果随机数计算结果返回 1 或者 2 时,都不适合在 catch 语句块中再释放异常对象,因为这时被抛出的异常对象的生命周期并不完全在 try-catch 语句块内,内存管理不一致,擅自释放对象可能导致程序崩溃的。

再说,如果异常对象是通过 new 在 try 语句块中动态实例化的,那么在 catch 语句块中释放异常对象就一定没有问题了吗?

抛出异常对象时,需要依赖内存分配顺利,也就是说当内存不足时,继续使用这种方式抛出异常有可能会无法正常抛出。

可见,通过指针来捕捉异常的真实处理过程可以非常复杂,并不是所有场合都应该推荐使用。

看到这里,有的读者朋友可能会突然想起,微软曾经称霸一时的骨灰级 GUI 框架 MFC 里边就充斥着这种指针捕捉异常的方式,你看微软不也照用不误嘛,也不耽误人家软件产品风靡全世界。

诚然,这种方式放在现在的确很让人疑惑,但是,MFC 推出的时候,C++ 的异常处理标准还没发布呢!总不能要求别人发布的时候就可以预测未来吧?再说,这妨碍人家发布 Windows 了吗?

所以,如果你用了什么比较古老的 SDK 或者库,跟着人家的调调走也是可以的。用了人家的框架,不妨接受人家的约束,照着框架的思维处理问题。

本系列文章还没结束,这是第一篇,欢迎关注我!

标签:健壮,对象,抛出,秘诀,C++,捕捉,catch,异常,MyException
From: https://blog.csdn.net/2301_76403635/article/details/140537238

相关文章

  • Android C++系列:Linux文件系统(二)
    1.VFS虚拟文件系统Linux支持各种各样的文件系统格式,如ext2、ext3、reiserfs、FAT、NTFS、iso9660等等,不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式,然而这些文件系统都可以mount到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用l......
  • Android C++系列:函数返回值注意事项
    1.背景函数返回值就是使用return语句终止正在执行的函数,看是很简单的问题有什么说的呢?因为越是简单的问题里面越是有一些不易发现的坑。比如在循环中使用return语句:boolfindChar(conststring&str,constcharc){autosize=str.size();for(decltype(size......
  • 【C++BFS 回溯】756. 金字塔转换矩阵
    本文涉及知识点C++BFS算法C++回溯LeetCode756.金字塔转换矩阵你正在把积木堆成金字塔。每个块都有一个颜色,用一个字母表示。每一行的块比它下面的行少一个块,并且居中。为了使金字塔美观,只有特定的三角形图案是允许的。一个三角形的图案由两个块和叠在上面的单......
  • GESP C++ 二级真题(2023年12月)T1 小杨做题
    问题描述:为了准备考试,小杨每天都要做题。第一天做了a道题;第二天做了b道题;从第三天起,小杨每天做的题目数量是前两天的总和。此外,小杨还规定当自己某一天做了大于或等于m题时,接下来的日子,他就不做题了。请问到了第n天,小杨总共做了多少道题?输入描述:总共4行。第一行一个整数a,......
  • 使用GDAL(C++库)从末尾行开始向上读取图像数据
    使用GDAL(C++库)从末尾行读取图像数据OpenCV等图像库默认的读取方式都是从第一行开始,逐行读取数据(自顶向下),填充到内存缓冲区;对于某些特殊应用,需要反行序读取(从末尾行读到起始行)的图像数据结果。GDAL提供了灵活的栅格数据读取方式RasterIO,下面介绍RasterIO的调用方式,以及如何......
  • 在VS2022中通过Nuget将vcpkg环境集成/卸载到c++项目
    在VS2022中通过Nuget将vcpkg环境集成/卸载到c++项目vcpkg是微软和C++社区维护的免费开源C/C++包管理器。利用它,可以一条命令编译安装用户所需的库;提供CMake配置文件;并且对于Windows开发者,在VisualStudio中集成后还可以自动链接静态库,非常方便易用。一般而言,开发者仅需要......
  • C++的输入输出(ACM模式)
    原文1.输入首先,在C++语言中,要使用标准的输入,需要包含头文件<iostream>1.1cincin是c++中标准的输入流对象,cin有两个用法,单独读入和批量读入cin的原理:简单来讲,有一个缓冲区,键盘输入的数据会先存到缓冲区,用cin可以从缓冲区中读取数据。注意:cin可以连续从键盘读入数据cin......
  • 如何编写一个C++程序来整蛊你的好基友
    如何编写一个C++程序来整蛊你的好基友如何编写一个C++程序来整蛊你的好基友整蛊按照危险性来排序3星类1.每行输出一句2.一直输出,不换行3.给控制台换一个颜色(较有威慑力)颜色代码4.扫盘(配上第三个效果更好,可以用来装B)4星类(含部分解药)弹窗类弹窗代码按下反馈键判定另......
  • c++里数的存储
    hello,大家好啊,这里是文宇,不是文字,是文宇哦。C++中的数的存储方式涵盖了整数、浮点数、字符等多种类型。每种类型的数有不同的位数和存储规则。下面将详细介绍C++中数的存储。首先,整数类型的存储通常使用二进制来表示。C++中提供了多种整数类型,包括char、short、int、longlon......
  • C++生化危机2.0.yl.3已更新
    本版本修复了一个BUG,邻居家无法进入已修复一些小BUG也修复完成(作者体验游戏时发现的)下载链接:生化危机2.0.yl.3.rar-蓝奏云代码如下(建议下载,因为rar解压包内内容更全):#include<bits/stdc++.h>#include<windows.h>#include<time.h>#include<conio.h>usingnamespacestd......