3、C++基础回顾与SLAM常用语法:指针与引用、类与继承、STL容器(vector, map)、智能指针、OpenCV基础操作

做SLAM这几年,我越来越觉得C++基础就是你的内功。算法思路再漂亮,写出来的代码跑不动、内存泄漏、或者效率低下,那都是白搭。今天这一讲,咱们就把SLAM工程里最常用的C++语法过一遍。嗯,都是实战中天天碰到的。

3.1 指针与引用:别搞混了

先说指针和引用。很多新手问我:「这俩到底啥区别?」说白了,指针是一个变量,存的是地址;引用是一个别名,本质上是另一个变量的「小名」。

核心区别:

  • 指针可以为空(nullptr),引用必须初始化且不能为空
  • 指针可以重新指向别的对象,引用一旦绑定就不能改
  • 指针有多级(int**),引用只有一级

我在项目中遇到过最坑的事:用指针传参,结果函数内部把指针指向了别处,外部完全不知道。后来我习惯用引用传参,尤其是大对象,既避免了拷贝开销,又不会出现「指针被篡改」的问题。

// 我推荐这样写
void updatePose(Eigen::Matrix4d& pose, const Eigen::Vector3d& delta) {
    pose.block<3,1>(0,3) += delta;  // 直接修改原对象
}

// 而不是这样
void updatePose(Eigen::Matrix4d* pose, const Eigen::Vector3d* delta) {
    if (pose == nullptr || delta == nullptr) return;  // 还得判空
    pose->block<3,1>(0,3) += *delta;
}

我的习惯:函数参数用const引用传输入,用非const引用传输出。指针只在「可能为空」或「需要重新指向」时才用。

3.2 类与继承:SLAM里的抽象思维

SLAM系统里,类是最基本的组织单元。你想想看,一个完整的SLAM系统有多少东西?前端、后端、回环检测、地图管理……每个模块都是一个类。

我一般这样设计基类:

class VisualOdometryBase {
public:
    VisualOdometryBase() = default;
    virtual ~VisualOdometryBase() = default;  // 虚析构,重要!

    virtual bool processFrame(const cv::Mat& image) = 0;  // 纯虚函数
    virtual Eigen::Matrix4d getPose() const = 0;

protected:
    Eigen::Matrix4d current_pose_ = Eigen::Matrix4d::Identity();
    std::vector<cv::KeyPoint> keypoints_;
};

为什么要用继承?因为SLAM的算法太多了。今天用ORB特征,明天想试试SuperPoint,后天又想换光流法。只要继承同一个基类,上层代码完全不用改。这就是多态的魅力。

我曾经踩过的坑:基类的析构函数忘了加virtual。结果子类对象被基类指针删除时,子类的资源没释放,内存泄漏查了我两天。嗯,从那以后我写基类第一件事就是加virtual析构。

3.3 STL容器:vector和map是左膀右臂

STL容器在SLAM里用得太多太多了。我个人最常用的是vector和map,几乎每个工程文件里都有它们的身影。

vector:动态数组,存特征点、存位姿

std::vector<cv::KeyPoint> kps;          // 特征点
std::vector<Eigen::Vector3d> landmarks; // 地图点
kps.reserve(1000);  // 预分配,减少频繁扩容

你想想看,SLAM里特征点数量动不动就上千。如果vector频繁扩容,性能损耗不小。我习惯在知道大概数量时用reserve预分配空间。

map:字典,存匹配关系、存关键帧索引

std::map<int, cv::DMatch> matches;  // 特征匹配
std::unordered_map<int, Frame::Ptr> keyframes;  // 关键帧数据库

map和unordered_map的区别:map有序但慢(红黑树),unordered_map无序但快(哈希表)。如果你不需要排序,用unordered_map。我在回环检测里就用的unordered_map,查找速度能快一个数量级。

容器 底层结构 查找速度 适用场景
std::vector 动态数组 O(1)随机访问 特征点、位姿序列
std::map 红黑树 O(log n) 需要有序的键值对
std::unordered_map 哈希表 O(1)平均 快速查找,无序

3.4 智能指针:再也不用担心内存泄漏

说到内存管理,智能指针绝对是C++11以来最伟大的发明之一。在SLAM里,我几乎不用裸指针了。

#include <memory>

class Frame {
public:
    using Ptr = std::shared_ptr<Frame>;
    // ...
};

// 创建共享指针
Frame::Ptr frame = std::make_shared<Frame>();

// 创建唯一指针
std::unique_ptr<VisualOdometryBase> vo = std::make_unique<ORBVO>();

shared_ptr和unique_ptr怎么选?我的原则很简单:

  • 如果对象只有一个所有者(比如VO实例),用unique_ptr
  • 如果对象被多处共享(比如关键帧、地图点),用shared_ptr
  • 千万别用auto_ptr,那玩意儿已经被废弃了

避坑指南:shared_ptr循环引用会导致内存泄漏。比如两个类互相持有对方的shared_ptr。解决办法是用weak_ptr打破循环。我在设计地图点和关键帧的关系时就遇到过这个问题。

3.5 OpenCV基础操作:图像处理是SLAM的入口

SLAM的第一步就是处理图像。OpenCV是绕不开的工具。我整理了几个最常用的操作:

// 读取图像
cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);

// 图像缩放(构建图像金字塔)
cv::Mat small_img;
cv::resize(img, small_img, cv::Size(), 0.5, 0.5);

// 特征提取
cv::Ptr<cv::ORB> orb = cv::ORB::create(1000);
std::vector<cv::KeyPoint> kps;
cv::Mat descriptors;
orb->detectAndCompute(img, cv::noArray(), kps, descriptors);

// 特征匹配
cv::BFMatcher matcher(cv::NORM_HAMMING);
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);

这里要注意几个细节:

  • SLAM里一般用灰度图,彩色图信息冗余还占内存
  • 图像金字塔的层数和尺度因子要根据实际场景调,我一般用4层,尺度因子1.2
  • 特征匹配后一定要做筛选,直接用所有匹配会引入大量外点

我曾经犯过的错:直接用cv::imread读彩色图,然后转灰度。后来发现直接读灰度图能省一半内存和加载时间。别小看这点优化,在嵌入式设备上就是生死之别。

3.6 知识体系总览

为了让你更直观地理解这一章的内容,我画了一张图。它展示了C++基础语法在SLAM工程中的位置和关系。

SLAM工程中的C++核心语法 指针与引用 传参、避免拷贝 空指针检查 类与继承 多态、虚函数 模块化设计 STL容器 vector、map unordered_map 智能指针 shared_ptr unique_ptr OpenCV基础 图像读写、特征提取 特征匹配 SLAM工程应用 前端跟踪 · 后端优化 · 回环检测 · 地图构建

这张图想表达的是:指针引用、类继承、STL容器、智能指针和OpenCV,这五块内容不是孤立的。它们共同构成了SLAM工程的代码骨架。你写前端跟踪要用OpenCV和vector,写后端优化要用类和智能指针,写回环检测要用map。缺了哪一块,代码都会别扭。

好了,这一章的内容就到这里。C++基础是SLAM工程的基石,别嫌它简单,真正写起来处处是坑。多写、多练、多踩坑,慢慢就有感觉了。


公众号:蓝海资料掘金营,微信deep3321