SLAM 岗位 C++ 面试速查手册
本文覆盖 SLAM 算法岗面试中最常见的 17 道 C++ 基础题,每题给出标准答案 + 面试官常见追问的应对策略。
面试速查(30秒版)
| 知识点 | 一句话核心 |
|---|---|
| 面向对象 vs 面向过程 | OOP通过封装/继承/多态管理复杂度,POP以函数为中心更高效 |
| 指针 vs 引用 | 引用是别名不可变不可空,指针是地址可重指可为空 |
| 多态 | 运行时多态靠虚函数表(vtable)实现动态绑定 |
| 虚/纯虚函数 | 虚函数有默认实现可覆盖,纯虚函数无实现必须覆盖 |
| 析构函数为虚 | 基类指针delete派生对象时确保析构完整 |
| 4种类型转换 | static快但不安全,dynamic慢但安全(RTTI),const去常量,reinterpret底层位模式 |
| 智能指针 | unique独占,shared共享(引用计数),weak打破循环引用 |
| 锁 | mutex互斥,shared_mutex读写,condition_variable条件等待 |
| 堆 vs 栈 | 栈自动管理快速连续,堆手动管理灵活离散 |
| STL容器 | vector连续随机访问,list离散快速插删,map有序log(n),unordered_map哈希O(1) |
| vector<bool> | 是位压缩特化,非真正容器,不支持指针/引用 |
| resize vs reserve | resize改变size(可访问),reserve改变capacity(预分配) |
| 左值/右值 | 左值有地址可取&,右值是临时量;右值引用实现移动语义避免拷贝 |
1. 面向过程 vs 面向对象编程
答案
面向过程(POP):以函数/过程为核心组织代码,数据和操作分离。
- 优点:执行效率高、逻辑直观、适合小程序
- 缺点:数据和函数耦合松散,大型系统难维护
面向对象(OOP):以类/对象为核心,通过封装、继承、多态组织代码。
- 优点:高内聚低耦合、代码复用、适合大型系统
- 缺点:设计开销大、运行时有虚函数表等间接开销
SLAM 工程中的体现
- PCL、OpenCV 大量使用面向对象(继承体系)
- 但 FAST-LIO2 中
so3_math.h的模板函数是面向过程风格(追求效率) - 实际工程中两者结合:框架用 OOP,核心计算用 POP/模板
2. 指针和引用的区别
答案(5个核心区别)
| 维度 | 指针 | 引用 |
|---|---|---|
| 本质 | 存储变量地址的变量 | 变量的别名 |
| 可否为空 | 可以为 nullptr | 不能为空,声明时必须初始化 |
| 可否重新绑定 | 可以指向其他对象 | 一旦绑定不可更改 |
| sizeof | 指针本身大小(64位=8字节) | 所引用对象的大小 |
| 多级 | 有多级指针int** | 无多级引用 |
| 自增 | 地址偏移 | 值递增 |
追问:什么时候用指针什么时候用引用?
- 参数可能为空 → 指针
- 需要重新绑定/多态所有权 → 指针
- 函数参数传递避免拷贝 → const引用
- 运算符重载返回值 → 引用
3. 什么是多态?虚函数和纯虚函数的区别?
答案
多态:同一接口不同实现。分为:
- 编译时多态:函数重载、模板
- 运行时多态:虚函数 + 基类指针/引用
虚函数 vs 纯虚函数:
| 维度 | 虚函数 | 纯虚函数 |
|---|---|---|
| 声明 | virtual void f() {} | virtual void f() = 0; |
| 基类能否实例化 | 能 | 不能(抽象类) |
| 是否必须重写 | 不必须 | 派生类必须重写 |
| 用途 | 提供默认实现 | 定义接口规范 |
虚函数表(vtable)机制
Base* p = new Derived(); p->func(); // 通过 vtable 查找实际调用的函数 对象内存布局: +------------------+ | vptr → vtable | ← 指向虚函数表的指针 | member data | +------------------+ vtable: +------------------+ | &Derived::func() | ← 被覆盖,指向派生类实现 | &Base::other() | ← 未覆盖,仍指向基类 +------------------+4. 为什么基类析构函数建议定义为虚函数?
答案
Base*p=newDerived();deletep;// 如果~Base()不是虚函数,只调用Base的析构,Derived部分内存泄漏!当通过基类指针delete派生类对象时:
- 非虚析构:只调用基类析构函数 → 派生类资源泄漏
- 虚析构:通过 vtable 找到派生类析构函数,正确释放所有资源
注意:纯虚析构函数virtual ~Base() = 0;也必须提供定义(因为派生类析构会隐式调用基类析构)。
5. C++ 的4种类型转换
答案
| 转换 | 用途 | 运行时开销 | 安全性 |
|---|---|---|---|
static_cast | 编译时已知的类型转换(基本类型、上行转换) | 无 | 中(不检查运行时类型) |
dynamic_cast | 多态类的安全下行转换 | 有(RTTI) | 高(失败返回nullptr) |
const_cast | 去除/添加 const/volatile | 无 | 低(滥用导致UB) |
reinterpret_cast | 底层位模式重解释 | 无 | 最低 |
6. 基类指针调用派生类对象时用哪种转换?
答案
下行转换(Base→ Derived)用dynamic_cast**:
Base*base=getObject();Derived*d=dynamic_cast<Derived*>(base);if(d!=nullptr){d->derivedMethod();}- 需要基类有虚函数(开启RTTI)
- 失败时返回
nullptr(指针)或抛出bad_cast(引用)
上行转换(Derived→ Base)用static_cast** 或隐式转换即可。
7. dynamic_cast vs static_cast 哪个效率高?
答案
static_cast效率更高。
原因:
static_cast:编译时完成,零运行时开销dynamic_cast:需要 RTTI(Run-Time Type Information),运行时遍历类继承链检查类型,有开销
但static_cast的下行转换是不安全的(不检查实际类型),如果类型不匹配会导致未定义行为。
SLAM 工程实践:
- 高频调用(如每个点的处理)→ 用
static_cast(确保类型正确的前提下) - 低频调用(如传感器类型判断)→ 用
dynamic_cast(安全优先)
8. 智能指针有哪几种?区别是什么?
答案
| 智能指针 | 语义 | 引用计数 | 典型场景 |
|---|---|---|---|
unique_ptr | 独占所有权 | 无 | 工厂函数返回值、独占资源 |
shared_ptr | 共享所有权 | 有(线程安全的原子计数) | 多处共用同一资源 |
weak_ptr | 弱引用(不增加计数) | 观察shared_ptr | 打破循环引用、缓存 |
SLAM 工程中的实例
// FAST-LIO2 中:shared_ptr<Preprocess>p_pre(newPreprocess());// 多处使用预处理器shared_ptr<ImuProcess>p_imu(newImuProcess());// 多处使用IMU处理器// 点云用裸指针(PCL的Ptr本质上是shared_ptr)PointCloudXYZI::Ptrfeats_undistort(newPointCloudXYZI());// PCL中 Ptr = boost::shared_ptr<PointCloud>追问:shared_ptr 的性能问题?
- 引用计数的原子操作有开销(多线程下尤其明显)
- 内存布局:控制块(计数器)和数据可能不连续(除非用
make_shared) make_shared一次分配控制块+对象,更高效
9. unique_ptr 什么时候允许赋值?
答案
unique_ptr禁止拷贝(删除了拷贝构造和拷贝赋值),但允许移动:
unique_ptr<int>p1=make_unique<int>(42);// unique_ptr<int> p2 = p1; // ❌ 编译错误unique_ptr<int>p2=std::move(p1);// ✅ 移动(p1变为nullptr)编译器允许的赋值场景:
- 显式
std::move:如上 - 函数返回局部变量(NRVO/移动语义):
unique_ptr<Foo>createFoo(){autop=make_unique<Foo>();returnp;// ✅ 编译器自动move(返回值优化)}- 临时对象赋值:
unique_ptr<int>p=make_unique<int>(10);// ✅ 右值直接构造10. C++ 中提供了哪些锁?
答案
| 锁/同步原语 | 头文件 | 特点 | 适用场景 |
|---|---|---|---|
std::mutex | <mutex> | 基本互斥锁,非递归 | 通用互斥访问 |
std::recursive_mutex | <mutex> | 可递归加锁(同线程多次lock) | 递归函数中的互斥 |
std::timed_mutex | <mutex> | 支持超时的互斥锁 | 避免死锁等待 |
std::shared_mutex(C++17) | <shared_mutex> | 读写锁(多读单写) | 读多写少场景 |
std::condition_variable | <condition_variable> | 条件等待 | 生产者-消费者 |
std::atomic | <atomic> | 无锁原子操作 | 简单计数器/标志位 |
SLAM 工程实例
// FAST-LIO2 中的数据缓冲区保护:mutex mtx_buffer;condition_variable sig_buffer;voidimu_cbk(...){mtx_buffer.lock();imu_buffer.push_back(msg);mtx_buffer.unlock();sig_buffer.notify_all();// 通知主循环有新数据}RAII锁管理
// 推荐用 lock_guard/unique_lock,不要手动 lock/unlock{std::lock_guard<std::mutex>lock(mtx_buffer);// 构造时加锁,析构时解锁imu_buffer.push_back(msg);}// 离开作用域自动解锁,异常安全11. 堆与栈的区别
从内存管理角度
| 维度 | 栈 (Stack) | 堆 (Heap) |
|---|---|---|
| 分配/释放 | 编译器自动管理(LIFO) | 程序员手动(new/delete)或智能指针 |
| 速度 | 极快(移动栈指针) | 较慢(系统调用/内存碎片) |
| 大小 | 有限(通常1-8MB) | 很大(受物理内存限制) |
| 地址增长 | 向低地址增长 | 向高地址增长 |
| 碎片 | 无 | 有外部碎片 |
| 生命周期 | 函数结束自动回收 | 手动释放或引用计数为0 |
从数据结构角度
| 维度 | 栈 (Stack) | 堆 (Heap) |
|---|---|---|
| 结构 | LIFO(后进先出) | 完全二叉树(优先队列) |
| 操作 | push/pop O(1) | insert/extract O(log n) |
| 应用 | 函数调用、DFS、括号匹配 | 优先队列、TopK问题 |
| STL | std::stack | std::priority_queue |
12. STL 容器有哪些?
答案
| 类别 | 容器 | 底层实现 | 随机访问 | 插入/删除 |
|---|---|---|---|---|
| 序列 | vector | 动态数组 | O(1) | 尾部O(1),中间O(n) |
| 序列 | deque | 分段数组 | O(1) | 头尾O(1),中间O(n) |
| 序列 | list | 双向链表 | O(n) | 任意位置O(1)(已定位) |
| 关联 | map/set | 红黑树 | — | O(log n) |
| 关联 | unordered_map/set | 哈希表 | — | 平均O(1),最差O(n) |
| 适配器 | stack/queue/priority_queue | 封装底层容器 | — | — |
13. vector<int> vs vector<bool> 的区别
答案
vector<bool>是一个特化版本,不是真正的 STL 容器:
| 维度 | vector<int> | vector<bool> |
|---|---|---|
| 存储 | 每个元素占4字节 | 每个元素占1 bit(位压缩) |
operator[] | 返回int&(真正的引用) | 返回代理对象reference(非真引用) |
| 取地址 | &v[0]合法 | &v[0]不合法 |
| 与C数组互操作 | v.data()可用 | 不支持 |
| 迭代器 | 随机访问迭代器 | 特殊迭代器 |
替代方案
// 如果需要真正的bool容器:std::deque<bool>// 不做位压缩std::vector<char>// 用char模拟boolstd::bitset<N>// 编译时大小确定的位集合14. resize 和 reserve 的区别
答案
vector<int>v;v.reserve(100);// capacity=100, size=0, 不能v[50]访问v.resize(100);// capacity≥100, size=100, 可以v[50]访问(值初始化为0)| 操作 | 改变 size | 改变 capacity | 可访问元素 | 构造元素 |
|---|---|---|---|---|
reserve(n) | 否 | 是(≥n) | 不变 | 否 |
resize(n) | 是(=n) | 可能(如果n>capacity) | 增加/减少 | 是(默认构造) |
SLAM 实践
// FAST-LIO2 中预分配,避免频繁重新分配:Nearest_Points.resize(feats_down_size);// 需要访问每个元素PointToAdd.reserve(feats_down_size);// 只是预分配空间,后续push_back15. map vs unordered_map
答案
| 维度 | std::map | std::unordered_map |
|---|---|---|
| 底层 | 红黑树(自平衡BST) | 哈希表(拉链法/开放寻址) |
| 查找/插入/删除 | O(log n) | 平均 O(1),最差 O(n) |
| 有序性 | key有序遍历 | 无序 |
| 内存 | 每节点含左右指针+颜色 | bucket数组+链表 |
| key要求 | 需要operator< | 需要hash+operator== |
时间效率选择
- 绝大多数场景用
unordered_map:O(1) vs O(log n) - 需要有序遍历时用
map - 数据量小(<100)时
map可能更快(cache友好、无哈希开销)
哈希冲突与性能退化
// 最差情况:所有key哈希到同一bucket → O(n)// 解决:// 1. 好的哈希函数// 2. 预留足够bucket: m.reserve(n)// 3. 控制负载因子: m.max_load_factor(0.7)16. 频繁插入中间且频繁访问,用什么容器?
答案
这是一个经典的 tradeoff 问题:
| 容器 | 中间插入 | 随机访问 |
|---|---|---|
vector | O(n)(需要搬移) | O(1) |
list | O(1)(已定位) | O(n)(不支持随机访问) |
deque | O(n)(中间插入) | O(1) |
推荐方案:
std::deque:如果插入主要在头尾附近,deque 是好选择std::list+ 缓存迭代器:如果确实需要频繁中间插入,用 list 并缓存常用位置的迭代器std::vector+ 标记删除:如果访问极频繁而插入不多,用 vector + 惰性删除(标记而非真正删除)- 考虑
std::deque或boost::container::flat_set:分段连续内存,折中方案
面试追问的"标准答案":如果同时需要快速随机访问和快速中间插入,考虑std::deque(折中)或根据实际访问模式选择。没有完美方案,需要根据具体的 读/写比例 权衡。
17. 左值、右值与右值引用
答案
左值 (lvalue):有持久身份的表达式,可以取地址&x
intx=10;// x 是左值int&ref=x;// 左值引用右值 (rvalue):临时的、即将销毁的表达式
int&&rref=10;// 右值引用绑定到临时量int&&rref2=x+1;// x+1 的结果是临时量(右值)右值引用的使用场景
1. 移动语义(避免深拷贝):
classPointCloud{vector<Point>points;public:// 移动构造函数:偷走临时对象的资源PointCloud(PointCloud&&other)noexcept:points(std::move(other.points)){}};PointCloudprocessCloud(){PointCloud cloud;// ... 处理returncloud;// 移动而非拷贝(或NRVO直接省略)}2. 完美转发(模板编程):
template<typenameT>voidwrapper(T&&arg){// 万能引用realFunction(std::forward<T>(arg));// 完美转发:保持左/右值性}3. STL容器的 emplace 系列:
vector<Pose>poses;poses.emplace_back(rotation,translation);// 直接原地构造,无临时对象// 比 push_back(Pose(rotation, translation)) 少一次移动SLAM 中的实际应用
// FAST-LIO2 中 set_pose6d 使用移动语义:returnmove(rot_kp);// 避免拷贝 Pose6D 结构体// Eigen 矩阵的移动(Eigen 3.4+ 支持移动语义)Eigen::MatrixXdcomputeH(){Eigen::MatrixXdH(n,12);// ... 计算returnH;// 移动返回,无需拷贝大矩阵}附:SLAM工程中的C++最佳实践总结
| 场景 | 推荐做法 |
|---|---|
| 点云传递 | PointCloud::Ptr(shared_ptr) |
| 矩阵参数 | const Eigen::Ref<const MatrixXd>& |
| 数据缓冲区 | std::deque+mutex+condition_variable |
| 高频小对象 | 栈分配 or 对象池 |
| 配置参数 | const &传递 |
| 回调函数 | std::functionor 模板(无虚函数开销) |
| 并行计算 | OpenMP#pragma omp parallel for |
| 内存对齐 | EIGEN_MAKE_ALIGNED_OPERATOR_NEW |
面试真题与答题要点
Q: 面试官:“说说 SLAM 项目中你用到了哪些 C++ 特性?”
答题模板:
在我的 FAST-LIO2 项目中:
- 模板编程:SO(3) 数学库全部用模板实现(
so3_math.h),支持 float/double- 智能指针:点云用
shared_ptr管理生命周期,避免手动内存管理- 多线程:IMU回调和主处理循环通过
mutex+condition_variable同步- STL容器:IMU缓冲区用
deque(双端操作),最近邻结果用vector(随机访问)- Eigen内存对齐:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW避免SSE指令的段错误- OpenMP并行:点云配准中的最近邻搜索和残差计算并行化
延伸阅读
- 书籍:Scott Meyers, “Effective Modern C++”(C++11/14最佳实践)
- 书籍:Anthony Williams, “C++ Concurrency in Action”(并发编程)
- 参考:cppreference.com(权威语言参考)
- 实践:阅读 PCL / Eigen / Sophus 源码学习模板和内存管理
