成员函数指针不是单一地址,而是编译器私有的复合结构,不可直接转为整数或普通函数指针;union“偷渡”仅适用于非虚、单继承场景,且需严格满足abi条件。

不能直接获取成员函数指针的“原始地址”——它根本就不是单一地址,而是一个编译器私有的复合结构。你看到的 &T::func 表达式结果,类型是 void (T::*)(),这个类型在内存中可能占 4、8、12 或 16 字节,取决于继承方式和是否虚函数,且内容含义随编译器而异。
为什么 reinterpret_cast<uintptr_t>(&T::func)</uintptr_t> 会失败
编译器明确禁止将成员函数指针转为整数或普通函数指针,这是语言层硬性限制(C++ 标准 [expr.cast]),不是语法糖问题。MSVC、GCC、Clang 全部报 C2440 或 error: cannot cast,因为这种转换语义不成立:成员函数调用必须携带 this 调整逻辑,而整数地址无法表达 delta、vtbl 索引等信息。
- 哪怕类是空的单继承,
sizeof(void (T::*)())在 MSVC 是 4,在 GCC/Clang 常为 8 -
&Derived::show可能被隐式转换为Base::*类型,但底层字节布局已不同 - 对虚函数取地址,得到的不是代码地址,而是 vtable 中的槽位索引(如偏移 0x8)
用 union “偷渡”地址的实操边界
利用 union 的公共初始子序列特性绕过类型检查,是唯一被广泛验证的可行方案,但必须清楚它的适用前提和风险:
- 只适用于非虚、单继承、无虚基类的成员函数;多重继承下 union 解包出的地址可能跳转到错误位置
- 示例中
union { void(T::*)() pfn; uintptr_t addr; } u; u.pfn = &T::func;后读u.addr,拿到的是函数入口地址(不含 this 调整),但仅当该函数不依赖this偏移时才安全 - MSVC 需开启
/vd0(禁用 vtordisp)才能让 union 解包行为可预测;GCC/Clang 依赖-fno-rtti和具体 ABI - 不能把解包出的
addr直接传给std::function或线程启动函数——缺少 this 绑定,必然崩溃
真正需要“原始地址”的典型场景与替代方案
绝大多数声称要“获取地址”的需求,实际目标是 hook、热重载、调试符号解析或跨语言绑定,这些场景有更健壮的路径:
- Windows API hook(如
DetourAttach):必须用static_cast到FARPROC,但前提是目标函数是静态或自由函数;成员函数需先包装成 thunk(如用 inline asm 或 MSVC 的__declspec(naked)) - 调试器符号查找(如 DWARF / PDB):用调试接口(
IDiaSymbol::get_addressOffset)查的是 vtable slot 或 thunk 地址,而非&T::func本身 - 想存成员函数供 later call:用
std::function<void></void>包装,或手写 lambda 捕获对象,比折腾地址安全十倍 - 性能敏感回调:避免成员函数指针,改用
void* user_data + void (*cb)(void*, ...)模式,由调用方负责传this
成员函数指针的“地址”不是可以自由搬运的数据块,它是编译器生成的调用协议封装体。试图把它当普通指针处理,等于在 ABI 边界上走钢丝——偶尔能过,但任何一次继承变更、编译器升级或优化开关调整,都可能让解包出的字节变成无效指令。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











