跳转到内容

结构图总览

这些都是矢量图(SVG):手机上可以放大看、打印不糊、深浅色自动适配。 面试前建议:把「对象内存布局与虚函数表」「多继承对象布局」「菱形继承」这三张默画一遍——它们是 C++ 岗最高频的“现场画图”题。

出处 面试怎么用
对象内存布局与虚函数表 05 章 现场画:vptr → vtable → 表项谁覆盖谁
多继承对象布局:三个合法指针 05 章 解释 (void*)pa != (void*)pbthis 指针调整
对象切片 vs 指针/引用 05 章 解释“多态只对指针/引用生效”
菱形继承与虚继承 05 章 解释二义性、两份 A、为什么虚继承更占内存
构造/析构顺序与 vptr 切换 04 章 解释“构造/析构里调虚函数不走多态”
进程内存布局 02 章 高→低地址七段 + 变量归属速答
shared_ptr 控制块与循环引用 03 章 画控制块、解释 weak_ptr 为什么能破环
内存层级与访问延迟 11 章 说明“缓存命中与否差 100 倍”
缓存行:数组 vs 链表 11 章 解释 vector 遍历比 list 快的原因
OOP 与 ECS 的内存布局对比 12 章 解释 ECS 为什么缓存友好、易并行
渲染管线 13 章 按顺序讲四阶段 + DrawCall 出现在哪一步

进程虚拟地址空间(高地址在上)栈 Stack向下增长 · 局部变量/调用帧 · 自动分配释放空闲区未分配(栈堆相向生长)↓ ↑堆 Heap向上增长 · new/malloc · 手动管理 · 会碎片化BSS未初始化的全局/static 变量(启动时清零)已初始化数据 .data已初始化的全局/static 变量只读常量 .rodata字符串常量、const 全局量代码段 .text程序指令(只读)面试答法:从高到低栈 → 空闲 → 堆 → BSS→ .data → .rodata → .text易错点:· 字符串常量在 .rodata· 未初始化全局量在 BSS· static 局部量也在 .data/.bss
堆/栈上的 Dog 对象 dvptr(偏移 0,8 字节)其他成员(Animal 的数据)运行期寻址Dog 的虚函数表(vtable)&Dog::speak覆盖了 Animal::speak&Dog::~Dog析构:先派生后基类&Dog::~Dog(deleting)delete 基类指针时调用· 虚表在只读数据段,同类型对象共享一张· 派生类重写哪一项,就替换哪一项的地址· 派生类新增的虚函数追加到表尾

图多继承对象布局:两个 vptr 并不相邻

Section titled “图多继承对象布局:两个 vptr 并不相邻”
class A { virtual void fa(); int a; };class C : public A, public B { int cc; };+0A 的 vptr8B主基类子对象以 vptr 打头(与 C 共享这张表)+8A::a4B主基类的成员紧跟自己的 vptr+12padding4B把 B 子对象对齐到 8 字节+16B 的 vptr8B次基类子对象从 +16 开始,vptr 打头 —— 不在对象头部+24B::b4B次基类的成员跟在次基类 vptr 后面+28C::cc4B派生类自己的成员放在所有基类子对象之后+32padding8B整体补到 8 的倍数 → sizeof(C) = 40pc / papb同一块内存三个合法指针pb = pc + 16为什么 C* → B* 必须 +16:B 的虚函数要用「B 子对象自己的 vptr」查表,访问 B::b 也要以 B 子对象为基准。指针不调,就会读到 A 的 vptr → 调错函数。三件容易记混的事:① 数值:(void*)pa != (void*)pb(差 16);(void*)pa == (void*)pc;② 比较:pa == pb 结果为 true(标准要求先各自转回 C* 再比);③ 工程:跨层次一律 static_cast / dynamic_cast,绝不手算偏移。自己验证:printf("%td", (char*)pb - (char*)pc); 或 g++ -fdump-lang-class a.cpp
① 按值传参 f(Animal a)Dog 对象Dog 特有的成员/数据↑ 被切掉(丢失)只剩 Animal 部分 → 调 Animal::speak输出「动物叫」,多态失效② 指针 / 引用Dog 对象Dog 特有数据(完整保留)Animal* p = &d;对象没被拷贝 → 动态类型仍是 Dog输出「汪汪」,多态生效记忆句:多态只对指针/引用生效;按值传递 = 切片(slicing)。

图菱形继承与虚继承的对象布局

Section titled “图菱形继承与虚继承的对象布局”
普通继承:A 子对象有两份AB : AC : AD : B, CD 的内存B::A::data ← 第一份C::A::data ← 第二份(重复!)d.data 编译错误:二义性;改 B::data 不影响 C::datasizeof(D) = 8(数据翻倍、状态割裂)虚继承:A 子对象只有一份AB : virtual AC : virtual AD : B, CD 的内存B 的部分(偏移指针)A::data ← 唯一一份(虚基类子对象)d.data 合法;sizeof(D) 变大(多偏移指针),但语义正确代价:访问虚基类成员要多一次查表
构造:基类 → 成员(声明顺序)→ 自身构造体 | 析构:完全相反Base 构造vptr → Base 的虚表此时 Derived 还没构造,虚函数走 Base 版本成员按声明顺序构造vptr 仍指向 Base(成员不是多态的一部分)Derived 构造体vptr 被改写 → Derived 的虚表从这里开始虚函数才动态绑定到派生类Derived 析构体vptr 仍指向 Derived成员逆序析构vptr 切回 BaseBase 析构vptr → Base 的虚表基类析构里调虚函数 = 调基类版本结论:构造/析构期间不动态绑定——vptr 在构造体执行前才切到本类虚表,析构体执行后切回基类。
shared_ptr 两块内存shared_ptr aMonster 对象控制块(堆上,make_shared 时与对象同一块分配)强引用计数 = 2弱引用计数 = 1删除器 / 分配器循环引用 → 泄漏对象 A对象 Bshared_ptr 互相持有计数永远 ≥ 1 → 两个对象都不析构解法:其中一侧改 weak_ptr.lock()· 拷贝 shared_ptr:强计数 +1;析构:-1;归零才 delete 对象 + 释放控制块· weak_ptr 只增弱计数,不阻止释放;用 lock() 提升为 shared_ptr 再访问
容量越大越慢;缓存命中与否差 100 倍CPU 寄存器~1 cycleL1 缓存~4 cycles(~1ns)L2 缓存~12 cyclesL3 缓存~40 cycles主内存 RAM~200+ cycles(~100ns)SSD / 磁盘~10 万+ cycles推论:性能优化的头号手段是「让数据在缓存里」——连续数组 > 链表/指针跳转;把「热数据」和「冷数据」分开(SoA / ECS),减少 cache miss。
一条缓存行(典型 64 字节)数组 vector<int>a[0]a[1]a[2]a[3]a[4]a[5]a[6]a[7]一次 load 把整条缓存行搬进 L1 → 后续 7 个元素几乎零成本(空间局部性)链表 list<int>datanextdatanextdatanextdatanextdatanext每个节点独立分配 → 地址分散,每跳一次都可能 cache miss;预取器也帮不上忙面试答法:vector 遍历比 list 快,不是因为算法,而是因为缓存行和顺序预取。
传统 OOP:对象数组,字段交错xyhpvxvytex遍历移动系统时,x/y 之间隔着 hp/tex →每读几个字段就跨缓存行 → cache missECS / SoA:同类组件连续存放Position[]x,yx,yx,yx,yVelocity[]vx,vyvx,vyvx,vyvx,vyHealth[]hphphphp移动系统只碰 Position/Velocity 两块连续内存→ 顺序预取、缓存命中率高,可 SIMD 批处理面试答法:ECS 的收益不是「架构更优雅」,而是「内存布局对缓存和并行友好」——实体只是一个 ID,组件是纯数据,系统遍历同类组件。
GPU 渲染管线(概念级,面试能按顺序讲一遍即可)① 顶点着色器 Vertex Shader每个顶点一次:模型→世界→相机→投影变换(MVP)② 图元装配 + 光栅化三角形 → 像素片元(判断哪些像素被覆盖)③ 片元着色器 Fragment Shader每个像素一次:光照、纹理采样、材质计算④ 输出合并 Output Merger深度测试 / 模板测试 / 混合 → 写入帧缓冲帧缓冲 → 屏幕显示双缓冲交换(避免撕裂)DrawCall 出现在「CPU 提交 → GPU 执行」这一步:每次提交都有固定开销,所以要用批处理/Instancing 减少次数。