UB 的真正危险在于行为脱离标准约束,导致编译器、平台或优化级别不同而结果迥异,越“稳定”的表现越易被误判为正确。

访问已释放的内存(use-after-free)
这是最典型的 UB 之一,程序可能看似正常运行,也可能随机崩溃、读到垃圾值,甚至被利用触发远程代码执行。
常见场景包括:用 delete 或 delete[] 释放指针后继续解引用;容器(如 std::vector)扩容导致迭代器/指针失效后仍使用;返回局部对象地址并后续访问。
-
std::vector的push_back可能重分配内存,使原有&v[0]或迭代器立即失效 - 不要写
int* p = new int(42); delete p; return *p;—— 即使它在某次测试中输出 42,也不代表安全 - UB 不保证崩溃,所以靠“没崩”来验证逻辑是危险的
有符号整数溢出(signed integer overflow)
C++ 标准明确将 int、long 等有符号类型溢出定义为 UB,编译器可基于“它不会发生”做激进优化,导致逻辑被意外删减。
例如,循环条件 i 中若 i 是 int 且持续递增,编译器可能假设 i 永远不会溢出,从而移除边界检查或整个循环。
立即学习“C++免费学习笔记(深入)”;
- 用
unsigned int替代(其溢出是定义良好的模运算),但需确认语义允许回绕 - 对关键计算用
std::add_overflow(C++23)或手动检查,如:if (a > INT_MAX - b) /* 处理溢出 */ - Clang/GCC 加
-fsanitize=signed-integer-overflow可在运行时捕获这类 UB
未初始化变量读取(reading uninitialized memory)
读取未初始化的栈变量(如 int x; 后直接用 x)、未初始化的类成员、或 malloc 返回但未 memset 的内存,都属 UB。
它不等于“读到零”或“读到随机数”——编译器可假设该变量“不存在合理值”,进而优化掉依赖它的分支。例如:
int foo() {
int x;
if (x > 0) return 1; // 编译器可能直接删掉这个 if,因为 x 未定义
return 0;
}- 始终显式初始化:用
= {}、= 0或构造函数初始化 - 避免裸
malloc;优先用std::vector、std::unique_ptr等 RAII 容器 - 启用
-Wuninitialized和-Wmaybe-uninitialized(GCC/Clang)
违反严格别名规则(strict aliasing violation)
当通过非兼容类型指针访问同一块内存时触发,比如用 float* 强制 reinterpret 一个 int 变量,或用 char* 以外的类型绕过类型系统读写。
编译器依赖该规则做寄存器复用和指令重排,UB 可能导致读到旧值、写入被忽略,或产生不可预测的汇编结果。
- 正确方式是用
std::memcpy或std::bit_cast(C++20)实现类型双关 - 禁止写类似
*(float*)&i = 3.14f;的代码(即使它在某些平台“工作”) -
union在 C++17 前用于类型双关也是 UB;C++17 起仅允许访问最后写入的成员
UB 的真正危险不在崩溃,而在它让程序行为脱离标准约束——编译器、平台、甚至同一编译器的不同优化级别,都可能给出完全不同的结果。越“稳定”的 UB 表现,越容易让人误以为代码正确。











