本文覆盖 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 reserveresize改变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 = new Derived();
delete p;  // 如果~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(new Preprocess());   // 多处使用预处理器
shared_ptr<ImuProcess> p_imu(new ImuProcess());   // 多处使用IMU处理器

// 点云用裸指针(PCL的Ptr本质上是shared_ptr)
PointCloudXYZI::Ptr feats_undistort(new PointCloudXYZI());
// 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)

编译器允许的赋值场景:

  1. 显式 std::move:如上
  2. 函数返回局部变量(NRVO/移动语义):
unique_ptr<Foo> createFoo() {
    auto p = make_unique<Foo>();
    return p;  // ✅ 编译器自动move(返回值优化)
}
  1. 临时对象赋值
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;

void imu_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问题
STLstd::stackstd::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模拟bool
std::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_back

15. map vs unordered_map

答案

维度std::mapstd::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 问题:

容器中间插入随机访问
vectorO(n)(需要搬移)O(1)
listO(1)(已定位)O(n)(不支持随机访问)
dequeO(n)(中间插入)O(1)

推荐方案

  1. std::deque:如果插入主要在头尾附近,deque 是好选择
  2. std::list + 缓存迭代器:如果确实需要频繁中间插入,用 list 并缓存常用位置的迭代器
  3. std::vector + 标记删除:如果访问极频繁而插入不多,用 vector + 惰性删除(标记而非真正删除)
  4. 考虑 std::dequeboost::container::flat_set:分段连续内存,折中方案

面试追问的"标准答案":如果同时需要快速随机访问和快速中间插入,考虑 std::deque(折中)或根据实际访问模式选择。没有完美方案,需要根据具体的 读/写比例 权衡。


17. 左值、右值与右值引用

答案

左值 (lvalue):有持久身份的表达式,可以取地址 &x

int x = 10;      // x 是左值
int& ref = x;    // 左值引用

右值 (rvalue):临时的、即将销毁的表达式

int&& rref = 10;           // 右值引用绑定到临时量
int&& rref2 = x + 1;      // x+1 的结果是临时量(右值)

右值引用的使用场景

1. 移动语义(避免深拷贝)

class PointCloud {
    vector<Point> points;
public:
    // 移动构造函数:偷走临时对象的资源
    PointCloud(PointCloud&& other) noexcept 
        : points(std::move(other.points)) {}
};

PointCloud processCloud() {
    PointCloud cloud;
    // ... 处理
    return cloud;  // 移动而非拷贝(或NRVO直接省略)
}

2. 完美转发(模板编程)

template<typename T>
void wrapper(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 使用移动语义:
return move(rot_kp);  // 避免拷贝 Pose6D 结构体

// Eigen 矩阵的移动(Eigen 3.4+ 支持移动语义)
Eigen::MatrixXd computeH() {
    Eigen::MatrixXd H(n, 12);
    // ... 计算
    return H;  // 移动返回,无需拷贝大矩阵
}

附:SLAM工程中的C++最佳实践总结

场景推荐做法
点云传递PointCloud::Ptr(shared_ptr)
矩阵参数const Eigen::Ref<const MatrixXd>&
数据缓冲区std::deque + mutex + condition_variable
高频小对象栈分配 or 对象池
配置参数const & 传递
回调函数std::function or 模板(无虚函数开销)
并行计算OpenMP #pragma omp parallel for
内存对齐EIGEN_MAKE_ALIGNED_OPERATOR_NEW

面试真题与答题要点

Q: 面试官:“说说 SLAM 项目中你用到了哪些 C++ 特性?”

答题模板

在我的 FAST-LIO2 项目中:

  1. 模板编程:SO(3) 数学库全部用模板实现(so3_math.h),支持 float/double
  2. 智能指针:点云用 shared_ptr 管理生命周期,避免手动内存管理
  3. 多线程:IMU回调和主处理循环通过 mutex + condition_variable 同步
  4. STL容器:IMU缓冲区用 deque(双端操作),最近邻结果用 vector(随机访问)
  5. Eigen内存对齐EIGEN_MAKE_ALIGNED_OPERATOR_NEW 避免SSE指令的段错误
  6. OpenMP并行:点云配准中的最近邻搜索和残差计算并行化

延伸阅读

  • 书籍:Scott Meyers, “Effective Modern C++”(C++11/14最佳实践)
  • 书籍:Anthony Williams, “C++ Concurrency in Action”(并发编程)
  • 参考:cppreference.com(权威语言参考)
  • 实践:阅读 PCL / Eigen / Sophus 源码学习模板和内存管理

更多推荐