非法内存访问(空指针解引用、越界写、double-free、use-after-free)不是 C++ 异常,而是未定义行为 + 硬件/操作系统级错误。C++ 的 try/catch 只处理“语言层异常”,不处理“进程级崩溃”。

try {
    int* p = nullptr;
    *p = 42;
} catch (...) {
    // 不会进来
}

此段代码使用空指针非法访问内存,尽管写了try/catch,但程序还是会崩溃,不是 catch 写错了,是这套机制本来就不负责这件事。


一、两套完全独立的“异常”体系

“Exception” 这个词在 C++ 里被用了两次,但根本不是同一个东西。

1. C++ 语言异常

  • 由 throw产生

  • 由 try/catch捕获

  • 对象是 std::exception派生类

  • 发生在用户态、语言运行时层

  • 栈展开(stack unwinding)由编译器生成

  • 例:std::vector::at()越界抛 std::out_of_range

2. 硬件 / OS 异常

  • 由 CPU + MMU 触发

  • 空指针解引用、写只读页、访问未映射地址

  • Linux:内核发 SIGSEGV/ SIGBUS

  • Windows:内核抛 EXCEPTION_ACCESS_VIOLATION(0xC0000005)

  • 属于操作系统和进程之间的事

C++ 标准完全没有规定:

“访问非法地址时必须抛一个 C++ 异常。”

相反,标准说的是:

这是未定义行为(Undefined Behavior)。


二、非法内存访问到底经历了什么

以 *((int*)nullptr) = 1为例:

  1. 程序执行 mov dword ptr [0], 1

  2. CPU 发现虚拟地址 0 没有合法页表项 / 没有写权限

  3. MMU 产生页错误(page fault)

  4. 操作系统内核接管

  5. 内核判断:这不是合法缺页,是非法访问

  6. 向进程发送 SIGSEGV(Linux)或派发 SEH(Windows)

  7. 进程没有有效恢复逻辑 → 终止

关键点:

在 C++ 的 try块执行到那一行时,控制权根本没经过 C++ 异常系统。

它是被“硬件中断 → 内核 → 信号/SEH”这条线打断的。

try/catch只在 C++ 抽象机里生效;

页错误发生在 C++ 抽象机底下。


三、为什么 C++ 标准不把它变成异常

这是设计哲学问题,不是实现偷懒。

1. “不为不用的东西付钱”

如果每次指针解引用都要先问一句:

这个地址合法吗?

代价极高:

  • 每次读写都加边界检查

  • 失去裸指针性能优势

  • 和 C ABI 不兼容

  • 嵌入式 / 游戏 / HFT / 操作系统内核都没法用

C++ 的底线是:

你写 *p,编译器就假设 p合法;不合法就是你自己砸自己脚。

2. 非法内存访问往往意味着状态已腐败

段错误常常不是“单独一条指令错了”,而是:

  • 指针早就被写飞了

  • 栈被踩了

  • vtable 坏了

  • heap metadata 被覆盖了

这时候就算“捕获”了,程序也已经不可信:

  • 析构函数可能再踩一次内存

  • 锁状态可能损坏

  • 对象生命周期已经乱套

所以标准不鼓励“捕获后继续跑”。

3. C++ 没有托管运行时

Java 有 JVM:

  • 所有对象由 GC 管

  • 所有数组访问由 VM 检查

  • 空指针 → NullPointerException

C++ 没有 VM:

  • 直接跑在裸指令上

  • 数组就是一段内存

  • 指针就是个整数

  • 编译器不保证“访问前先检查”


四、那为什么 Windows 上有时“好像能捕”?

因为 MSVC 有 SEH(Structured Exception Handling):

__try {
    *((int*)nullptr) = 1;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
    // 能进来,但这是 SEH,不是 C++ 异常
}

或者开 /EHa+ _set_se_translator,把 SEH 转成 C++ 异常。

但注意:

  • 这是 Microsoft 扩展

  • 不是标准 C++

  • 捕获后继续跑依然危险

  • Release 下栈/状态可能已坏

Linux 上对应的是:

signal(SIGSEGV, handler);
// 或
sigaction(SIGSEGV, ...);

但 POSIX 明确态度:

捕获 SIGSEGV 通常用于“写崩溃日志”,不是用来恢复业务。

你用 setjmp/longjmp跳回去,下一条指令还可能再触发 SIGSEGV。


五、为什么 catch (...)也不是万能兜底

catch (...)捕获的是:

  • C++ 异常对象

它不捕获:

  • SIGSEGV

  • SIGFPE

  • SIGILL

  • SIGBUS

  • Windows SEH(默认 /EHsc下)

  • std::terminate前的栈展开失败

  • 栈溢出(经常连栈都没地方展开)

尤其栈溢出很阴:

栈都坏了,C++ 异常处理本身就需要栈,谁来救谁?


六、正确认知:段错误不是“错误”,是“死刑判决”

层级

机制

是否能恢复

C++ 语言层

throw / try / catch

可以

OS 信号层

SIGSEGV / SIGBUS

通常只记录,不恢复

Windows 系统层

SEH

能拦截,但恢复风险极高

硬件层

MMU / page fault

没有“异常”概念

所以工程上正确做法是:

  • 用智能指针代替裸指针

  • 用 std::vector::at()做边界检查的关键路径

  • 用 std::span传递边界信息

  • 用 AddressSanitizer / UBSan 在测试期抓 UB

  • 用 crash handler 写 minidump / backtrace

  • 用子进程 / 沙箱跑不可信代码

而不是:

try {
    do_risky_pointer_stuff();
} catch (...) {
    // 以为自己救回来了
}

七、一句话总结

  • 非法内存访问 = 未定义行为 + 硬件页错误

  • C++ 异常 = 语言层控制流

  • SIGSEGV / Access Violation = 操作系统对进程的强制干预

  • try/catch活在 C++ 抽象机里,段错误发生在抽象机底下

  • 标准不捕获,是因为:捕获了也不安全,检查了太慢,恢复了这个进程已经不可信

C++ 的哲学不是“别让它崩”,而是“别写出会让它崩的代码”。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐