【C++进阶】异常

目录

[1 为什么需要异常?C 语言错误处理的痛点](#1 为什么需要异常?C 语言错误处理的痛点)

[2 throw try catch 基础语法](#2 throw try catch 基础语法)

throw:抛出异常

try:受监控代码块

catch:捕获处理异常

[3 栈展开(stack‑unwind)核心原理](#3 栈展开(stack‑unwind)核心原理)

[4 catch 异常匹配规则](#4 catch 异常匹配规则)

[5 工程实践:自定义异常继承体系](#5 工程实践:自定义异常继承体系)

[6 异常重新抛出 throw;](#6 异常重新抛出 throw;)

[7 异常安全与资源泄漏问题](#7 异常安全与资源泄漏问题)

示例:内存泄漏场景

[8 noexcept 异常说明(C++11)](#8 noexcept 异常说明(C++11))

[C++98 旧异常说明(已经废弃)](#C++98 旧异常说明(已经废弃))

[C++11 noexcept](#C++11 noexcept)

[9 C++ 标准库异常继承体系](#9 C++ 标准库异常继承体系)

[10 面试高频问答总结](#10 面试高频问答总结)

Q1:什么叫栈展开?

Q2:捕获异常为什么要用引用?传值捕获会发生什么?

[Q3:重新抛出异常throw;和throw e;区别?](#Q3:重新抛出异常throw;和throw e;区别?)

Q4:异常带来什么安全问题?怎么解决?

[Q5:noexcept 是做什么的,编译器会检查函数会不会抛异常吗?](#Q5:noexcept 是做什么的,编译器会检查函数会不会抛异常吗?)

[Q6:catch (...) 作用?](#Q6:catch (...) 作用?)

[Q7 C 语言错误码和 C++ 异常对比](#Q7 C 语言错误码和 C++ 异常对比)


1 为什么需要异常?C 语言错误处理的痛点

C 语言处理运行时错误,依靠返回错误码。

cpp 复制代码
// C语言风格
int Div(int a, int b, int* ret)
{
    if(b == 0)
    {
        return -1; // 返回错误码
    }
    *ret = a / b;
    return 0;
}

错误码模式的缺点:

  1. 每调用一个函数,都必须手动检查返回值,代码充斥大量if判断;
  2. 错误码只是数字,需要查表才能知道具体错误信息,携带信息有限;
  3. 多层函数调用时,每一层都要向上转发错误码,业务逻辑和错误处理代码耦合在一起;
  4. 很容易忘记判断返回值,错误被直接忽略,程序悄悄出错。

C++ 引入异常机制,核心思想:错误检测逻辑和错误处理逻辑解耦。

  • 下层函数:检测到错误,throw抛出异常对象,对象可以携带完整错误信息;
  • 上层:使用try‑catch捕获并处理异常;
  • 下层不需要关心上层如何处理错误,不需要层层转发错误码。

注意:异常不是用来处理普通逻辑错误,专门用于运行时不可预期的错误:除零、内存分配失败、网络请求失败、数据库执行失败等。

2 throw try catch 基础语法

throw:抛出异常

throw后面跟一个任意类型对象,可以是内置类型、字符串、自定义类对象。

  1. 执行throw语句之后,throw 后面的代码不会再执行;
  2. 抛出的时候会生成异常对象的拷贝。因为抛出的可能是局部对象,局部对象函数结束就销毁,拷贝一份异常对象存放在特殊区域,供 catch 捕获使用;
  3. 抛出之后程序不再顺序执行,进入栈展开流程。

try:受监控代码块

把有可能抛出异常的代码放在try{}大括号内。 如果 try 块内部代码抛出异常,就会去匹配后面的catch; 如果没有异常,所有 catch 会直接跳过。

catch:捕获处理异常

catch(类型 对象),匹配对应类型的异常对象,做错误处理。

示例:除法除零异常演示

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

double Divide(int a, int b)
{
    if(b == 0)
    {
        string err = "Divide by zero condition!";
        throw err; // 抛出string类型异常对象
    }
    return (double)a / b;
}

int main()
{
    int a, b;
    while(cin >> a >> b)
    {
        try
        {
            double ret = Divide(a,b);
            cout << "结果:" << ret << endl;
        }
        catch(const string& e)
        {
            cout << "捕获异常:" << e << endl;
        }
    }
    return 0;
}

3 栈展开(stack‑unwind)核心原理

栈展开是 C++ 异常最核心的底层机制,也是笔试面试高频考点。

函数调用链示例:main() → Func3() → Func2() → Func1() 在最内层Func1()抛出异常:

  1. 首先看throw是否处于当前函数的try块里面,如果是,尝试匹配本函数后面的catch;
  2. 如果没有匹配的 catch ,退出当前函数,当前函数栈上所有局部对象自动调用析构函数销毁;
  3. 回到上层调用函数,继续查找try‑catch;重复上面的流程;
  4. 一层一层向外回溯,如果回溯到main函数,仍然找不到任何匹配的 catch;程序调用库函数 std::terminate(),直接终止整个进程。

关键点:抛出异常向外回溯的时候,每退出一层函数,该函数栈上的局部对象全部析构。 只有堆上 new 出来的对象不会自动释放,这就是异常安全资源泄漏问题的来源。

流程图文字描述: Func1 throw异常 → 本函数无匹配 catch,销毁 Func1 局部对象,回到 Func2 → Func2 无匹配 catch,销毁 Func2 局部对象,回到 Func3 → Func3 无匹配 catch,销毁 Func3 局部对象,回到 main → main 如果依然没有匹配 catch → terminate () 程序崩溃。

4 catch 异常匹配规则

  1. 优先找距离 throw 抛出点最近、类型匹配的 catch 分支;
  2. 允许的隐式转换(只有下面几种允许,不会做任意类型转换)
    • 非 const 对象 → const 引用(权限收缩)
    • 数组 → 数组元素指针;函数 → 函数指针
    • 派生类对象 → 基类引用(大型项目自定义异常体系依靠这条规则)
  3. catch(...):捕获任意所有类型异常,无法拿到异常对象内容,一般写在所有 catch 最后,做兜底,防止程序直接 terminate 崩溃。

注意:值捕获异常对象会发生对象切片,工程中一律使用引用捕获 catch(const BaseException& e)。

错误示范:值捕获,派生类信息被切片丢失

cpp 复制代码
// 不推荐,切片!
catch(Exception e)
{
    e.what();
}

推荐写法,引用捕获,保留派生类完整信息,支持多态:

cpp 复制代码
catch(const Exception& e)
{
    e.what();
}

5 工程实践:自定义异常继承体系

大型后端项目,会设计一套统一的异常继承体系:

  1. 一个异常基类 ,提供虚函数what()返回错误描述,getid()获取错误码;
  2. 各个业务模块派生子类:数据库异常SqlException、缓存异常CacheException、网络异常HttpException;
  3. 上层捕获基类引用,利用多态自动调用子类重写的 what (),统一打印日志。

完整可运行示例:

cpp 复制代码
#include<iostream>
#include<string>
#include<cstdlib>
#include<ctime>
#include<thread>
#include<chrono>
using namespace std;

// 异常基类
class Exception
{
public:
    Exception(const string& errmsg, int id)
        :_errmsg(errmsg),_id(id)
    {}
    virtual string what() const
    {
        return _errmsg;
    }
    int getid() const
    {
        return _id;
    }
protected:
    string _errmsg;
    int _id;
};

// 数据库异常
class SqlException : public Exception
{
public:
    SqlException(const string& errmsg, int id, const string& sql)
        :Exception(errmsg,id),_sql(sql)
    {}
    virtual string what() const override
    {
        string str = "SqlException:";
        str += _errmsg;
        str += " SQL语句:";
        str += _sql;
        return str;
    }
private:
    const string _sql;
};

//缓存异常
class CacheException : public Exception
{
public:
    CacheException(const string& errmsg, int id)
        :Exception(errmsg,id)
    {}
    virtual string what() const override
    {
        string str = "CacheException:";
        str += _errmsg;
        return str;
    }
};

//http网络异常
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;
};

void SQLMgr()
{
    if(rand()%7 == 0)
    {
        throw SqlException("权限不足",100,"select * from name = '张三'");
    }
    cout << "SQLMgr 调用成功" << endl;
}

void CacheMgr()
{
    if(rand()%5 ==0)
    {
        throw CacheException("权限不足",100);
    }
    else if(rand()%6 ==0)
    {
        throw CacheException("数据不存在",101);
    }
    cout << "CacheMgr 调用成功" << endl;
    SQLMgr();
}

void HttpServer()
{
    if(rand()%3 ==0)
    {
        throw HttpException("请求资源不存在",100,"get");
    }
    else if(rand()%4 ==0)
    {
        throw HttpException("权限不足",101,"post");
    }
    cout << "HttpServer调用成功" << endl;
    CacheMgr();
}

int main()
{
    srand((unsigned int)time(nullptr));
    while(true)
    {
        this_thread::sleep_for(chrono::seconds(1));
        try
        {
            HttpServer();
        }
        //捕获基类引用,多态,所有派生异常全部可以捕获
        catch(const Exception& e)
        {
            cout << e.what() << endl;
        }
        catch(...)
        {
            cout << "Unknown Exception" << endl;
        }
    }
    return 0;
}

6 异常重新抛出 throw;

业务场景:本层只能处理一部分异常,剩下异常自己处理不了,需要交给上层调用者。

⚠重点语法:catch 块里面单独写 throw;,不要写对象,裸 throw 。 ❌禁止写throw e;,会发生对象切片,丢失派生类的子类信息。 ✅裸throw;:原样重新抛出原来捕获的异常对象。

示例场景:发送网络消息,102 错误代表网络波动,本层可以重试 3 次;其他错误不能重试,直接重新抛给上层。

cpp 复制代码
void _SendMsg(const string& s)
{
    if(rand()%2 == 0)
    {
        throw HttpException("网络不稳定,发送失败",102,"put");
    }
    else if(rand()%7 ==0)
    {
        throw HttpException("你已经不是好友,发送失败",103,"put");
    }
    cout << "发送成功" << endl;
}

void SendMsg(const string& s)
{
    //最多重试3次
    for(size_t i = 0; i <4; i++)
    {
        try
        {
            _SendMsg(s);
            break; //发送成功,跳出循环
        }
        catch(const Exception& e)
        {
            //网络波动错误,可以重试
            if(e.getid() == 102)
            {
                if(i == 3)
                {
                    //重试耗尽,重新抛出给外层
                    throw;
                }
                cout << "第" << i+1 << "次重试" << endl;
            }
            else
            {
                //其他错误,本层处理不了,直接重抛
                throw;
            }
        }
    }
}

int main()
{
    srand((unsigned int)time(nullptr));
    string str;
    while(cin >> str)
    {
        try
        {
            SendMsg(str);
        }
        catch(const Exception& e)
        {
            cout << e.what() << "\n";
        }
        catch(...)
        {
            cout << "Unknown Exception\n";
        }
    }
    return 0;
}

7 异常安全与资源泄漏问题

异常会打断代码顺序执行流:如果函数中间抛出异常,函数提前退出。

  • 栈对象:栈展开自动析构,不会泄漏;
  • 堆 new 出来内存、锁、文件句柄,不会自动释放,会资源泄漏。

示例:内存泄漏场景

复制代码
void Func()
{
    int* arr = new int[10]; //堆内存
    int a,b;
    cin >> a >> b;
    double ret = Divide(a,b); //这里可能抛异常!
    cout << ret << endl;
    delete[] arr; // 如果抛异常,这行永远不会执行!内存泄漏!
}

两种解决方案:

  1. catch 捕获异常,释放资源之后再裸 throw; 重新抛出异常交给上层。
cpp 复制代码
void Func()
{
    int* arr = new int[10];
    int a,b;
    cin >> a >> b;
    try
    {
        double ret = Divide(a,b);
        cout << ret << endl;
    }
    catch(...)
    {
        delete[] arr;
        throw; //释放完资源,继续把异常向外抛
    }
    delete[] arr;
}
  1. 工程推荐方案:RAII(资源获取即初始化),智能指针。

用对象管理资源,栈展开的时候,对象析构自动释放内存、锁。不需要手动在 catch 写释放逻辑。
重要编码规范:不要让异常逃离析构函数 (Effective C++ 条款 8) 如果析构函数内部抛出异常,此时程序已经处于栈展开流程,会直接触发terminate(),程序直接崩溃。析构函数内部捕获处理全部异常,不要向外抛。

8 noexcept 异常说明(C++11)

C++98 旧异常说明(已经废弃)

cpp 复制代码
// 只会抛出bad_alloc异常
void* operator new(size_t size) throw(std::bad_alloc);
// 不会抛出任何异常
void* operator delete(size_t size,void* ptr) throw();

这套语法实践中实用性很差,编译器不强制校验,C++11 被废弃。

C++11 noexcept

  1. void func() noexcept;
    • 声明这个函数承诺不会抛出异常;
    • ⚠只是编译期声明,编译器不会强制检查函数内部代码;
    • 如果noexcept修饰的函数内部真的抛出异常,程序直接调用terminate()终止。
  2. noexcept(表达式) 运算符:编译期判断表达式是否可能抛出异常,返回 bool。
cpp 复制代码
#include<iostream>
using namespace std;

double Divide(int a,int b) noexcept
{
    if(b ==0)
    {
        throw "除零错误"; // 声明noexcept还抛异常,运行直接terminate
    }
    return (double)a/b;
}

int main()
{
    cout << noexcept(Divide(1,2)) << endl; //0,该表达式可能抛异常
    cout << noexcept(1+2) << endl; //1,不会抛异常
    return 0;
}

STL 容器大量接口使用noexcept修饰,比如size() begin() end()。

9 C++ 标准库异常继承体系

C++ 标准库所有异常全部继承基类 std::exception。 虚函数:virtual const char* what() const noexcept; 返回异常信息字符串。

两大分支:

  1. std::logic_error:逻辑错误,属于程序 bug,理论写代码阶段可以避免。
    • invalid_argument:无效参数
    • length_error:容器长度超限
    • out_of_range:下标越界(vector.at () string.at () 抛出)
  2. std::runtime_error:真正运行时才发生,无法提前预判。
    • overflow_error溢出、range_error范围错误

独立异常子类:

  • std::bad_alloc:new 分配内存失败抛出
  • std::bad_cast:dynamic_cast 向下转换引用失败
  • std::bad_typeid
  • std::bad_exception

开发中捕获标准库异常,写法:

cpp 复制代码
catch(const std::exception& e)
{
    cout << e.what() << endl;
}

10 面试高频问答总结

Q1:什么叫栈展开?

throw 抛出异常之后,停止顺序执行,逐层退出函数调用栈,每一层函数栈局部对象析构,一层层向外寻找匹配的 catch。直到 main 函数还找不到匹配 catch,调用 terminate 终止程序。

Q2:捕获异常为什么要用引用?传值捕获会发生什么?

如果传值捕获,会发生对象切片,派生类独有的成员被切掉,多态失效,丢失真实异常信息。引用捕获保留原始异常对象完整信息,支持多态。

Q3:重新抛出异常throw;和throw e;区别?

throw;裸抛:原样把捕获到的原始异常重新抛出,不会切片。 throw e;会拷贝生成局部对象,发生切片,丢失派生类信息,业务禁止这么写。

Q4:异常带来什么安全问题?怎么解决?

异常打断执行流,如果中间有堆内存、锁没有释放,会资源泄漏。 方案 1:catch 捕获,释放资源,再 throw 重抛; 方案 2:优先 RAII,对象生命周期管理资源,栈展开自动调用析构释放。 析构函数不要向外抛出异常,否则会直接 terminate 崩溃。

Q5:noexcept 是做什么的,编译器会检查函数会不会抛异常吗?

noexcept 用来声明函数承诺不抛出异常;编译器编译阶段不强制检查;如果 noexcept 函数内部抛出异常,程序直接 terminate。

Q6:catch (...) 作用?

捕获任意类型的异常,但是拿不到异常对象信息;一般放在所有 catch 最后,做兜底保护,防止程序直接终止。

Q7 C 语言错误码和 C++ 异常对比

错误码:每处调用都要判断返回值,错误信息携带弱,容易被忽略; 异常:错误检测与处理分离,异常对象可以携带完整错误信息;但是会有运行时代价,不能滥用,只用于运行时错误。
补充:异常不适合嵌入式、对运行时间极度敏感场景。


拓展思考:异常开销:抛出异常路径会生成异常表;正常无异常的路径几乎没有性能损耗,只有真正抛出异常的时候才有开销。

相关推荐
朝朝辞暮i2 小时前
C++ 第 15 课:指针 * 和地址 &
开发语言·c++
奶茶树2 小时前
【C++】14. C++11新特性【下】
开发语言·c++
web打印社区2 小时前
汽修 / 4S 店:维修工单、结算单网页怎么静默出纸
开发语言·前端·javascript·chrome·ecmascript
Sirens.2 小时前
Java多线程实例:单例模式、阻塞队列、线程池与定时器
java·开发语言·单例模式
牢姐与蒯2 小时前
c++加餐之位图
c++·算法·哈希
沫璃染墨2 小时前
《从零入门Linux系统篇(五十三):线程篇·六——线程互斥详解:从并发问题到mutex互斥锁》
linux·运维·服务器·开发语言·后端·架构·系统架构
老王爱玩车2 小时前
深入理解指针6
c语言·开发语言·学习
苏辰澈3 小时前
C++自己的族谱规则:继承详解上
c语言·c++·visualstudio
Eloudy3 小时前
全文 - version.A - AMBA CHI Chip-to-Chip(C2C)
java·开发语言·数据库·gpu·chiplet