成员函数指针不是普通地址,而是编译器私有结构,直接转换为void*或整数属未定义行为;应使用std::function+lambda安全封装调用逻辑。

不能安全获取类成员函数指针的内存入口地址——它在标准 C++ 中根本就不是一个“地址”,而是一个编译器私有的多字段结构(如函数地址 + this 偏移 + vtable 索引),直接 reinterpret_cast 是未定义行为,多数编译器会报错或运行时崩溃。
为什么 &MyClass::func 不能当普通函数指针用
成员函数调用必须携带 this 调整逻辑,而普通函数指针不包含该语义。C++ 标准明确禁止将 T::* 类型转为整数或 void*(见 [expr.cast]),主流编译器都会拦截:
-
error: cannot convert 'void (A::*)()' to 'void*'(MSVC/Clang/GCC 通用报错) - 即使绕过编译(比如用
union或memcpy),运行时调用仍可能因this错位、跳转到 thunk 或虚表槽位而 segfault - 虚函数、多重继承、
-fPIC、LTO 优化后,底层布局随时变化,同一段“解包”代码在不同构建下结果完全不同
非虚、单继承、无 RTTI 场景下如何提取近似函数地址
仅限调试或 hook 等非生产场景,且必须严格限定平台与编译器(如 x86_64 Linux + GCC + -fno-rtti -fno-exceptions)。Itanium ABI 下,非虚成员函数指针通常为 16 字节,前 8 字节是函数地址,后 8 字节是 this 偏移(对平凡类常为 0):
- 用
memcpy拷贝到足够大的缓冲区(避免越界读):uint64_t buf[2]; memcpy(buf, &MyClass::foo, sizeof(buf)); - 若
buf[1] == 0,则buf[0]很可能是真实函数入口(可用objdump -d验证是否落在目标函数指令范围内) - 若
buf[1] != 0,说明存在this调整,此时buf[0]可能指向一个 thunk,不能直接跳转 - 此法在 MSVC x64 下完全不适用——其布局是 {addr, offset},但字节顺序和字段含义与 Itanium 不同
获取虚函数实际地址必须走 vtable 查找
对虚函数,&MyClass::virt_func 得到的只是“虚调用描述符”,不是最终代码地址。真实入口藏在对象实例的虚表中:
- 先确保对象已构造完成:
MyClass obj; - 取虚表指针:
void** vtable = *reinterpret_cast(&obj); - 假设该虚函数是类中第 2 个虚函数(索引 1),则:
uintptr_t addr = reinterpret_cast(vtable[1]); - 注意:
vtable偏移依赖编译器;Clang/GCC 通常放对象开头,MSVC 在多重继承下可能不保证;该地址仍可能指向 thunk,需反汇编确认是否为最终目标
真正该做的事:放弃“取地址”,改用语义等价的安全封装
如果你的目标是回调、跨模块传递、延迟调用或绑定对象,std::function + lambda 是唯一可移植、零成本抽象的方案:
-
std::function—— 闭包捕获f = [&w]() { return w.get_x(); }; this,内部存储完整调用上下文 - 编译器优化后,调用开销通常等于裸指针调用(内联成功时无虚函数开销)
- 完全避开 ABI 不透明性问题,不依赖任何编译器扩展或内存布局假设
- 不要试图从
std::function中反向提取“原始函数地址”——f.target<...>()对 lambda 返回nullptr,这是设计使然
所有“提取地址”的尝试,本质都是在和编译器、ABI、链接器、优化器博弈。真正容易被忽略的点是:你并不需要那个地址——你需要的是可调用性。而可调用性,std::function 已经给你了;地址,只是底层实现细节,且标准从未承诺它存在。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











