面试时没回答好问题再正常不过了,被面试官追问到卡壳也不必太沮丧。真正重要的是面后复盘,确保同一道题不跌倒第二次。今天就先从 C++ 异常处理机制这块高频考点入手,把该说的、该展开的、该追问的一次性理清楚。
就以海尔专业一面里那道题为例:“介绍一下 C++ 中的异常处理机制。”如果只回答“异常捕获、抛出、栈展开”,面试官大概率会继续追问细节。
简要回答
C++ 异常处理基于 try-throw-catch 机制,提供了一套结构化的错误处理方式。
try 块定义监控范围,throw 抛出异常对象,catch 捕获并处理异常。整个机制还涉及异常类型匹配、栈展开(Stack Unwinding)以及资源自动清理,通过 RAII 来保证异常安全。
详细回答
C++ 异常处理的核心组件可以分为四个部分:
-
异常抛出(throw):使用 throw 表达式抛出异常对象,可以抛出任意类型的对象——内置类型、自定义类、标准异常等。一旦抛出异常,当前函数的执行会立即终止。
-
异常捕获(catch):catch 块按顺序匹配异常类型,支持异常类型继承层次中的多态捕获,还可以使用 catch(...) 兜底捕获所有异常。
-
栈展开(Stack Unwinding):从抛出点开始向上查找匹配的 catch 块,在此过程中自动调用局部对象的析构函数,保证资源正确释放。
-
异常规格(Exception Specifications):C++11 引入了 noexcept 说明符,用来替代旧的动态异常规格,提供编译期的异常保证。
代码示例
先看一个基本的异常处理示例,覆盖自定义异常、标准异常和捕获顺序:
#include <iostream>
#include <stdexcept>
#include <string>
// 自定义异常类
class MyException : public std::exception {
private:
std::string message;
public:
MyException(const std::string& msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
};
double divide(double a, double b) {
if (b == 0) {
throw std::invalid_argument("Division by zero!");
}
return a / b;
}
void process_data(int value) {
if (value < 0) {
throw MyException("Negative value not allowed: " + std::to_string(value));
}
if (value > 100) {
throw std::out_of_range("Value too large: " + std::to_string(value));
}
std::cout << "Processing value: " << value << std::endl;
}
void basic_exception_demo() {
try {
std::cout << "10 / 2 = " << divide(10, 2) << std::endl;
std::cout << "10 / 0 = " << divide(10, 0) << std::endl; // 这里会抛出异常
}
catch (const std::invalid_argument& e) {
std::cout << "Caught invalid_argument: " << e.what() << std::endl;
}
try {
process_data(50); // 正常
process_data(-5); // 抛出 MyException
process_data(200); // 不会执行到这里
}
catch (const MyException& e) {
std::cout << "Caught MyException: " << e.what() << std::endl;
}
catch (const std::exception& e) {
std::cout << "Caught std::exception: " << e.what() << std::endl;
}
catch (...) {
std::cout << "Caught unknown exception" << std::endl;
}
}
再来看一个结合 noexcept 和 RAII 的示例,重点观察移动构造与析构函数上的异常规格:
#include <iostream>
#include <vector>
#include <stdexcept>
class NoExceptDemo {
public:
// 基本不抛出异常的函数
int get_value() const noexcept {
return 42;
}
// 可能抛出异常的函数
void risky_operation() {
throw std::runtime_error("This might fail");
}
// 移动构造函数通常标记为 noexcept
NoExceptDemo(NoExceptDemo&& other) noexcept {
std::cout << "Move constructor (noexcept)" << std::endl;
}
// 析构函数不应该抛出异常
~NoExceptDemo() noexcept {
std::cout << "Destructor (noexcept)" << std::endl;
}
};
void noexcept_usage() {
NoExceptDemo demo;
// noexcept 运算符检查表达式是否可能抛出异常
std::cout << "get_value is noexcept: " << noexcept(demo.get_value()) << std::endl;
std::cout << "risky_operation is noexcept: " << noexcept(demo.risky_operation()) << std::endl;
// 在 vector 重新分配时,noexcept 移动构造函数更高效
std::vector<NoExceptDemo> items;
items.push_back(NoExceptDemo()); // 优先使用移动构造
}
知识拓展
知识图解
下面这张流程图展示了从进入 try 块、抛出异常、栈展开到匹配 catch 块的完整过程:

适用场景
异常机制并非适合所有错误场景,以下情况更适合用它:
- 不可恢复错误:内存分配失败、硬件错误等
- 构造函数失败:对象构造无法完成时
- 资源获取失败:文件打开、网络连接等
- 逻辑错误:违反前置条件、无效参数等
- 跨多层调用错误处理:错误需要向上传递多级时
面试官很能追问
Q1: 异常处理相比返回错误码有什么优势?
A1: 优势主要体现在几个方面。正常逻辑和错误处理代码分离,可读性更好;异常能够自动向上传播,不需要每层都检查错误码;异常是类型安全的,编译器可以参与类型检查;未处理的异常会直接导致程序终止,错误不会被静默忽略;另外构造函数无法返回错误码,只能通过异常报告失败。
Q2: 什么是栈展开?在异常处理中起什么作用?
A2: 栈展开是从异常抛出点开始,沿着调用链向上查找匹配 catch 块的过程。在这个过程中,会自动调用局部对象的析构函数,释放自动存储期的资源,维护调用栈的完整性,并结合 RAII 保证资源不泄漏。简单说,栈展开确保了即使发生异常,程序也能正确清理资源。
Q3: 为什么应该按引用捕获异常?
A3: 按引用捕获可以避免切片问题,捕获派生类异常时保持多态性;同时避免了拷贝异常对象,性能更好;还能保持原始异常对象,便于重新抛出;与标准库异常处理方式也保持一致。
Q4: 在析构函数中抛出异常会有什么问题?
A4: 这是非常危险的。如果栈展开过程中析构函数又抛出异常,会立即调用 terminate,造成双重异常;也可能中断其他资源的清理过程,导致资源泄漏;C++ 标准规定这种情况最终会导致程序终止。最佳实践是析构函数用 noexcept 声明,并吞掉所有异常。