c++对象模型--指针

1.成员指针与获取字段偏移

1.1.基本语法与本质

c 复制代码
struct A { virtual void vf(); int x; int y; };

int A::*p = &A::x;   // 注意:不是 int*,是"指向 A 成员的指针"
A a{...};
a.*p = 42;           // 通过对象解引用
A* pa = &a;
pa->*p = 42;         // 通过对象指针解引用

核心认知:&A::x 不是地址,是偏移量。 它缺少"哪个对象"这一信息,只有绑定了对象才成为真正的地址。

实测字节(x86-64):

c 复制代码
offsetof(A,x) = 8, offsetof(A,y) = 12     ← A 有 vptr,x 从 8 开始
&A::x 的内存表示 = 08 00 00 00 00 00 00 00   ← 就是数字 8!
&A::y 的内存表示 = 0c 00 00 00 00 00 00 00   ← 就是数字 12!

a.*p 的求值就是 (char)&a + 偏移,编译成一条加法指令。

1.2.获取字段偏移的两种手段

c 复制代码
// ① 标准宏(编译期常量,只能用于标准布局类型成员)
offsetof(A, x)          // 需要 <cstddef>

// ② 成员指针转整数(技巧性,非标准但通用)
auto off = reinterpret_cast<size_t>(&A::x);   // 本例 = 8

1.3. 成员指针作为"可传递的字段存取器"

c 复制代码
template <class T, class M>
void sort_by(std::vector<T>& v, M T::*key) {
    std::sort(v.begin(), v.end(),
              [&](const T& a, const T& b){ return a.*key < b.*key; });
}
sort_by(students, &Student::score);   // 想按哪个字段排就传哪个

2.非成员函数指针

c 复制代码
int free_add(int, int);
int (*fp)(int, int) = free_add;      // 或 &free_add,等价
fp(3, 4);                            // 或 (*fp)(3,4),等价
  • 本质就是代码地址,一个普通数据指针大小的整数。
  • 函数名在表达式中自动 decay 为函数指针(类似数组 decay)。
  • sizeof(fp) == 8(x86-64)。

函数指针数组是经典用法(状态机、跳转表):

c 复制代码
void (*handlers[256])(Event&) = {...};
handlers[e.type](e);

限制:reinterpret_cast 在函数指针间转换是实现定义行为;POSIX 保证了函数指针与 void* 可互转(dlsym 依赖这一点),但 C++ 标准并不保证。

3.三类成员函数指针

3.1.非静态、非虚成员函数指针

c 复制代码
void (A::*nf)() = &A::nf;   // 类型里必须有类名
(a.*nf)();                  // 调用

实测 16 字节,内部是 {函数地址, this 调整量} 二元组:

c 复制代码
&A::nf = c6 14 ce f0 e1 55 00 00 | 00 00 00 00 00 00 00 00
         ^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^
         函数代码地址                this 调整量(单继承下为 0)

为什么需要 16 字节而不是 8?结合多重继承回忆:this 在调用时必须指向正确的子对象。如果函数定义在第二个基类里,调用前要把 D* 调整为 B* 子对象地址------这个调整量就存在第二槽里。

3.2.虚成员函数指针

这里需进一步明确--to do

cpp 复制代码
void (A::*vf)() = &A::vf;
(a.*vf)();   // 走多态,调用的可能是派生类覆写版

实测字节揭示了惊天真相:

c 复制代码
&A::vf = 01 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
         ^^^^^^^^^^^^^^^^^^^^^^^
         不是地址!是 vtable 偏移的编码(1 = vtable 字节偏移 0 + 标志位)

取虚函数的地址,得到的不是代码地址。 成员函数指针内部约定:低位为 1 表示"这是虚函数,请查虚表第 N 槽"。调用 (a.*vf)() 的展开逻辑相当于:

c 复制代码
if (ptr 低位为 1) {
    vtbl = *(void***)&a;                    // 取 vptr
    addr = vtbl[(ptr >> 1) 换算的槽位];      // 间接寻址
    addr(a 的 this 调整后的地址);
} else {
    ptr(a);                                  // 直接调用
}

编译器生成一个 trampoline(跳板函数) 来处理这个分派,&A::vf 实际存的是跳板逻辑所需的编码。

3.3.静态成员函数指针

c 复制代码
auto sf = &A::sf;    // 类型是 void(*)(),不带类名!
sf();

实测只有 8 字节,内容就是普通函数地址。静态成员函数没有 this,不依赖任何对象,语义上就是名字空间受限的普通函数。因此:

c 复制代码
void (*fp)() = &A::sf;        // ✅ 直接赋给普通函数指针
void (A::*mfp)() = &A::sf;    // ❌ 编译错误:类型不匹配

3.汇总

指针类型 大小 内部表示 需要对象?
int*(对象指针) 8 地址 ---
void(*)()(自由/静态函数) 8 代码地址 否
int A::*(数据成员) 8 偏移量 是(.*/->*)
void (A::*)()(成员函数) 16 {地址或 vtable 编码, this 调整量} 是
虚函数的成员函数指针 16 {vtable 偏移编码, this 调整量} 是
  • 所有对象指针(T*、void*、函数指针在主流 ABI 上)大小相同------这是各平台 ABI 的约定,使 void* 通用存储可行。但标准只保证 char*/void*/std::byte* 能表示任何对象地址,不保证 double* 与 void* 同尺寸(历史上 DOS 近/远指针时代就不等)。
  • 成员指针是另一物种。int A::* 不是指针,是偏移+标志;void (A::*)() 是二元组。16 字节的成员函数指针不能塞进 void*,成员指针与 void* 之间不能转换。
  • MSVC 上更夸张:多重/虚拟继承的成员函数指针可达 20 字节,甚至涉及虚表查找。
  • &a.x 与 &A::x 是两种完全不同的东西!前者是类型指针,后者常用语定位字段在类型实例中偏移。

4.std::function 与 std::bind

4.1.std::function:类型擦除的可调用对象包装

c 复制代码
std::function<int(int,int)> f1 = free_add;                 // 函数
std::function<int(int,int)> f2 = [](int a,int b){...};     // lambda
std::function<int(int,int)> f3 = std::bind(&Adder::add, &ad, _1, _2); // bind 结果
std::function<int(int,int)> f4 = SomeFunctor{};            // 函数对象
  • 接口统一:调用方只认 int(int,int) 签名,不关心底层是什么。
  • 实现机制:内部持有指向堆上"控制块"的指针(vtable + 对象副本),小对象可能 SBO 优化就地存储。实测 sizeof(std::function<void()>) == 32(一个 16 字节就地存储 + 两个指针)。
  • 成本:一次间接调用 + 可能的一次堆分配;比裸函数指针慢,但比 std::bind 结果更直观。
  • 空状态:默认构造的 std::function 为空,调用抛 std::bad_function_call(实测确认)。
  • 不能包装 move-only 捕获的 lambda(unique_ptr 捕获)------拷贝语义的要求所致。

4.2.std::bind:参数绑定(C++11 时代的 currying)

c 复制代码
using namespace std::placeholders;

auto add5 = std::bind(free_add, 5, _1);   // 预绑定第一个参数
add5(10);                                  // = free_add(5, 10) = 15

auto swapped = std::bind(free_add, _2, _1); // 占位符重排参数
swapped(3, 10);                              // = free_add(10, 3) = 13
相关推荐
流年如夢1 小时前
stack和queue_介绍、用法及模拟实现
c++·stl·queue·stack·priority_queue
AA陈超1 小时前
CN-Language中文编程语言:第03篇-语法基础
开发语言·cn·cn中文编程语言·cn-language
熊猫钓鱼>_>1 小时前
CMP for OpenHarmony 实战:Coil 图片加载库鸿蒙化使用心得
运维·服务器·开发语言·华为·ai编程·harmonyos·openharmony
fengci.1 小时前
CTF2刷题记录(困难部分)
android·开发语言·前端·学习·php
老王IT1 小时前
Rust开发环境搭建教程
开发语言·后端·rust
chenbingjie_c1 小时前
C++ 模板进阶:从 “会用“ 到 “懂底层“(非类型参数・特化・分离式编译)
c++·后端
world78112262 小时前
【C++】set 和 map
java·前端·c++·set·map
程序猿乐锅2 小时前
Streamlit 实现 AI 聊天 Agent:角色提示词、流式输出与会话管理
开发语言·前端·笔记·后端·python·学习
小南知更鸟2 小时前
【2026 年 Python 包管理工具选型:从“工具崇拜”回到工程约束】
开发语言·python