前言:
本文以开源ORB_SLAM2为例,分析Scott Meyers大佬的Effective C++,同时还用到了视觉slam十四讲高博的开源代码。使用这些素材其实是因为我最近也在学习SLAM,发现自己对SLAM代码中的许多语法不明所以。希望这个系列能给大家带来收获,我们一起进步!
资料链接:
- 链接: https://pan.baidu.com/s/16eN1vPq3Et1LEpXWlwColQ?pwd=hzfn 提取码: hzfn 复制这段内容后打开百度网盘手机App,操作更方便哦
- 内含:ORB_SLAM2代码,kitti数据集 2个G, Effective C++ PDF
目录
<1> 四大联邦 - 四大次语言(只是建立次语言概念,不细讲知识点)
🥝 条款02:尽量以const, enum, inline 替换 #define
条款10:令operator= 返回一个reference to *this (只是代码规范
条款01:视C++为一个语言联邦
<1> 四大联邦 - 四大次语言(只是建立次语言概念,不细讲知识点)
C C语言
Object-Oriented C++ 面向对象: 封装、继承、多态
Template C++ 模板 范性编程 generic programming
STL 容器、迭代器、算法
<2> 四大联邦守则
C 内置类型 pass-by-value 传递值
Object-Oriented C++ 用户自定义类型 pass-by-reference-const 传递常量引用,通过引用避免开辟新的内存,因为自定义类型(结构体、类等)体积较大
STL pass-by-value 但我看到string还是pass-by-reference-const
/***以我最近学习的视觉SLAM工程中的相机类为例***/
// 注意力放在Camera类的构造函数上
#ifndef CAMERA_H
#define CAMERA_H
#include <opencv2/core/core.hpp>
#include <sophus/se3.hpp>
class Camera{
public:
double fx_ = 0, fy_ = 0, cx_ = 0, cy_ = 0;
Sophus::SE3d pose_;
Camera(double fx, double fy, double cx, double cy, const Sophus::SE3d &pose):
fx_(fx), fy_(fy), cx_(cx), cy_(cy), pose_(pose)
{}
// 内置类型 double 使用 pass-by-value; Sophus库自定义类型使用 pass-by-reference-const
public:
cv::Mat toMat(const Sophus::SE3d &T_se3_mat);
/***不全***/
}; // Camera
#endif // CAMERA_H
🥝 条款02:尽量以const, enum, inline 替换 #define
宁可以编译器代替与处理器:
const全局常量代替宏定义#define 预编译报错远离我
如果你被宏的小括号与莫名奇妙的结果逼疯了,如果你的函数体小且调用频繁,就在头文件用inline关键字修饰你的函数并实现它,成为放进编译器而不再需要在cpp中定义的内联函数。
它是类专属的“宏定义”: enum{变量名 = 值} // 不需要声明变量类型。如果你既希望全局的宏能有作用域,为某个类所私有,又能像宏一样得不到地址只需要得到值(并且暗示后来者与后来的自己这一点),那就用enum关键字
inline函数一定置于头文件内,一定用在小型、被频繁调用的函数上
// converters 接口的函数体小,又会被频繁调用,所以用inline关键字修饰的内联函数
inline Vec2 toVec2(const cv::Point2f p) { return Vec2(p.x, p.y); }
P45声明后立刻要用它的值,就用enum解决
// orb-slam2使用enum关键字的例子
class Tack(){
public:
// Tracking states
enum eTrackingState{
SYSTEM_NOT_READY=-1,
NO_IMAGES_YET=0,
NOT_INITIALIZED=1,
OK=2,
LOST=3
};
};
……
// 用在了状态判断上:
if(mState==NOT_INITIALIZED || mState==NO_IMAGES_YET)
inline关键字在以下几个类中被使用:ORBextractor ORB提取器(六个内联函数,用于计算特征点与描述子), Frame(获取相机中心,求旋转矩阵的逆), MapPoint(路标,ORB匹配上的点)
// frontend.h
enum class FrontendStatus { INITING, TRACKING_GOOD, TRACKING_BAD, LOST };
// frontend.cpp
// bool Frontend::Track()
// 判断前端的状态是好的,还是有哪种错误
if (tracking_inliers_ > num_features_tracking_) {
// tracking good
status_ = FrontendStatus::TRACKING_GOOD;
} else if (tracking_inliers_ > num_features_tracking_bad_) {
// tracking bad
status_ = FrontendStatus::TRACKING_BAD;
} else {
// lost
status_ = FrontendStatus::LOST;
}
// bool Frontend::AddFrame(type)
// 检查视觉里程计前端运行情况:位于Frontend类
switch (status_) {
case FrontendStatus::INITING:
StereoInit();
break;
case FrontendStatus::TRACKING_GOOD:
case FrontendStatus::TRACKING_BAD:
Track();
break;
case FrontendStatus::LOST:
Reset();
break;
}
🌈 条款03:尽可能用const
- 给对象加const,常量:如果你担心你的开发者用户以及你自己错误地对某变量赋值,那就在相应的作用域内使用const关键字(注意本书的“对象”包括变量)。(可以用在参数、全局/局部变量,甚至返回值上)
- 给指针加const,常量指针:const出现在"*"左边是给对象的,在右边是给指针的
- 给成员函数加const,常量成员函数:{函数体} 前加const,使其无法对类内成员赋值,除非是加了mutable关键字的成员
P49返回值加const有什么意义,一是避免赋值出错,二是作为注释提示这一性质
P54利用const关键字修饰返回类型进行函数重载。常量对象调用返回常量对象的函数,反之非常量对象调用返回非常量对象的函数
🍊条款04: 确定对象使用前已被初始化
构造函数写成列表初始化效率更高,注意顺序:初始化顺序与成员变量声名顺序有关,与初始化列表先后顺序无关,比如数组长度要在数组之前完成初始化
如果有跨编译单元(可以理解为不同的cpp文件)的初始化,就用Singleton模式,即设计一个全局函数,在这个函数内完成对象初始化,静态的,然后返回对象的引用(reference-returning)。
最妙的是该函数返回的是一个对象引用,后面可以直接接上成员函数。
问题来了,为什么对象fs初始化一定要放在一个函数tfs()里呢?我没有找到有说服力的答案,我个人认为是因为比起在Directory构造函数内加一行fs初始化再使用fs.numDisks(),tfs().numDisks()只要一行,更优雅……
条款05 (大部分教程里都有,故略写)
成员变量不要使用引用和常量,否则赋值时将会出错(C++不允许reference改变指向,也不允许更改const成员)。
就算你不写,编译器也会自动生产defult构造函数,copy构造函数,析构函数,copy assignment操作符(也就是operator重载等号)
条款06 :明确地拒绝复制(这不是原标题,我自己总结的)
如果要杜绝对象copy(每一个对象都必然独一无二的时候)就显式地把copy构造函数、copy assignment操作符写在private下面。或者建一个Uncopyable基类继承它(见下图)
P69这样一来,任何复制行为都会以失败收场,错误将被及时发现
orb-slam2中不能复制的东西很多,Camera, System, Tacking, Viewer但是作者完全没有考虑防复制
条款07:多态基类声明virtual析构函数
多态基类ploymorphic base classes应该声明一个virtual析构函数,以免基类指针base pointer指向派生类derived class对象(背景:只想使用派生类类中基类的成员)然后delete释放指针时内存泄漏(解释:只释放了基类部分)。
如果class带有虚函数,最好有一个virtual析构函数,防止在遇到上一段的情形时出错
不是基类或无需多态的类,不该声明virtual析构函数
然而我在测试时发现,slam的作者对析构函数是完全忽略的,也就是在一个slam方案中,所以析构函数都是缺省实现的。所以我想看看slam要不要在对象析构的时候手动释放堆区内存,在哪里释放的?
很快我找到了,答案是利用智能指针管理堆区内存,有兴趣的读者参见我下一篇文章的条款13
条款08 :别让异常逃离析构函数
在析构函数里出现问题,用try-catch结构吞掉这个错误
Class::~Class(){
try{报错代码}
catch(你发现的那个错误类型){
/*生成报错日志*/
std::abort(); // 强制结束程序,避免错误进一步传播导致令人眼花缭乱的报错页面
}
}
🍉条款09:绝不在构造和析构函数内调用虚函数
因为派生类调用的是基类的构造函数,基类构造期间虚函数绝不会下降到派生类,用的还是基类的虚函数:“基类构造期间,虚函数不是虚函数”。析构函数同理。
派生类构造函数是总结可以参考这篇文章:C++派生类的构造函数总结-CSDN博客
不使用虚函数,将派生类必要的构造信息上传至基类构造函数。在派生类的成员初始列表中给予基类所需数据,构建private static辅助函数传递值(加辅助函数只是因为可读性高,加static关键字是因为静态成员编译阶段就分配内存,在构造中使用静态成员不会出现派生类还没构造、初始化就使用派生类成员的问题)
P81 其中Transaction是基类,BuyTransaction是派生类