乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • C++ 学习路径
  • 第1章 C++概述与开发环境

    • C++ 语言概述与编译模型
    • 第一个 C++ 程序与规范
    • 开发环境配置
    • 预处理指令详解
  • 第2章 基本语法与类型系统

    • 变量与基本类型
    • 类型转换
    • 枚举类型
    • 引用与指针
    • 数组与 stdarray
    • 字符串与原始字符串字面量
    • const 与 constexpr
    • nullptr 与空指针
    • 类型推导 auto 与 decltype
    • 基于范围的 for 循环
    • 值类别全面解析
    • char16_t 与 char32_t
    • static_assert 编译期断言
  • 第3章 函数与重载

    • 函数声明与定义
    • 函数重载
    • 默认参数与内联函数
    • Lambda 表达式
    • 函数对象与 stdfunction
    • 后置返回类型与 noexcept
  • 第4章 类与对象

    • 类的基本定义
    • 构造函数与析构函数
    • 拷贝控制
    • 移动构造函数与移动赋值
    • 列表初始化与类内初始化器
    • 静态成员与嵌套类
    • 友元
    • =default 与 =delete
  • 第5章 继承与多态

    • 继承基础
    • 虚函数与多态
    • 虚函数表与动态绑定原理
    • 虚析构函数
    • 抽象类与纯虚函数
    • 多重继承与虚继承
    • 继承构造函数
  • 第6章 运算符重载

    • 运算符重载基础
    • 算术与关系运算符重载
    • 赋值与移动运算符重载
    • 特殊运算符重载
  • 第7章 模板与泛型编程

    • 函数模板
    • 类模板
    • 模板特化与偏特化
    • 可变参数模板
    • 别名模板与模板模板参数
    • SFINAE 与类型萃取
    • 依赖名与 typename/template 关键字
  • 第8章 异常处理

    • 异常处理机制
    • noexcept 与异常安全
    • RAII 与异常安全实践
  • 第9章 内存管理与智能指针

    • 动态内存与内存分区
    • RAII 惯用法
    • unique_ptr
    • shared_ptr 与 weak_ptr
    • 内存管理最佳实践
  • 第10章 右值引用与移动语义

    • 右值引用与值类别深度解析
    • std::move 原理与使用
    • 完美转发与 std::forward
    • 移动语义性能对比与最佳实践
  • 第11章 STL容器

    • vector 深度剖析
    • deque 内部机制
    • list 与 forward_list
    • map 与 set 深度解析
    • unordered 容器与哈希原理
    • array 与 tuple
    • 容器适配器
    • 容器选择全景指南
  • 第12章 STL算法与迭代器

    • 迭代器体系全解
    • 非变异算法
    • 变异算法
    • 排序与二分算法
    • Lambda 与算法组合
    • std::random 随机数库
    • 自定义迭代器开发
  • 第13章 IO流与文件

    • 标准 IO 流
    • 格式化输出控制
    • 文件流操作
    • 字符串流
    • std::regex 正则表达式
  • 第14章 并发与多线程

    • thread 基础与线程管理
    • mutex 与 lock_guard
    • unique_lock 与 condition_variable
    • thread_local 线程局部存储
    • atomic 与内存序
    • future 与 async 异步编程
    • std::chrono 时间库
  • 第15章 现代C++新特性

    • 从 C++11 到 C++20 演进路线
    • C++14 关键新特性
    • C++17 关键新特性
    • C++20 核心特性速览
  • 第16章 面试考点与最佳实践

    • C++ 综合最佳实践清单
    • 高频面试题精讲
    • 多线程面试题与实战
    • 内存管理常见陷阱与排查
  • 附录

    • C++ 核心知识点
    • C++ 专业术语

异常处理机制

定义与作用

异常处理是 C++ 的错误传播机制——当函数无法正常完成任务时,throw 一个异常对象,沿调用栈向上传播,直到被匹配的 catch 块捕获:

try {
    int result = divide(10, 0);  // 可能抛出异常
} catch (const std::exception& e) {
    std::cerr << "错误: " << e.what() << std::endl;
}

相比 C 风格的错误码返回,异常处理的优势:

  • 错误无法被忽略(不捕获就 terminate)
  • 将正常逻辑和错误处理分离
  • 支持跨多层函数调用传播

核心原理

栈展开(Stack Unwinding)

std::exception 体系

完整示例

示例一:学生成绩管理系统的异常处理

场景说明:学生成绩录入和管理系统,对非法输入、空数据、越界等情况抛出不同类型异常。

#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
#include <numeric>
#include <algorithm>
#include <map>

class ScoreDatabase {
public:
    void addScore(const std::string& student, int score) {
        if (score < 0 || score > 100)
            throw std::invalid_argument(
                "分数必须在 0~100 之间,实际值: " + std::to_string(score));

        if (student.empty())
            throw std::invalid_argument("学生姓名不能为空");

        scores_[student].push_back(score);
        std::cout << "  添加: " << student << " = " << score << std::endl;
    }

    double average(const std::string& student) const {
        auto it = scores_.find(student);
        if (it == scores_.end())
            throw std::out_of_range("未找到学生: " + student);

        const auto& scores = it->second;
        if (scores.empty())
            throw std::runtime_error(student + " 尚无成绩记录");

        double sum = std::accumulate(scores.begin(), scores.end(), 0.0);
        return sum / scores.size();
    }

    int highest(const std::string& student) const {
        auto it = scores_.find(student);
        if (it == scores_.end())
            throw std::out_of_range("未找到学生: " + student);

        const auto& scores = it->second;
        if (scores.empty())
            throw std::runtime_error(student + " 尚无成绩记录");

        return *std::max_element(scores.begin(), scores.end());
    }

    void printAll() const {
        if (scores_.empty()) {
            std::cout << "  数据库中无数据" << std::endl;
            return;
        }
        for (const auto& [name, scores] : scores_) {
            std::cout << "  " << name << ": ";
            for (size_t i = 0; i < scores.size(); ++i) {
                if (i > 0) std::cout << ", ";
                std::cout << scores[i];
            }
            std::cout << " (均分: " << average(name) << ")" << std::endl;
        }
    }

private:
    std::map<std::string, std::vector<int>> scores_;
};

int main() {
    ScoreDatabase db;

    std::cout << "===== 正常录入 =====" << std::endl;
    try {
        db.addScore("张三", 85);
        db.addScore("张三", 92);
        db.addScore("李四", 78);
        db.addScore("李四", 88);
        db.addScore("李四", 95);
    } catch (const std::exception& e) {
        std::cerr << "添加失败: " << e.what() << std::endl;
    }

    std::cout << "\n===== 查询成绩 =====" << std::endl;
    db.printAll();

    std::cout << "\n===== 异常场景 =====" << std::endl;

    // 场景 1:非法分数
    try {
        db.addScore("王五", 150);
    } catch (const std::invalid_argument& e) {
        std::cerr << "非法参数: " << e.what() << std::endl;
    }

    // 场景 2:查询不存在的学生
    try {
        db.average("赵六");
    } catch (const std::out_of_range& e) {
        std::cerr << "越界: " << e.what() << std::endl;
    }

    // 场景 3:统捕获所有标准异常
    try {
        std::vector<int> v;
        v.at(100);  // 抛出 std::out_of_range
    } catch (const std::logic_error& e) {
        std::cerr << "逻辑错误: " << e.what() << std::endl;
    } catch (const std::runtime_error& e) {
        std::cerr << "运行时错误: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "其他异常: " << e.what() << std::endl;
    }

    return 0;
}

预期输出:

===== 正常录入 =====
  添加: 张三 = 85
  添加: 张三 = 92
  添加: 李四 = 78
  添加: 李四 = 88
  添加: 李四 = 95

===== 查询成绩 =====
  张三: 85, 92 (均分: 88.5)
  李四: 78, 88, 95 (均分: 87)

===== 异常场景 =====
非法参数: 分数必须在 0~100 之间,实际值: 150
越界: 未找到学生: 赵六
逻辑错误: invalid vector subscript

逐段分析:

  • throw 抛出异常对象——编译器根据静态类型创建副本
  • catch (const std::invalid_argument& e) 按异常类型匹配——推荐用 const 引用避免切片
  • 多个 catch 块按书写顺序匹配——派生类异常必须写在基类异常前面
  • 栈展开保证局部对象正确析构——RAII 资源自动释放
  • std::exception::what() 返回描述信息的 C 字符串

示例二:文件处理中的异常安全

#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
#include <vector>
#include <sstream>

class ConfigFile {
public:
    explicit ConfigFile(const std::string& path) {
        std::ifstream file(path);
        if (!file.is_open())
            throw std::runtime_error("无法打开配置文件: " + path);

        std::string line;
        int lineNum = 0;
        while (std::getline(file, line)) {
            ++lineNum;
            if (line.empty() || line[0] == '#') continue;  // 跳空行和注释

            auto pos = line.find('=');
            if (pos == std::string::npos) {
                file.close();
                throw std::runtime_error(
                    "配置文件格式错误 (第" + std::to_string(lineNum) + "行)");
            }

            std::string key = trim(line.substr(0, pos));
            std::string value = trim(line.substr(pos + 1));

            if (key.empty())
                throw std::invalid_argument("第" + std::to_string(lineNum) + "行: 键名为空");

            entries_.push_back({key, value});
        }
    }

    std::string get(const std::string& key) const {
        for (const auto& [k, v] : entries_) {
            if (k == key) return v;
        }
        throw std::out_of_range("配置项不存在: " + key);
    }

    void print() const {
        for (const auto& [k, v] : entries_)
            std::cout << "  " << k << " = " << v << std::endl;
    }

private:
    static std::string trim(const std::string& s) {
        auto start = s.find_first_not_of(" \t");
        if (start == std::string::npos) return "";
        auto end = s.find_last_not_of(" \t");
        return s.substr(start, end - start + 1);
    }

    std::vector<std::pair<std::string, std::string>> entries_;
};

int main() {
    std::cout << "===== 读取正常配置 =====" << std::endl;
    try {
        // 先创建一个测试配置文件
        {
            std::ofstream out("test_config.ini");
            out << "# 服务器配置\n";
            out << "host = 127.0.0.1\n";
            out << "port = 8080\n";
            out << "timeout = 30\n";
        }

        ConfigFile cfg("test_config.ini");
        cfg.print();

        std::cout << "host: " << cfg.get("host") << std::endl;
        std::cout << "port: " << cfg.get("port") << std::endl;

    } catch (const std::exception& e) {
        std::cerr << "配置错误: " << e.what() << std::endl;
    }

    std::cout << "\n===== 异常场景 =====" << std::endl;

    // 场景 1:文件不存在
    try {
        ConfigFile cfg("nonexistent.ini");
    } catch (const std::exception& e) {
        std::cerr << "捕获: " << e.what() << std::endl;
    }

    // 场景 2:查询不存在的键
    try {
        ConfigFile cfg("test_config.ini");
        std::cout << cfg.get("username") << std::endl;
    } catch (const std::out_of_range& e) {
        std::cerr << "捕获: " << e.what() << std::endl;
    }

    return 0;
}

预期输出:

===== 读取正常配置 =====
  host = 127.0.0.1
  port = 8080
  timeout = 30
host: 127.0.0.1
port: 8080

===== 异常场景 =====
捕获: 无法打开配置文件: nonexistent.ini
捕获: 配置项不存在: username

易错场景与面试考点

易错场景

场景错误表现正确做法
按值捕获异常对象切片(派生类信息丢失)用 const 引用捕获
派生类异常写在基类之后派生类异常永远不会被匹配派生类 catch 在前
析构函数中抛出异常std::terminate 调用析构函数必须 noexcept
构造函数中抛出异常已构造的成员未析构已构造的成员自动析构,但裸指针管理的资源需手动处理
异常对象用原生指针内存泄漏throw 值对象,catch const 引用

常见面试问题

  1. 异常处理的栈展开过程?——从 throw 点到匹配 catch 之间,依次销毁所有局部自动对象(RAII),直到找到匹配的 catch 块。如果没找到 catch,调用 std::terminate。

  2. 为什么 catch 块中应该用 const 引用?——(1) 避免异常对象的拷贝;(2) 如果按值捕获,派生类异常对象会被切片成基类对象,丢失派生类信息。

  3. 构造函数中可以抛出异常吗?析构函数呢?——构造函数可以抛出异常,已成功构造的成员和基类会自动析构。析构函数不应该抛出异常(C++11 起默认 noexcept),否则在栈展开时析构函数抛异常会导致 std::terminate。

  4. std::exception::what() 的作用?——返回异常描述的 C 风格字符串。自定义异常类应 override 此方法。

  5. noexcept 标记的函数抛了异常会怎样?——直接调用 std::terminate 终止程序,不会进行栈展开。

小结

  • throw 抛出异常对象 → 栈展开 → catch 匹配类型捕获
  • 用 const 引用捕获——避免切片
  • 派生类异常 catch 块必须在基类之前
  • 析构函数默认 noexcept——绝不要在析构函数中抛异常
  • 标准异常体系:std::exception → logic_error / runtime_error → 更具体子类
下一页
noexcept 与异常安全