空指针调用虚函数直接触发未定义行为,因其需先解引用空指针获取_vptr以查找vtable,而c++标准禁止解引用空指针;编译器可假设指针非空并优化掉防护逻辑,导致崩溃、静默错误或随机行为。

空指针调用虚函数直接触发未定义行为
因为标准明确禁止解引用空指针,而虚函数调用必须通过对象的 _vptr 查找虚函数表(vtable),这本质上就是一次隐式解引用。只要指针是 nullptr,就不存在对象,也就没有 _vptr 可读——这一步本身已是未定义行为(UB),后续任何表现(崩溃、静默跳过、随机跳转)都合法。
为什么不是“抛异常”或“返回错误码”
C++ 标准不强制要求运行时检查空指针,编译器甚至可以假设“虚函数调用前指针必非空”,从而删掉防护逻辑。常见现象包括:
- 在
-O2下,if (ptr) ptr->show();可能被优化成无条件调用ptr->show(),因为编译器认为ptr不可能为空(否则就是 UB,而 UB 允许被忽略) - 某些平台会立即触发
SIGSEGV(段错误),但另一些平台可能只是读到内存地址 0 处的垃圾值,误跳到非法指令地址,导致不可预测崩溃 - 调试器里看到“访问了 0x0000000000000000”,但这个地址不一定是崩溃点——它只是第一次尝试读
_vptr的位置
和普通成员函数调用的区别在哪
普通非虚函数(如 void foo() { ... })若声明为 static,可直接通过类名调用,不依赖对象;但即使是非静态普通成员函数,obj_ptr->foo() 仍需访问 this,而 this == nullptr 时仍是 UB —— 只不过虚函数多了一层间接:它必须先取 this->_vptr,再取 vtable[0],再跳转。多这一级间接,就多一次对空地址的读操作,风险更暴露。
关键差异在于:
-
obj_ptr->non_virtual_func():UB 发生在函数体内访问this->member时(可能晚于入口) -
obj_ptr->virtual_func():UB 几乎必然发生在函数调用的第一条指令——取_vptr时就崩了
容易被忽略的隐式空指针场景
真正危险的不是明晃晃的 Base* p = nullptr; p->f();,而是那些看起来“应该不为空”的地方:
-
dynamic_cast失败后返回nullptr,但后续没检查就直接调用虚函数 - 容器查找(如
map::find)返回end()迭代器,解引用前未判有效 - 智能指针
std::shared_ptr<base> p;默认构造即为空,p->f()同样 UB - 多线程中,一个线程刚
reset()智能指针,另一个线程恰好执行虚函数调用——即使加了锁,也可能因编译器重排或 CPU 缓存未同步而读到已失效的_vptr
这些都不是语法错误,编译器不会报错,ASan 也未必能捕获(尤其涉及寄存器缓存或跨线程时)。最稳妥的做法,是在每次虚函数调用前确保指针非空——不是靠“我觉得它不会空”,而是靠显式判断或 RAII 约束。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











