成员函数指针不能直接转 void*,否则行为未定义;其本质是编译器特定的复杂结构,含 this 偏移、虚表索引等,非纯代码地址;安全做法是用 std::mem_fn、thunk 或 lambda 封装调用。

成员函数指针不能直接转 void*,否则会崩溃或行为未定义
在 C++ 中,&ClassName::member_func 得到的不是普通函数指针,而是“成员函数指针”(ReturnType (ClassName::*)()),其大小和布局由编译器决定(MSVC 下通常是 4/8/16 字节,GCC/Clang 多为 8 或 16 字节)。直接 reinterpret_cast 成 void* 或 uintptr_t 是未定义行为——它可能包含隐式 this 调整偏移、虚表索引甚至 vcall thunk 地址,不是纯代码地址。
常见错误现象:
• 编译通过但运行时调用崩溃(尤其是含虚函数或多重继承时)
• 同一函数在不同编译器下转出的数值完全不同
• reinterpret_cast<void>(&A::foo)</void> 在 GCC 上能打印地址,但传给 dlsym / mprotect / hook 框架后调用失败
- 不要试图用
memcpy或联合体(union)暴力提取字节来“猜”入口地址 - 若目标是 hook 或动态调用,优先走标准调用路径(如封装成
std::function或用 trampoline) - 仅当调试、反汇编或与底层 ABI 交互(如手写 inline hook)时,才需深入解析其二进制表示,且必须限定编译器+ABI+类结构
MSVC 下可用 __identifier 和 sizeof 判断是否为标准调用约定的非虚函数
MSVC 提供了有限但实用的窥探手段:对无虚函数、单继承、无数据成员的简单类,若成员函数为 __thiscall(默认),其成员函数指针底层可能只存一个真实函数地址(即 sizeof 为 4 或 8)。此时可借助 undocumented 的 __get_obj_offset(不推荐)或更稳妥的「thunk 构造法」间接获取入口。
实操建议:
• 先确认类无虚表:static_assert(!std::is_polymorphic_v<classname>, "no vtable allowed");</classname>
• 再检查函数是否 non-virtual:static_assert(!std::is_virtual_base_of_v<base derived>, ...) 不够,应靠编译期断言 + 注释约束
立即学习“C++免费学习笔记(深入)”;
- 用
sizeof(&ClassName::func)辅助判断:若等于sizeof(void*),大概率是裸地址(仅限 MSVC x64 + 简单类) - 安全提取方式:声明一个同签名的自由函数指针变量,再用 memcpy 拷贝成员函数指针的字节过去(需严格对齐和大小匹配)
- 示例(MSVC only,仅作演示):
void (Test::*pmf)() = &Test::simple; void (*raw_func)(); static_assert(sizeof(pmf) == sizeof(raw_func)); memcpy(&raw_func, &pmf, sizeof(raw_func)); // raw_func 现在可能指向真实入口
GCC/Clang 下必须用 thunk 或 std::mem_fn 绕过 ABI 黑盒
Clang/GCC 使用“通用成员函数指针”格式:通常 16 字节,含函数地址 + this 偏移(+ vtable 偏移,若虚)。没有标准方法解包。强行 reinterpret_cast 会导致 this 指针错位,调用时访问非法内存。
正确做法不是“取地址”,而是“构造可调用对象”:
- 用
std::mem_fn(&ClassName::func)得到可调用对象,再绑定实例后调用 —— 这是标准、安全、跨平台的方案 - 若需裸函数指针(例如注入到 C 回调),必须手写 thunk:分配可执行内存,写入几条指令(mov rdi, imm / jmp func_addr),再把 thunk 地址当普通函数指针用
- 使用
libffi或boost::hana等库做类型擦除,避免硬编码 this 调整逻辑 - 注意:thunk 必须用
mmap(MAP_JIT)(macOS)或VirtualAlloc(..., PAGE_EXECUTE_READWRITE)(Windows)分配,普通 malloc 内存不可执行
真正需要“入口地址”的场景极少,多数时候你其实只需要可调用性
绝大多数所谓“要成员函数地址”的需求,本质是想实现回调、hook、序列化调用或反射式调用。这些场景中,硬抠机器码地址既脆弱又不可移植。C++20 的 std::bind_front、C++17 的 std::invoke、甚至 C++11 的 lambda,都能干净替代。
- 错误思路:
auto addr = reinterpret_cast<uintptr_t>(&A::on_click); // ❌</uintptr_t> - 正确思路:
auto cb = [obj = std::make_shared<a>()](int x) { obj->on_click(x); }; // ✅</a> - 若必须跨语言(如暴露给 Lua),用
extern "C"包装一层静态函数,内部转发到成员函数(带显式 this 参数) - 调试时看真实地址?用 GDB:
p/x &A::on_click显示的是编译器生成的“指针值”,不是 JMP 目标;用disassemble *(&A::on_click)才能看到实际指令流起点
成员函数指针的 ABI 实现细节是编译器内部契约,不是接口。一旦你开始拆解它,就已站在未定义行为的悬崖边——除非你在写编译器、调试器或操作系统内核级工具。


















