跳转到内容

附录 A · 术语表

本表收录游戏开发/引擎方向 C++ 实习面试的高频术语,按主题分组,共 10 组 140 条。 「面试要点」列写的是最容易被追问的那个点或易错点,不是“很重要”式的空话。 术语覆盖范围与正文各章一致(02~13 章),数值、结论、易错点均以正文为准。

术语 英文/缩写 一句话解释 面试要点
链接 Linking 符号解析(把引用找到定义)+ 重定位(填真实地址) undefined reference 发生在这一步,不在编译期
翻译单元 Translation Unit 一个 .cpp 加上它展开后的所有头文件,是编译的基本单位 ODR 按翻译单元判定,所以头文件里放定义很容易踩坑
单一定义规则 ODR 程序里每个实体只能有一份定义,声明可以有很多份 例外只有两个:inline 函数/变量与模板,其余放头文件必报重复定义
符号表 symbol table 目标文件里“函数/变量名 → 地址”的映射 链接靠符号名匹配,符号名不一致就会找不到定义
重定位 relocation 把符号引用填成运行期真实地址 动态库加载时也要做重定位,这是它启动稍慢的原因
名字修饰 name mangling C++ 为支持重载给函数名改名,foo(int) 变成 _Z3fooi C 库符号是原名,所以 C++ 直接链 C 库会 undefined reference
C 语言链接 extern “C” 关闭名字修饰,让 C++ 能链上 C 编译出的符号 只影响链接不影响函数行为;这个块里不能重载
静态库 static library (.a / .lib) 链接期把用到的库代码拷进可执行文件 体积大、部署独立、启动快;改库要重新编译整个可执行文件
动态库 dynamic library (.so / .dll) 链接时只记录依赖,运行时或加载时才加载 可共享、可热更新,但缺文件就运行时报错
位置无关代码 PIC 动态库用相对寻址 + GOT 让代码与加载地址无关 做动态库要加 -fPIC,否则可能无法被多进程共享映射
术语 英文/缩写 一句话解释 面试要点
进程内存布局 process memory layout 高地址到低地址:栈→空闲区→堆→BSS→.data→.rodata→.text 栈向下增长、堆向上增长,中间留空闲区避免相撞
代码段 .text 存放程序指令,只读 问“函数指令放哪”答 .text,别答 .rodata
只读数据段 .rodata 字符串常量、const 全局常量、虚函数表 改写 .rodata 会段错误崩掉
已初始化数据段 .data 已初始化的全局变量与 static 变量 int b = 5;(文件作用域)在 .data,static int c; 在 .bss
未初始化数据段 .bss 未初始化的全局变量与 static 变量,运行时由系统清零 不占可执行文件体积,只记录大小
stack 存局部变量与函数调用帧,编译期自动分配释放,向下增长 容量约 1~8MB,递归太深或超大局部数组会栈溢出
heap new/malloc 出来的内存,向上增长,手动管理 越界破坏堆管理结构,可能延迟崩溃而不是立刻崩
默认初始化规则 default initialization 全局/static 默认为 0;栈和堆默认不初始化 易错:new int 是垃圾值,new int() 才是 0
栈帧 stack frame 一次函数调用在栈上占用的那段空间,含局部变量与返回地址 返回局部变量地址会悬垂,因为栈帧已被回收
sizeof sizeof 类型/对象占多少字节,含尾部 padding 空类 sizeof 是 1;含虚函数是 8(vptr)
alignof alignof 类型的对齐数,即起始地址必须是它的倍数 sizeof 答“占地多大”,alignof 答“住址要求”
内存对齐 alignment 成员起始偏移是对齐数的整数倍,CPU 才能一次读完 对齐是为了避免跨字访问,不是为省内存
填充与对齐规则 padding / alignment rule 成员偏移取整到自身对齐;结构体对齐取成员最大对齐;总大小再取整 {char,int,double} 是 16,{char,double,int} 是 24,大的放前面省内存
pragma pack #pragma pack(n) 强制按 min(n, alignof(成员)) 对齐,压掉 padding 用于网络报文、文件格式、硬件寄存器映射
大小端 endianness 大端高位字节存低地址;小端低位字节存低地址 小端机 0x12345678 内存里依次是 78 56 34 12
RAII Resource Acquisition Is Initialization 构造获取资源、析构释放资源,靠栈对象保证一定被调用 智能指针/lock_guard/fstream 都是;这是异常安全的根基
浅拷贝 shallow copy 默认拷贝只逐成员复制指针值,两个对象共享同一块内存 裸资源类浅拷贝 → 两次析构同一内存 → double free
深拷贝 deep copy 为新对象重新分配资源并复制内容 拷贝赋值要先分配新内存成功后再释放旧资源,并做自赋值检查
悬垂指针 dangling pointer 曾经有效,但指向的对象已被释放 来源:delete 后没置空、返回局部变量或按值形参的地址
内存池 memory pool 预分配一大块切成固定大小块,用自由链表管理 分配/释放都是 O(1) 指针操作、无碎片,但只管内存不调构造
对象池 object pool 预创建一批对象,借出/归还复用 归还时重置状态不析构;地址固定,可做到连续内存
独占所有权 unique_ptr 独占资源,禁拷贝允许移动,默认删除器下与裸指针同大小 能放进 vector,因为 vector 只要求可移动不要求可拷贝
共享所有权 shared_ptr 引用计数共享对象,最后一个销毁时释放 计数在堆上的控制块里;计数原子安全,所指对象不安全
控制块 control block shared_ptr 在堆上的那块内存:强计数、弱计数、删除器、分配器 循环引用就是两块控制块的强计数都为 1,谁都放不掉
弱引用 weak_ptr 只加弱计数不加强计数,用 lock() 提升为 shared_ptr lock() 会临时把强计数 +1,保证提升期间对象不被销毁
循环引用 cyclic reference 两个 shared_ptr 互相持强引用,计数永不归零 → 泄漏 解法是把一个方向改 weak_ptr;谁不依赖对方活着谁就 weak
make_shared make_shared 对象与控制块一次分配完成,缓存友好且异常安全 对象大且还有 weak_ptr 观察时,改用 shared_ptr(new T) 让对象内存尽早释放

三、对象模型:类、继承与多态

Section titled “三、对象模型:类、继承与多态”
术语 英文/缩写 一句话解释 面试要点
特殊成员函数 special member functions 默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值 六个的调用时机要能逐个说清什么时候被调用
Rule of 0/3/5 Rule of Zero/Three/Five 管裸资源就得成套写;用标准库类型则一个都不用写 只写析构不写拷贝构造,编译器仍生成浅拷贝 → double free
拷贝构造 copy constructor 用已有对象创建一个对象 口诀:有新对象 = 拷贝构造,老对象赋值 = 拷贝赋值
拷贝赋值 copy assignment 已存在对象赋新值 必须处理自赋值;返回 T& 而不是 T,支持链式 a=b=c
移动构造 move constructor 把源对象的资源指针搬过来并置空源对象 必须尽量 noexcept,否则 vector 扩容会退化为拷贝
初始化列表 member initializer list 在构造函数体之前直接构造成员,只调一次构造 初始化顺序按声明顺序,与列表书写顺序无关
有效但未指定 valid but unspecified 被 move 之后对象的状态:能析构能再赋值,内容不可依赖 别和悬垂混:悬垂是访问已释放内存(UB),这个是合法的
析构函数 destructor 无参无返回值、不可重载,对象销毁时自动调用 析构里抛异常可能 double throw → std::terminate
空基类优化 EBO 空类作基类时那 1 字节被塞进填充字节,零额外空间 作成员不能优化;两个同类型空基类不能共用同一地址
虚函数表 vtable 每个含虚函数的类一张表,存该类所有虚函数地址 存在只读数据段 .rodata,每类一份、所有对象共享
虚指针 vptr 对象里指向所属类虚表的指针 单继承下在对象偏移 0,占 8 字节(64 位)
纯虚函数 pure virtual function (= 0) 强制派生类实现的虚函数声明 纯虚 ≠ 没有实现:基类可给实现,用 Base::f() 显式调用
抽象类 abstract class 含至少一个纯虚函数的类,不能实例化 派生类没实现全部纯虚函数 → 它仍是抽象类
虚析构 virtual destructor 多态基类的析构函数必须 virtual 否则通过基类指针 delete 是 UB,常见表现是派生类资源泄漏
对象切片 object slicing 按值传递派生对象时按基类拷贝构造,派生部分被切掉 多态只对指针/引用生效,按值传参多态一定失效
运行时类型识别 RTTI 运行期查对象真实类型的能力,只对多态类型可用 type_info 挂在虚表前置槽,没有 vptr 就没地方存类型信息
dynamic_cast dynamic_cast 运行期安全的下行转换,失败返回 nullptr 或抛异常 是运行期库调用、无法内联,热路径尽量别用
虚继承与菱形继承 virtual inheritance / diamond B、C 都继承 A、D 继承 B 和 C → A 子对象两份且访问二义;虚继承只留一份 代价是访问虚基类成员要查偏移表多一次间接;虚基类由最派生类构造
术语 英文/缩写 一句话解释 面试要点
模板定义可见性 definition must be visible 模板定义必须写在头文件里 定义放 .cpp,别的翻译单元看不见 → 链接报 undefined reference
偏特化 partial specialization 限定一类类型(如 T*const T)给出实现 函数模板不能偏特化,只能用重载 + 全特化替代
SFINAE Substitution Failure Is Not An Error 替换失败不是错误:签名处替换失败就跳过该候选,不报错 只适用于直接上下文(签名/模板形参),函数体里的错误不算
enable_if std::enable_if 条件成立才有 type 成员,从而让某个重载“存在” 用于删掉整个重载;C++17 后多数场景可换 if constexpr
if constexpr if constexpr (C++17) 编译期分支,未选中的依赖分支不实例化 普通 if 两个分支都会编译;非依赖代码仍会被完整检查
类型萃取 type_traits 编译期类型判断与变换的库,如 is_integralremove_reference C++17 起用 _v 拿值、_t 拿类型
可变参数模板 variadic template typename... Args 参数包,配 sizeof...(Args) 取个数 展开两种:递归剥参数(C++11)或折叠表达式(C++17)
折叠表达式 fold expression (C++17) 用同一运算符把参数包折叠成一个表达式,如 (args + ...) 空参数包需要初始值,如 (args + ... + 0) 才返回 0
CRTP Curiously Recurring Template Pattern 派生类把自己作模板参数传给基类,基类 static_cast 静态分发 零虚表开销且可内联;ECS 组件、数学库常用
编译期多态 static polymorphism 用模板/重载在编译期决定调用哪个实现 可内联、零抽象开销,代价是代码膨胀、类型必须编译期确定
模板与虚函数 template vs virtual 模板函数不能是虚函数 原因是虚表要运行期确定函数集合,与编译期实例化冲突
术语 英文/缩写 一句话解释 面试要点
值类别 value category 左值(有名字可取地址)、纯右值(临时无名)、将亡值(可被偷资源) 陷阱:int&& r 里的 r 本身是左值,右值引用是类型不是值类别
右值引用 rvalue reference (T&&) 只能绑右值的引用类型,是移动语义的基础 没有右值引用就没有 unique_ptr 的所有权转移
移动语义 move semantics 把源对象的资源“偷”过来并置空源对象,成本 O(1) 于资源指针 移动构造/移动赋值应尽量 noexcept,否则容器扩容会退化为拷贝
std::move std::move 不移动任何东西,只是一个到右值引用的类型转换 真正搬资源的是移动构造函数,move 本身零操作
完美转发 perfect forwarding 保持参数在函数链中传递时的左/右值性不变 写法是转发引用 T&& + std::forward<T>(arg),用 move 会误伤左值
引用折叠 reference collapsing T& + &&T&T&& + &&T&& 这是完美转发能工作的底层机制;左值传入时 T 推导为 T&
lambda 表达式 lambda 匿名仿函数对象,捕获的变量成为这个类的成员 值捕获是快照,引用捕获会悬垂;空捕获可转函数指针
lambda 捕获 capture list [x] 值捕获、[&x] 引用捕获、[x = expr] 初始化捕获(C++14) 引用捕获的 lambda 逃出作用域就悬垂,放进异步任务要特别小心
nullptr nullptr (C++11) 类型是 std::nullptr_t,专门表示空指针 NULL 是整数 0 的宏,重载 f(int)/f(char*) 时会选错版本
constexpr constexpr 强制编译期求值,可做数组大小、模板参数 const 只表示不可修改,运行期才知道值也可以
consteval consteval (C++20) 强制编译期求值,运行时调用直接编译失败 比 constexpr 更严格:consteval 函数不允许退化成运行期调用
concepts concepts (C++20) 给模板参数加命名约束,报错信息友好 写法 template <std::integral T>,比 SFINAE 可读得多
三向比较 <=> (C++20) 一次定义 <=>,自动生成 == != < <= > >= 六个比较 保证六者语义一致,避免手写出现 a<bb>a 矛盾
协程 coroutines (C++20) 可挂起/恢复的函数,co_await / co_yield / co_return C++20 是无栈协程,状态放在堆上的 coroutine frame
术语 英文/缩写 一句话解释 面试要点
进程 process 有独立内存空间与文件描述符,是隔离单位 一个进程崩不影响别的进程;切换要换页表,开销大
线程 thread 进程内的最小执行单元,共享进程内存 一个线程段错误整个进程崩;游戏用线程是为了共享内存零拷贝
数据竞争 data race 两个线程并发访问同一内存、至少一个写、且无同步 → UB 读读不算竞争,读写与写写才算
互斥量 std::mutex 锁,保护临界区,同一时刻只允许一个线程进入 裸 lock/unlock 一旦中途 return 就忘解锁 → 其他线程永久阻塞
lock_guard std::lock_guard RAII 锁:构造加锁、析构解锁,不能手动控制 绝大多数加锁场景用它最安全
unique_lock std::unique_lock 可手动 lock/unlock、可移动、可配条件变量 条件变量必须用它,因为 wait 要原子地释放锁并重新上锁
scoped_lock std::scoped_lock (C++17) 一次性锁多个互斥量,内部用死锁避免算法 “两个账户互相转账”这类场景的标准答案
死锁四条件 deadlock conditions 互斥、占有且等待、不可剥夺、循环等待 破坏任意一个即可;实践中最常破坏循环等待(按固定顺序加锁)
条件变量 condition_variable 让线程等待某条件成立,由其他线程 notify 唤醒 必须配 unique_lock;condition_variable_any 才允许其他锁类型
虚假唤醒 spurious wakeup 被唤醒 ≠ 条件一定满足,可能空唤醒 wait(ul, pred) 内部等价 while(!pred) wait(ul),天然免疫
内存序 memory order 控制编译器/CPU 对指令可见性与重排的约束 relaxed 只保证原子性;acquire/release 配对;seq_cst 是默认最安全也最慢
线程池 thread pool 固定数量工作线程 + 任务队列 + 条件变量 + stop 标志 省掉反复创建销毁线程的开销;析构要置 stop 并唤醒全部再 join
job system job system 把大任务拆成互相独立的小 job 放进多线程队列并行执行 UE 的 TaskGraph 是例子;比“每系统一个线程”更负载均衡、少死锁
线程安全单例 magic static 局部 static 变量的初始化由 C++11 保证线程安全 单例首选写法;DCLP 的坑是 new 三步可能乱序,会拿到未构造完的对象
术语 英文/缩写 一句话解释 面试要点
vector std::vector 动态连续数组,三个指针管理 start / finish / end_of_storage 结构题要能说出这三个指针分别代表什么
扩容机制 reallocation 容量满就分配新内存、搬元素、释放旧内存 增长因子 GCC 是 2 倍、MSVC 是 1.5 倍
迭代器失效 iterator invalidation 容器操作后原迭代器/指针/引用不再有效 vector 扩容后全部失效;erase 后必须 it = v.erase(it)
reserve reserve 只扩 capacity,不改变 size,不构造元素 与 resize 的区别是高频考点:resize 会真的构造/销毁元素
emplace_back emplace_back 转发参数在容器内原地构造,省一次临时对象拷贝/移动 参数已是现成对象时两者差不多;列表初始化容易踩坑
红黑树 red-black tree 自平衡 BST,最长路径 ≤ 2×最短路径,操作 O(log n) 选它不选 AVL 的理由:旋转少、常数小、整体统计性能好
map std::map 红黑树实现的有序映射 迭代器指向 pair<const Key, T>,所以 key 一定不可修改
map operator[] 副作用 map operator[] key 不存在时会插入一个默认值 只判断存在要用 find / count / contains(C++20),别用 []
unordered_map std::unordered_map 哈希表 + 链地址法,平均 O(1) 查找 桶内转红黑树是 Java 8 HashMap 的行为,C++ 没有
负载因子 load factor 元素数 / 桶数,超过阈值(默认 1.0)触发 rehash rehash 是 O(n) 且全部迭代器失效
自定义哈希 std::hash 特化 自定义类型作 unordered_map 的 key,要提供 hash 与 == 两者缺一不可,只写一个编译不过
vector<bool> vector<bool> 按位压缩存储,operator[] 返回 proxy 不是 bool& 所以它不满足标准容器要求,取它元素的地址会出错
术语 英文/缩写 一句话解释 面试要点
测量优先 measure, don’t guess 先用 profiler 找到热点再动手优化 90% 的直觉优化是浪费;流程是测量→定位→优化→复测
虚函数开销 virtual call overhead 间接跳转(取 vptr 查表跳转)+ 无法内联 + 分支预测变差 单次约 5~15ns 看似无感,但每帧×海量对象就累积成几毫秒
去虚拟化 devirtualization 编译器把查虚表的间接调用改写成直接调用,之后才谈得上内联 触发条件:直接对象、final 类/函数、构造析构内、显式限定、LTO
缓存行 cache line CPU 加载内存的单位,通常 64 字节 访问一个 int 会把周围 64 字节一起搬进缓存
缓存未命中 cache miss 数据不在缓存,要等主内存,约 200+ 周期 顺序遍历命中率接近 100%,随机访问可能慢 10~100 倍
SoA / AoS Structure of Arrays / Array of Structures AoS 一个对象一个结构体;SoA 每个字段一个数组 批量处理单字段时 SoA 的缓存行全是目标数据,粒子与 ECS 都用它
分支预测 branch prediction CPU 猜 if 走哪边,猜错要清空流水线,惩罚约 20 周期 数据有序则易预测;缓解用排序、branchless 位运算、查找表
内联 inline 把函数体展开到调用处,省调用开销并暴露优化机会 inline 关键字只是建议,编译器决定;虚调用无法内联
术语 英文/缩写 一句话解释 面试要点
单例模式 Singleton 保证全局唯一实例并提供全局访问点 游戏里诟病它把全局状态藏起来;替代是依赖注入或引擎统一持有子系统
工厂模式 Factory 把对象创建逻辑集中,加类型只改工厂 加类型用 switch 要改工厂代码,用注册表(map 存创建函数)只加一行
观察者模式 Observer 被观察者维护一组观察者,状态变化时逐一通知 四个坑:循环通知、回调里改监听容器、回调悬垂、热路径性能
状态模式 State 把状态抽成类,行为随状态切换 状态多了会类爆炸;要小心“update 中间切换状态”的时机问题
命令模式 Command 把操作封装成对象,可进队列、可撤销重做 undo 靠历史栈弹栈调 undo();联机只传命令两端确定性执行
术语 英文/缩写 一句话解释 面试要点
游戏循环 game loop 输入 → 更新 → 渲染,循环到退出 60fps 意味着一帧预算 16.7ms,超了就掉帧
固定步长 fixed timestep 逻辑/物理按固定 dt 推进,用 accumulator 累加 保证物理稳定与确定性,是联机同步的前提
可变步长 variable timestep 每帧用真实流逝时间做 dt 实现简单,但帧率波动会让物理抖动、跨机器结果不一致
四元数 quaternion 用 4 个分量(w + xyz)表示旋转 无万向节死锁、slerp 平滑插值;欧拉角两轴对齐会失去一个自由度
实体组件系统 ECS Entity 只是 ID、Component 是纯数据、System 是纯逻辑 缓存友好(组件数组天然 SoA)+ 易并行 + 加能力只需加组件
粗检测 broad phase 用空间分区快速筛掉绝大多数不可能碰撞的对象对 把全对全检测从 O(n²) 降到接近 O(n)
细检测 narrow phase 对候选对做精确求交 用 AABB/球/OBB 相交测试、多边形分离轴;它不再是全对全
四叉树/八叉树 quadtree / octree 递归把空间四等分/八等分 四叉树用于 2D 与静态地形,八叉树是 3D 场景查询的标准结构
BVH Bounding Volume Hierarchy 用包围盒树包住物体,可实时更新 动态物体(角色、刚体)友好,是物理引擎 broad phase 的核心
渲染管线 render pipeline 顶点着色 → 图元装配/光栅化 → 片元着色 → 输出合并 → 帧缓冲 顶点着色每顶点一次、片元着色每像素一次
深度测试 depth test 在输出合并阶段按深度决定像素是否被遮挡 它属于输出合并阶段,不是片元着色阶段
延迟渲染 deferred rendering 先渲出 GBuffer(位置/法线/颜色/金属度),再统一算光照 光照从“每物体”变“每像素一次”,但吃带宽、MSAA 差、透明要回前向
DrawCall draw call CPU 向 GPU 提交一次“画这个物体”的命令 贵在 CPU 提交通信 + GPU 状态切换,不是 GPU 算不过来
合批 batching(批处理) 同材质物体合并成一个网格一次画完 分静态合批与动态合批;减少 DrawCall 最常用的两招之一
GPU 实例化 instancing 同一网格不同变换一次提交 N 个,着色器按 instanceID 取自己的变换 十万颗草用一次 Instancing;与合批的差别是网格相同、变换不同
遮挡剔除 occlusion culling 被其他物体挡住的物体不画 与“不在视野内”的剔除不同:遮挡关系要靠额外查询判断,开销更高
UObject UObject UE 里几乎所有引擎对象的基类 提供反射、GC、序列化、编辑器集成、网络复制五大能力
反射 reflection 运行时查询类的属性/方法及其元信息 C++ 原生没有;UE 用 UCLASS/UPROPERTY/UFUNCTION 宏 + UHT 生成代码补上
UE 垃圾回收 UE GC 可达性分析(Mark-Sweep):从根出发标记可达对象,未标记的回收 循环引用天然解决;但 UObject 引用没用 UPROPERTY 标记就不算根,会被回收
Mono Mono (Unity) IL 在运行时 JIT 编译成机器码 迭代快、支持热更;iOS 曾禁止 JIT 所以受限
IL2CPP IL2CPP (Unity) IL 先转成 C++ 再 AOT 编译成原生机器码 平台全、防反编译、性能好,但包大、难热更;正式发布包用它
  1. 转换看数值,比较要同类型——多继承下 (void*)pa == (void*)pb 比的是地址数值(差 16);要判断“是不是同一个对象”,先把两边转成同一个指针类型再比。
  2. 有新对象 = 拷贝构造,老对象赋值 = 拷贝赋值——看运算符左边是“创建”还是“已存在”,一句话区分两个高频考点。
  3. 栈和堆默认不初始化,全局/static 默认是 0——new int 是垃圾值,new int() 才是 0,int y{} 也是 0。
  4. sizeof 答“占地多大”,alignof 答“住址要求”——{char,int} 是 sizeof 8、alignof 4;按成员从大到小排 padding 最少。
  5. 小端 = 尾巴(低位字节)先存——小端机 0x12345678 内存里依次是 78 56 34 12,别背成 12 34 56 78
  6. 不能拷贝 ≠ 不能进 vector——unique_ptr 禁拷贝但可移动,vector<unique_ptr<T>> 完全合法,扩容走移动构造。
  7. move 不移动,forward 才有条件——std::move 只是转型,真正搬资源的是移动构造;通用转发必须用 forward,用 move 会误伤左值。
  8. wait 一定要带谓词——cv.wait(ul, pred) 内部是 while 循环检查,天然免疫虚假唤醒;单参 wait 配 if 是经典错误。
  9. 谁不依赖对方活着,谁就用 weak——打破循环引用时,子→父用 weak、资源→管理器用 weak,方向选错等于没改。
  10. 先测量,再优化——90% 的直觉优化是浪费;性能话题先答“用 profiler 找热点”,再谈缓存、分支与内存池。