欢迎来到我的频道 【点击跳转专栏】
码云链接 【点此转跳】
在这里插入图片描述

1. 异常的概念及使⽤

1.1 异常的概念

  • 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。异常使得我们能够将问题的检测与解决问题的过程分开:程序的一部分负责检测问题的出现,然后将解决问题的任务传递给程序的另一部分。检测环节无须知道问题处理模块的所有细节。

通俗讲 就是一个程序 比如说客户端有一个bug (假设按钮出了问题跳转不了)导致程序出了问题,但是他不会终止你的整个客户端 而是以异常的方式抛出,纪录日志(抛到外层告诉你错误信息 捕获并处理问题,并将问题信息记录 同时调用界面库的API 弹出个窗口告诉用户哪里出问题了) 同时确保其他功能板块不受影响。

  • C语言主要通过错误码的形式处理错误。错误码本质上就是对错误信息进行分类编号,拿到错误码以后还要去查询对应的错误信息,比较麻烦。而异常是抛出一个对象,这个对象可以包含更全面的各种信息。

1.2 异常的抛出和捕获(结合1.3)

  • 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常。该对象的类型以及当前的调用链决定了应该由哪个 catch 块 的处理代码来处理该异常。
  • 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,抛出异常的部分会向异常处理部分传达到底发生了什么错误。
  • 当 throw 执行时,throw 后面的语句将不再被执行。程序的执行从 throw 位置跳转到与之匹配的 catch 模块。该 catch 可能是同一函数中的局部 catch,也可能是调用链中另一个函数中的 catch。控制权从 throw 位置转移到 catch 位置。这里还有两个重要的含义:
    1. 沿着调用链的函数可能提早退出;
    2. 一旦程序开始执行异常处理程序,沿调用链创建的对象都将被销毁
  • 抛出异常对象后,会生成一个异常对象的拷贝。因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝。这个拷贝对象会在 catch 子句结束后被销毁。(这里的处理类似于函数的传值返回)

不过C++11 以后的优化,省略掉了这里的拷贝,直接不产生对象 变成了别名的形式进行直接构造(但我认为没必要深究 1.4案例依然按照C++98标准讲)

1.3 栈展开(结合1.2)

  • 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。
  • 如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开
  • 如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的 terminate 函数终止程序。
  • 如果找到匹配的catch子句处理后,catch子句代码会继续执行

在这里插入图片描述

1.4 异常实例(仔细读完1.2 1.3 后给出的参考例子)

double Divide(int a, int b)
{
    try
    {
        // 当b == 0时抛出异常
        if (b == 0)
        {
            string s("Divide by zero condition!");
            throw s;
        }
        else
        {
            return ((double)a / (double)b);
        }

        //... fxx()
    }
    catch (const int& s)
    {
        cout << s << endl;
    }

    cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
    return 0;
}

void Func()
{
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
    cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}

int main()
{
    try
    {
        Func();
    }
    catch (const string& errmsg)
    {
        cout << errmsg << endl;
    }
    catch (int errid)
    {
        cout << errid << endl;
    }

    cout<<"hello"<<endl;

    return 0;
}

⚠️ 异常情况(b == 0
假设输入:5 0

步骤 1:Divide(5, 0) 中抛出异常

if (b == 0) {
    string s("Divide by zero condition!");
    throw s;   // 抛出 std::string 类型异常  ← 拷贝在此刻发生
    //以 s 为源,构造一个异常对象的拷贝
    //将这个拷贝存储在特殊的异常内存区域(通常不在栈上,也不在堆上,由运行时管理)

}

步骤 2:查找匹配的 catch 块(从当前函数开始)

  • Divide 函数内部有:
    catch (const int& s)
    {
        cout << s << endl;
    }
    
  • 但抛出的是 string,而 catch 只接受 const int&
  • 类型不匹配!这个 catch 无法捕获该异常

✅ C++ 规则:异常会跳过不匹配的 catch,继续沿调用栈向上传播。

步骤 3:异常离开 Divide 函数

  • 因为 Divide 内部没有能捕获 stringcatch
  • 异常自动传播到调用者Func()
  • Divide栈帧销毁,函数成员变量析构(包括s)这就是为什么会生成一个异常对象的拷贝的原因。

步骤 4:Func() 没有 try-catch

  • Func() 直接调用 Divide(len, time),未包裹在 try
  • Func() 函数栈帧销毁,不会输出 Func:37行执行
  • 所以异常继续向上传播 → 到 main()

步骤 5:main() 中的 catch 块

catch (const string& errmsg)   // ✅ 匹配!
{
    cout << errmsg << endl;
}
  • 成功捕获 string 异常
  • 输出:Divide by zero condition!
  • catch 后面代码正常执行 输出 hello

就近捕获 如果我在Divide(int a, int b)中修改catch 让其与对应throw匹配

double Divide(int a, int b)
{
    try
    {
        // 当b == 0时抛出异常
        if (b == 0)
        {
            string s("Divide by zero condition!");
            throw s;
        }
        else
        {
            return ((double)a / (double)b);
        }

        //... fxx()
    }
//    catch (const int& s)
//    {
//        cout << s << endl;
//    }
    catch (const string& errmsg)
    {
        cout << errmsg << endl;
    }


    cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
    return 0;
}
  • 此时直接在Divide()函数中就成功捕获异常 后面函数正常执行
    在这里插入图片描述

1.5 查找匹配的处理代码(重点)

  • 一般情况下抛出对象和 catch 是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
  • 但是也有一些例外,允许从非常量常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的

我重点标记的 也是这部分最重要的!!! 比如你定义了一个基类异常 Error,然后派生出 FileErrorNetworkError 等具体异常;当你在代码中抛出 FileError 时,只要用 catch (const Error& e) 就能捕获它——就像用“水果”这个类别接住一个“苹果”,无需为每种错误单独写 catch,既简洁又方便扩展,这也是我们将来在公司工作的时候(因为每个人会负责不同板块,但是异常的捕获 又支持任意类型 ,这能有效解决冲突!!)

  • 如果到 main 函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般 main 函数中最后都会使用 catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <cstdlib>  // for rand(), srand()
#include <ctime>    // for time()

using namespace std;

// ============================================================================
// 在大型项目中,不同模块(如数据库、缓存、网络)可能产生不同类型的错误。
// 为了统一处理,我们设计一个公共的异常基类 Exception,
// 所有具体异常都继承它,这样上层只需 catch 基类引用,就能捕获所有派生异常。
// 这利用了 C++ 的“派生类对象可被基类引用绑定”的特性(多态 + 异常匹配规则)。
// ============================================================================

// 异常基类:所有业务异常的公共祖先
class Exception
{
public:
    // 构造时记录错误信息和唯一错误码(便于程序判断)
    Exception(const string& errmsg, int id)
        : _errmsg(errmsg)
        , _id(id)
    {}

    // 虚函数:子类可重写以提供更详细的错误描述
    virtual string what() const
    {
        return _errmsg;
    }

    // 获取错误码,用于逻辑判断(比如是否重试)
    int getid() const
    {
        return _id;
    }

    // 虚析构函数
    virtualException() = default;

protected:
    string _errmsg;  // 错误描述
    int _id;         // 错误ID(如100=权限不足,101=资源不存在)
};

// ============================================================================
// SQL 模块异常:附加了出错的 SQL 语句,便于调试
// ============================================================================
class SqlException : public Exception
{
public:
    // 额外保存 SQL 语句
    SqlException(const string& errmsg, int id, const string& sql)
        : Exception(errmsg, id)   // 调用基类构造
        , _sql(sql)
    {}

    // 重写 what(),拼接 SQL 信息
    virtual string what() const override
    {
        string str = "SqlException:";
        str += _errmsg;
        str += "->";
        str += _sql;
        return str;
    }

private:
    const string _sql;  // 出问题的 SQL 语句
};

// ============================================================================
// 缓存模块异常
// 注意:现在正确继承了 Exception,所以能被 main 中的 catch(Exception&) 捕获!
// ============================================================================
class CacheException : public Exception
{
public:
    CacheException(const string& errmsg, int id)
        : Exception(errmsg, id)
    {}

    virtual string what() const override
    {
        string str = "CacheException:";
        str += _errmsg;  // 修复:之前漏掉了 _errmsg
        return str;
    }
};

// ============================================================================
// HTTP 服务异常:记录是 GET/POST 等请求类型
// ============================================================================
class HttpException : public Exception
{
public:
    HttpException(const string& errmsg, int id, const string& type)
        : Exception(errmsg, id)
        , _type(type)
    {}

    virtual string what() const override
    {
        string str = "HttpException:";
        str += _type;
        str += ":";
        str += _errmsg;
        return str;
    }

private:
    const string _type;  // HTTP 方法类型,如 "get", "post"
};

//⚠️项目功能模拟

// ============================================================================
// 模拟 SQL 管理模块:有一定概率失败
// ============================================================================
void SQLMgr()
{
    // 模拟 1/7 概率发生 SQL 错误
    if (rand() % 7 == 0)
    {
        throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
    else
    {
        cout << "SQLMgr 调用成功" << endl;
    }
}

// ============================================================================
// 缓存管理模块:可能抛出 CacheException,并调用 SQLMgr
// ============================================================================
void CacheMgr()
{
    if (rand() % 5 == 0)
    {
        throw CacheException("权限不足", 100);
    }
    else if (rand() % 6 == 0)
    {
        throw CacheException("数据不存在", 101);
    }
    else
    {
        cout << "CacheMgr 调用成功" << endl;
    }

    // 继续调用下层模块
    SQLMgr();
}

// ============================================================================
// HTTP 服务入口:可能抛出 HttpException,并调用 CacheMgr
// ============================================================================
void HttpServer()
{
    if (rand() % 3 == 0)
    {
        throw HttpException("请求资源不存在", 100, "get");
    }
    else if (rand() % 4 == 0)
    {
        throw HttpException("权限不足", 101, "post");
    }
    else
    {
        cout << "HttpServer调用成功" << endl;
    }

    // 调用下层缓存模块
    CacheMgr();
}

// ============================================================================
// 主函数:模拟一个持续运行的服务
// ============================================================================
int main()
{
    // 初始化随机数种子,让每次运行结果不同
    srand(static_cast<unsigned int>(time(0)));

    while (true)
    {
        // 每秒处理一次请求(模拟服务轮询)
        this_thread::sleep_for(chrono::seconds(1));

        try
        {
            // 入口:处理一个 HTTP 请求
            HttpServer();
        }
        // 【关键点】只用一个 catch 就能捕获所有 Exception 的派生类异常!
        // 因为 C++ 允许:throw 派生类对象 → catch 基类引用(自动向上转型)
        catch (const Exception& e)  // 多态:实际调用的是子类的 what()
        {
            // 输出具体错误信息(SqlException/CacheException/HttpException)
            cout << "[错误] " << e.what() << endl;
        }
        // 兜底:捕获非 Exception 体系的异常(如 std::exception 或其他)
        catch (...)
        {
            cout << "[未知异常] 程序遇到了未预期的错误!" << endl;
        }
    }

    return 0;
}

运行结果:
在这里插入图片描述

以下代码保障了程序的 健壮性

        // 兜底:捕获非 Exception 体系的异常(如 std::exception 或其他)
        catch (...)
        {
            cout << "[未知异常] 程序遇到了未预期的错误!" << endl;
        }

哪怕出现了未知的 异常 我的程序虽然有些模块挂了 但并不影响整体的运行!

1.6 异常重新抛出

有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。

// 模拟发送消息的底层函数
// 该函数会随机模拟两种失败场景:
//   - 网络不稳定(可重试)
//   - 好友关系已解除(不可重试)
void _SendMsg(const string& s)
{
    // 模拟 50% 概率因“网络不稳定”发送失败(错误码 102,属于临时性故障)
    if (rand() % 2 == 0)
    {
        throw HttpException("网络不稳定,发送失败", 102, "put");
    }
    // 模拟约 1/7 概率因“非好友”发送失败(错误码 103,属于永久性错误)
    else if (rand() % 7 == 0)
    {
        throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
    }
    else
    {
        // 其他情况视为发送成功
        cout << "发送成功" << endl;
    }
}

// 封装带重试机制的消息发送接口
// 对“网络问题”最多重试 3 次(共尝试 4 次),其他错误立即上报
void SendMsg(const string& s)
{
    // 总共尝试 4 次:第 0 次是首次,第 1~3 次是重试
    for (size_t i = 0; i < 4; i++)
    {
        try
        {
            _SeedMsg(s);  // 尝试发送
            break;        // 成功则跳出循环,不再重试
        }
        catch (const Exception& e)  // 捕获所有业务异常(多态)
        {
            // 判断是否为“网络不稳定”错误(错误码 102)
            if (e.getid() == 102)
            {
                // 如果已经是第 3 次重试(即总共第 4 次尝试)仍然失败,
                // 说明网络持续不佳,放弃重试,将异常重新抛出给上层处理
                if (i == 3)
                {
                    throw;  // 无参 throw:重新抛出当前异常(保留原始类型和信息)
                    //会被这里 throw 后用的一层补获 种植本次循环
                }

                // 否则,提示用户正在重试,并继续下一次循环
                cout << "开始第" << i + 1 << "次重试" << endl;
                // 注意:这里不 throw,循环会继续
            }
            else
            {
                // 如果是其他类型的错误(如 103:非好友),
                // 这类错误无法通过重试解决,应立即上报
                throw;  // 立即重新抛出,中断重试流程
            }
        }
    }
}

场景1:首次成功
_SeedMsg 不抛异常 → break → 函数正常返回。
场景2:网络错误,第2次重试成功
第0次:抛102 → 打印“第1次重试”
第1次:抛102 → 打印“第2次重试”
第2次:成功 → break
场景3:网络错误,4次都失败
前3次:打印重试信息
第3次(i=3):仍失败 → throw; → 异常传播到 SendMsg 调用者
场景4:非好友错误(103)
第0次:捕获103 → 立即 throw; → 不重试,直接上报

  1. throw;(无参)的作用
    • catch 块中使用 throw;重新抛出当前捕获的异常对象,保持其原始类型和状态。
    • 这是实现“中间层处理 + 上层统一处理”的关键。
  2. 错误分类处理
    • 可恢复错误(如网络抖动,ID=102)→ 重试。
    • 不可恢复错误(如权限拒绝,ID=103)→ 立即上报。
  3. 重试次数控制
    • i == 3 表示已经重试了 3 次(第 0 次是首次尝试),避免无限重试。
  4. 多态异常捕获
    • catch (const Exception& e) 能捕获所有派生类异常(如 HttpException),体现继承体系优势。

1.7 异常安全问题

  • 异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后再重新抛出。
  • 其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。《Effective C++》第8个条款也专门讲了这个问题,别让异常逃离析构函数。(尽量不要在析构函数内抛出异常 若抛出请谨慎处理!)
#include <iostream>
using namespace std;

// 简单除法函数:当除数为0时,抛出 C 风格字符串异常
double Divide(int a, int b)
{
    // 当 b == 0 时抛出异常
    if (b == 0)
    {
        // 抛出一个 const char* 类型的异常
        throw "Division by zero condition!";
    }
    return static_cast<double>(a) / static_cast<double>(b);
}

// 模拟一个可能抛异常的函数,并手动管理动态内存
void Func()
{
    // 【问题起点】手动分配堆内存
    int* array = new int[10];  // 分配 10 个 int 的数组

    // 如果在此之后、delete[] 之前发生异常,且未被捕获处理,
    // 就会导致内存泄漏!

    try
    {
        int len, time;
        cin >> len >> time;

        // 调用 Divide,若 time == 0,会抛出异常
        cout << Divide(len, time) << endl;

        // 如果 Divide 抛异常,下面这行不会执行!
    }
    catch (...)
    {
        // 【关键:异常安全处理】
        // 捕获任意类型的异常(... 表示“所有异常”)
        // 目的不是处理业务逻辑,而是 确保资源被释放

        cout << "delete []" << array << endl;
        delete[] array;   // 释放内存,防止泄漏

        // 【重要】重新抛出原始异常!
        // throw; (无参)会将捕获到的异常原样抛出,
        // 保证上层(main)仍能按类型处理(如捕获 const char*)
        throw;  // ← 这是实现“资源清理 + 异常透传”的核心
    }

    // 正常路径:没有异常时,也会释放内存
    cout << "delete []" << array << endl;
    delete[] array;
}

// 主函数:统一处理所有异常
int main()
{
    try
    {
        Func();
    }
    // 捕获 Divide 抛出的 C 风格字符串异常
    catch (const char* errmsg)
    {
        cout << "[Error] " << errmsg << endl;
    }
        // 兜底:捕获其他未知异常
    catch (...)
    {
        cout << "Unknown Exception" << endl;
    }

    return 0;
}
  1. 异常安全问题(Exception Safety)
  • Func() 中,new int[10] 申请了资源。
  • 如果 Divide 抛异常,程序会跳过正常路径的 delete[],导致内存泄漏
  • 通过 try-catch(...) 包裹可能抛异常的代码,并在 catch 中释放资源,解决了泄漏问题
  1. 为什么用 catch (...)
  • 因为 Divide 抛的是 const char*,但未来可能抛其他类型。
  • catch (...) 是“通用捕获”,确保任何异常都不会绕过资源释放
  1. throw; 的作用(无参重新抛出)
  • 保留原始异常的类型和值
  • 如果写成 throw errmsg;(有参),会丢失类型信息(比如无法被 catch(const char*) 捕获)。
  • throw; 是实现“中间层清理 + 上层处理”的标准做法。

当然智能指针的RAII方式解决这种问题是更好的 详细内容 请期待博主后面写的 智能指针 部分


static_cast(加餐)

static_cast 是 C++ 中四种类型转换操作符之一,用于在编译期进行安全、明确的类型转换。它比 C 风格强制转换 (type)value 更安全、更清晰,是现代 C++ 推荐的转换方式。

✅ 一、基本语法

static_cast<目标类型>(表达式)

✅ 二、常见用途与示例

  1. 基本数值类型之间的转换(隐式转换的显式写法)
double d = 3.14;
int i = static_cast<int>(d);  // i = 3(截断小数)

char c = static_cast<char>(65); // c = 'A'

📌 相当于告诉编译器:“我知道这会丢失精度,但我确定要这么做。”


  1. void* 与其他指针类型互转
int a = 10;
void* ptr = &a;

// 将 void* 转回 int*
int* p = static_cast<int*>(ptr);

⚠️ 注意:static_cast 不能去除 const 属性(需用 const_cast)。


  1. 有继承关系的类指针/引用转换(非多态)
  • 基类 → 派生类(向下转型,不安全!需程序员保证正确性
  • 派生类 → 基类(向上转型,安全,通常可省略 static_cast
class Base {};
class Derived : public Base {};

Derived d;
Base* b = &d;                          // 向上转型(隐式允许)
Derived* d2 = static_cast<Derived*>(b); // 向下转型(需显式,且必须确保 b 真指向 Derived)

🔥 危险!如果 b 实际指向的是 Base 对象(而非 Derived),则行为未定义。
✅ 安全做法:若涉及多态(有虚函数),应使用 dynamic_cast(运行时检查)。

C 风格 (int)d C++ 风格 static_cast<int>(d)
隐蔽、难以搜索 显式、代码意图清晰
可能意外执行危险转换(如 reinterpret_cast 编译器限制转换范围,更安全
无法区分不同转换语义 语义明确

1.8 异常规范

  • 对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。
  • C++98中函数参数列表的后面接 throw(),表示函数不抛异常,函数参数列表的后面接 throw(类型1, 类型2...) 表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。(不强制要求 兼容C语言)
// ===================================================================
// 1. 【C++98 风格(已弃用)】:throw() 表示不抛异常(等价于 noexcept)
//    throw(A, B) 表示只允许抛 A 或 B 类型(实践中几乎没人用)
// ===================================================================
void oldStyleFunction() throw()  // C++98 写法,C++11 起 deprecated
{
    cout << "这是 C++98 的异常规范(不推荐使用)\n";
}

// void anotherOldStyle() throw(int, string) { ... } // 更复杂,易出错
  • C++98的方式这种方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加 noexcept 表示不会抛出异常,啥都不加表示可能会抛出异常。
  • 编译器并不会在编译时检查 noexcept,也就是说如果一个函数用 noexcept 修饰了,但是同时又包含了 throw 语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了 noexcept 的函数抛出了异常,程序会调用 terminate 终止程序。
// ===================================================================
// 2. 【noexcept 声明】:告诉编译器和用户——此函数绝不会抛出异常
//    这有助于编译器优化,也帮助调用者简化错误处理逻辑。
// ===================================================================
void safeFunction() noexcept
{
    cout << "这个函数承诺不抛异常。\n";
    // 即使这里写 throw,编译也能通过(但运行时会 terminate!)
}

// ===================================================================
// 3. 【对比:未加 noexcept】
//    默认情况下,函数可能抛异常,调用者必须考虑异常安全。
// ===================================================================
void riskyFunction()
{
    if (rand() % 2 == 0)
        throw runtime_error("Oops! Something went wrong.");
    cout << "可能成功,也可能失败。\n";
}

// ===================================================================
// 4. 【危险示例】:声明 noexcept 但实际抛异常 → 程序直接终止!
//    编译器通常只警告(或不报错),但运行时调用 std::terminate()
// ===================================================================
void badNoexceptFunction() noexcept
{
    // throw 123;  // ← 如果取消注释,程序会 crash(调用 terminate)
    // 调用一个可能抛异常的函数也会导致 terminate!
    // riskyFunction(); // ← 同样危险!
    
    // 所以:noexcept 函数内部只能调用其他 noexcept 函数!
    safeFunction(); // ✅ 安全
}

  • noexcept(expression) 还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回 false,不会就返回 true。
// ===================================================================
// 【noexcept 作为运算符】:用于模板编程或条件编译
//    noexcept(expression) 是一个编译期常量表达式,返回 true/false
// ===================================================================
void demoNoexceptOperator()
{
    // 检查表达式是否会抛异常
    cout << "safeFunction() 是否 noexcept? " 
         << noexcept(safeFunction()) << endl;        // 输出 1 (true)

    cout << "riskyFunction() 是否 noexcept? " 
         << noexcept(riskyFunction()) << endl;       // 输出 0 (false)

    }
你的描述 代码中的体现
预先知道是否抛异常有益 safeFunction() 标记 noexcept,调用者无需 try-catch
C++98 的 throw()throw(A,B) oldStyleFunction() throw() 示例(已弃用)
C++11 用 noexcept 简化 所有 noexcept 声明均使用新语法
编译器不检查 noexcept,但运行时会 terminate badNoexceptFunction() 注释说明:若内部 throw,程序崩溃
noexcept(expr) 作为运算符 demoNoexceptOperator() 中演示返回 true/false

2.标准库的异常(了解)

C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,需要在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数派⽣类可以重写

https://legacy.cplusplus.com/reference/exception/exception/(参考文档)

2.1. C++ 标准异常体系的基类

  • 定义在头文件 <exception> 中。
  • 是所有标准库异常类的公共基类
  • 提供了一个虚函数: cpp virtual const char* what() const noexcept;
    • 返回一个描述错误信息的 C 风格字符串(const char*)。
    • noexcept 表示该函数自身不会抛出异常(析构/错误处理时必须保证安全)。

在这里插入图片描述

2.2 案例

#include <iostream>
#include <stdexcept>
#include <vector>

int main() 
{
    try 
    {
        std::vector<int> v;
        v.at(100); // 抛出 std::out_of_range(继承自 std::exception)
    }
    catch (const std::exception& e) {  // ← 关键:捕获基类引用
        std::cout << "Error: " << e.what() << std::endl;
    }
    // catch (...) 可作为兜底,但无法获取错误信息
}

✅ 好处:

  • 统一处理所有标准库异常(无需为每个异常写 catch)。
  • 多态调用 what():自动调用实际抛出对象的 what() 实现。
  • 避免对象切片:使用引用&)而非值传递。
  • 高效:不拷贝异常对象。

2.3 自定义异常(这块了解看看就行 看不懂也没事)

#include <stdexcept>  // 包含 std::runtime_error

// 推荐:继承标准异常子类(而不是直接继承 exception)
class MyNetworkError : public std::runtime_error
{
public:
    MyNetworkError(const std::string& msg)
        : std::runtime_error(msg) {}  // 调用基类构造
};

// 使用
void connect()
{
    throw MyNetworkError("Failed to connect to server");
}

int main() 
{
    try
    {
        connect();
    } 
    catch (const std::exception& e) 
    {
        std::cout << "Caught: " << e.what() << std::endl; // 输出自定义消息
    }
}

💡 为什么不直接继承 std::exception
因为 std::exceptionwhat() 默认只返回空字符串,你需要自己管理错误信息。
std::runtime_error / std::logic_error 已经实现了带字符串的构造和 what(),更方便,当然你要自己实现 what 也可以

what的定义方式:

#include <exception>
#include <string>

class MyCustomError : public std::exception 
{
    std::string msg_;
public:
    MyCustomError(const std::string& msg) : msg_(msg) {}

    // 必须重写 what(),因为 std::exception 的 what() 只返回空字符串
    const char* what() const noexcept override 
    {
        return msg_.c_str();
    }
};
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐