我的上一篇文章:从视觉SLAM方案中理解C++的语法条款(一)-CSDN博客
可能有助于理解这篇文章。
本文大量使用栈区堆区的知识,相关知识有较大进步空间的读者可以参考以下文章:
(简介版)C++ 学习(九)内存分区(代码区、全局区、栈区、堆区)_c++运行时 堆,栈,代码,全局区存储地址-CSDN博客
(深入版) 什么是代码区、常量区、静态区(全局区)、堆区、栈区?-CSDN博客
读者如果在阅读时有其他困难,请打在评论区,我会在文章中补上相关内容,这也是我进一步学习的动力。
目录
💫前言:
本文以开源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, 视觉SLAM十四讲从理论到实践 第2版.pdf
条款11-17可以说是多C++11中的智能指针和多线程编程原理的深入探讨,但是请注意本书是2011年以前很就就出版的,所以智能指针和多线程编程很可能就是照着本书的原理设计的。所以想要彻解slam方案中用到的智能指针和多线程编程的原理,条款11-17是值得一读的。
考虑到slam中会吞吐大量的图像数据,和从图像中提取出的信息,内存泄漏风险很高,这些条款中提到的管理资源的思想,是很有价值的。
🌈条款11:在operator=中处理自我赋值
为了避免纸上谈兵,下面搬来书中的两个类作为例子:
class Bitmap {}; // 我知道这是图片格式名,但不用在意这个名字
class Widget {
// 这里的Widget不是指qt里的Widget,不用在意类名
private:
Bitmap* pb; // Pointer of Bitmap 指向一个从堆区heap分配而得的对象
void swap(Widget* rhs); // 有自我赋值安全又有异常安全的等号重载方案2
};
有自我赋值安全但没有异常安全的等号重载(异常安全是什么在代码示例中有解释):
/// @brief 有自我赋值安全但没有异常安全的等号重载
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this; // 证同测试identity test,防止自我赋值
delete pb; // 停止使用当前的bitmap
pb = new Bitmap(*rhs.pb); // 使用rhs'bitmap的副本
/*不具备异常安全性:
一旦内存不足或者Bitmap的copy构造函数抛出异常,
pb将指向一块被删除的Bitmap,这样的指针是有害的,
既不能安全地删除pb,也不能安全地读取pb
*/
return *this;
}
有自我赋值安全又有异常安全的等号重载方案1:
/// @brief 有自我赋值安全又有异常安全的等号重载方案1
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb; // 记住原先的pb
pb = new Bitmap(*rhs.pb); // 使用rhs'bitmap的副本
delete pOrig; // 停止使用原先的bitmap
return *this;
}
有自我赋值安全又有异常安全的等号重载方案2:
void Widget::swap(Widget& rhs) // 有自我赋值安全又有异常安全的等号重载方案2
{
std::swap(this->pb, rhs.pb);
// 如果Widget类管理了不止一个资源,这里可以继续添加swap
}
/// @brief 有自我赋值安全又有异常安全的等号重载方案2
Widget& Widget::operator=(const Widget& rhs)
{
// copy-and-swap 拷贝再交换
Widget temp(rhs); // 拷贝
swap(temp); // 交换
return *this;
}
这里大量使用引用和指针(),就不得不提一下引用和指针的差异。同样储存值的地址,
引用的,值的解析是自动的、隐式的。上面代码中,体现在void swap函数中,rhs左边没有星号就解析其值,以及右边是句点。
指针的,值的解析是手动的、显式的。体现在指针变量的左边有星号才解析其值。(以及上面代码中,在void swap函数中,this右边是箭头。)
或者说,相较于引用,指针对于“是地址还是值的问题”更为较真。
🌈条款12:复制对象时勿忘其每一个部分
先准备一下示范的类:
// 高博的代码里,类内成员习惯以下划线做后缀,作为区分,构造函数的形参没有
class Base {
public:
Base(const Base& rhs)
public:
double baseMember_;
};
class Derived: public Base {
public:
double member1_;
double member2_;
double member3_;
};
然后是我们的copy构造函数:
Base::Base(const Base& rhs)
: baseMember_(rhs.baseMember_)
{
/*可以在这里运行写入日志信息的代码*/
}
Derived::Derived(const Derived& rhs)
: Base(rhs), // 初始化子类的时候别忘了父类成员的初始化
member1_(rhs.member1_),
member2_(rhs.member2_),
member3_(rhs.member3_),
{
/*可以在这里运行写入日志信息的代码*/
}
🥝条款13:以对象管理资源
资源取得时机便是初始化时机 Resource Acquisition Is Initialization (RAII,记住这个RAII,接下来会考)
在对象析构的时候释放它所管理的堆区资源,利用析构函数确保资源被释放,如果释放动作抛出异常导致在析构中出错,见条款08
为了实现上述目标,书中推荐使用智能指针std::shared_ptr与std::weak_ptr管理堆区资源
在测试智能指针之前,我们先做一点准备:
// 1-设计工厂函数,返回基类指针,指向派生类的动态分配对象
#include <memory>
#include <iostream>
using namespace std;
class TimeKeeper { // 这是一个抽象的钟
public:
TimeKeeper();
TimeKeeper(TimeKeeper& tkr);
virtual ~TimeKeeper(); // 为了实现工厂函数,需要写一个virtual的析构函数,见条款07
static void getCount();
static int _count;
};
int TimeKeeper::_count = 0;
TimeKeeper::TimeKeeper(){
++_count;
}
TimeKeeper::TimeKeeper(TimeKeeper& tkr){
++_count;
}
// 析构函数必须类外实现,否则undefined reference to `vtable for TimeKeeper'
TimeKeeper::~TimeKeeper(){
--_count;
}
void TimeKeeper::getCount(){
cout << _count << endl;
}
class AtomicClock: public TimeKeeper{
};
class WaterClock: public TimeKeeper{
};
class WristClock: public TimeKeeper{
};
这里介绍一下工厂函数:
// 工厂函数:如果只想知道时间而不关心如何计算等细节
TimeKeeper* getTimeKeeper(){
TimeKeeper* ptkr = new AtomicClock();
return ptkr;
}
大家可能有疑问,既然只使用基类的功能,那么直接生成一个基类对象不就完了吗,为什么还要搞一个工厂函数?那是因为派生类可能重写了基类的虚函数,基类对象用不了改写过的函数。
下面的测试验证了我的观点
准备阶段:
class TimeKeeper {
public:
TimeKeeper();
TimeKeeper(TimeKeeper& tkr);
virtual ~TimeKeeper();
virtual void getMaterial(){
cout << "wood" << endl;
}
};
// 重写getMaterial
class AtomicClock: public TimeKeeper{
public:
virtual void getMaterial() override {
cout << "iron" << endl;
}
};
class WaterClock: public TimeKeeper {};
class WristClock: public TimeKeeper {};
// 工厂函数:如果只想知道时间而不关心如何计算等细节S
TimeKeeper* getTimeKeeper(){
TimeKeeper* ptkr = new AtomicClock();
return ptkr;
}
测试结果与结论:
int main()
{
AtomicClock ac;
ac.getMaterial(); // 打印iron
ac.TimeKeeper::getMaterial(); // 打印wood
getTimeKeeper()->getMaterial(); // 打印iron
getTimeKeeper()->TimeKeeper::getMaterial(); // 打印wood
// 由此可知:派生类的基类部分既可以使用派生类重载过的函数,也可以使用基类原来的,但是基类对象就只有基类的了
}
接下来直接上智能指针的用法:
智能指针初始化: std::shared_ptr<T> Ptr(new T 或者 指向它的指针);
当最后一个指向new T堆区资源的智能指针析构,也就是没人用它了,该堆区资源将被释放
// 如果要初始化一个null shared ptr即一个空的智能指针
std::shared_ptr<T> ptr(static_cast<T>(0)[,自定义删除器]);
// 或者这样,高博就是这么做的
typedef std::shared_ptr<T> Ptr;
Ptr ptr = nullptr;
实战一下:
// 2-使用智能指针,释放堆区内存
// shared_ptr不能指向数组
void f(){
std::shared_ptr<TimeKeeper> pTkr1(getTimeKeeper());
cout <<TimeKeeper::_count << endl; // 打印1
std::shared_ptr<TimeKeeper> pTkr2(getTimeKeeper()); // 指向同一个资源
cout << TimeKeeper::_count << endl; // 打印2
// f()调用结束后,局部变量pTkr销毁,AtomicClock堆区内存释放
}
int main()
{
int arr[] = {1, 2, 3, 4, 5};
for(int i: arr){
f();
cout << "<" << i << ">: " << TimeKeeper::_count << endl;
}
getTimeKeeper()->getCount(); // 打印1
getTimeKeeper()->getCount(); // 打印2
getTimeKeeper()->getCount(); // 打印3
}
智能指针在slam方案中的使用,不能说很多,只能说铺天盖地,这也就是为什么条款13是用有营养的猕猴桃开头的
// 后端类
class Backend {
typedef std::shared_ptr<Backend> Ptr;
};
// 相机类
class Camera {
typedef std::shared_ptr<Camera> Ptr;
};
// 配置类
class Config {
static std::shared_ptr<Config> config_;
};
// 数据集类
class Dataset {
typedef std::shared_ptr<Dataset> Ptr;
};
// 特征类
struct Feature {
typedef std::shared_ptr<Feature> Ptr;v
std::weak_ptr<Frame> frame_; // 持有该feature的frame
std::weak_ptr<MapPoint> map_point_; // 关联地图点
};
// 帧类
struct Frame {
typedef std::shared_ptr<Frame> Ptr;
// extracted features in left image
std::vector<std::shared_ptr<Feature>> features_left_;
// corresponding features in right image, set to nullptr if no corresponding
std::vector<std::shared_ptr<Feature>> features_right_;
};
// 地图类
class Map {
typedef std::shared_ptr<Map> Ptr;
};
// 路标类
struct MapPoint {
typedef std::shared_ptr<MapPoint> Ptr;
std::list<std::weak_ptr<Feature>> observations_;
};
// 可视化类
class Viewer {
typedef std::shared_ptr<Viewer> Ptr;
};
// VO对外接口类
class VisualOdometry {
typedef std::shared_ptr<VisualOdometry> Ptr;
};
例如这个Frame对象,在Frame的成员函数Frame::Ptr Frame::CreateFrame()中,从堆区诞生并被编号,一诞生就被装进智能指针,这个智能指针先是在Dataset类的
Frame::Ptr Frame::CreateFrame() {
static long factory_id = 0;
Frame::Ptr new_frame(new Frame);
new_frame->id_ = factory_id++;
return new_frame;
}
Frame::Ptr Dataset::NextFrame()中装填opencv处理过的图片,然后进入
Frame::Ptr Dataset::NextFrame() {
boost::format fmt("%s/image_%d/%06d.png");
cv::Mat image_left, image_right;
// read images
image_left =
cv::imread((fmt % dataset_path_ % 0 % current_image_index_).str(),
cv::IMREAD_GRAYSCALE);
image_right =
cv::imread((fmt % dataset_path_ % 1 % current_image_index_).str(),
cv::IMREAD_GRAYSCALE);
if (image_left.data == nullptr || image_right.data == nullptr) {
LOG(WARNING) << "cannot find images at index " << current_image_index_;
return nullptr;
}
cv::Mat image_left_resized, image_right_resized;
cv::resize(image_left, image_left_resized, cv::Size(), 0.5, 0.5,
cv::INTER_NEAREST);
cv::resize(image_right, image_right_resized, cv::Size(), 0.5, 0.5,
cv::INTER_NEAREST);
auto new_frame = Frame::CreateFrame();
new_frame->left_img_ = image_left_resized;
new_frame->right_img_ = image_right_resized;
current_image_index_++;
return new_frame;
}
bool VisualOdometry::Step()并在其中被bool Frontend::AddFrame(type)当作参数,参数采用值传递pass-by-value,用于前端加载下一帧,用于前端的ORB特征提取
bool VisualOdometry::Step() {
Frame::Ptr new_frame = dataset_->NextFrame();
if (new_frame == nullptr) return false;
auto t1 = std::chrono::steady_clock::now();
bool success = frontend_->AddFrame(new_frame);
auto t2 = std::chrono::steady_clock::now();
auto time_used =
std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
LOG(INFO) << "VO cost time: " << time_used.count() << " seconds.";
return success;
}
这份Frame堆区资源,一直(有各个函数中)作为局部变量的智能指针指着,函数运行结束,指针析构,但是每次都有另一个指针接收它,薪火相传。(换句话说,不同的栈区指针指向同一份堆区资源。这使得各个函数得以引用这份资源,而又不会复制资源,造成内存的浪费。想必这份资源是很费内存的,比如一帧图像)
bool Frontend::AddFrame(myslam::Frame::Ptr frame) {
current_frame_ = frame;
switch (status_) {
case FrontendStatus::INITING:
StereoInit();
break;
case FrontendStatus::TRACKING_GOOD:
case FrontendStatus::TRACKING_BAD:
Track();
break;
case FrontendStatus::LOST:
Reset();
break;
}
last_frame_ = current_frame_;
return true;
}
直到bool VisualOdometry::Step()和bool Frontend::AddFrame(type), 终于没有后继的指针了,于是随着最后一个智能指针析构,这份Frame堆区资源释放,走完了它短暂而不凡的一生。(堂吉柯德附体)
🌈条款14:在资源管理类中小心coping行为
(如果要禁止对象copy行为:见条款06)
class Uncopyable {
private:
Uncopyable(Uncopyable &);
Uncopyable& operator=(Uncopyable &);
}; // 然后继承这个类,就能禁止复制行为了
#include <memory> // for std::shared_ptr
#include <thread>
#include <mutex>
void lock(std::mutex* pm) {
pm->lock();
}
void unlock(std::mutex* pm) {
pm->unlock();
}
class Lock {
public:
explicit Lock(std::mutex* pm) // 以某个Mutex初始化shared_ptr
: mutexPtr(pm, unlock) // 并以unlock函数为删除器,这样就不用担心忘记给互斥器解锁了
{
lock(mutexPtr.get()); // get函数见条款15
}
private:
std::shared_ptr<std::mutex> mutexPtr;
};
当某个资源可能被多个线程访问时,要加互斥器,就是你这个线程访问该资源的时候,排斥其他的线程访问该资源。
事实上slam方案中早已使用c++11中的std::unique_lock类来作为我们上面写的Lock类,因为它的private下面的内容不是智能指针:
直接上用法:
std::unique_lock<std::mutex> lck(mutex对象);
or
std::lock_guard<std::mutex> lck(mutex对象);
进一步了解C++11多线程编程的用法,请移步:https://www.cnblogs.com/zizbee/p/13520823.html
不知到你有没有意识到,我刚刚的说法其实是错误的,unique_lock类不能替代我们写的Lock类,因为它们被设计出来的目的不同,它没有在类的内部使用智能指针来储存原始资源(见条款15)。
(实际上我也还没有看懂Scott Meyers的Lock类究竟是干什么的,希望在我想出答案前能有大佬解疑)
例如高博的stereo slam方案中的代码:
struct Frame {
public:
typedef std::shared_ptr<Frame> Ptr;
SE3 pose_; // Tcw 形式Pose
std::mutex pose_mutex_; // Pose数据锁
public: // data members
// set and get pose, thread safe
SE3 Pose() {
// lck对象实例化,互斥锁自动lock。实例化即上锁
std::unique_lock<std::mutex> lck(pose_mutex_); // 这里的unique_lock可以用lock_guard代替
return pose_; // 多个线程抢着访问pose_
} // 栈区对象lck析构,互斥锁自动unlock
void SetPose(const SE3 &pose) {
std::unique_lock<std::mutex> lck(pose_mutex_);
pose_ = pose; // 多个线程抢着修改pose_
}
// 下面是拓展内容,也挺关键,有兴趣可以自行阅读
/// 工厂构建模式,分配id
static std::shared_ptr<Frame> CreateFrame(); // 静态成员函数的局部变量并不会在该函数运行结束后被释放
}; // 部分代码,不全
当然我对多线程编程这块还很薄弱,建议大家这方面参考其他大佬的博客,不足之处希望大家多多指正。
🌈条款15:在资源管理类中提供对初始资源的访问
你成功地把各种资源都装到了像shared_ptr这样的资源管理类RAII class中,但是,其他人写了些这样的函数,它们直接以资源为参数而不是装了资源的智能指针,当然这是很常见
现在你要做的就是通过显式转换或隐式转换,把一个RAII class对象转换为其所内含之原始资源
- 显式转换函数:get是shared_ptr的成员函数,返回原始资源的副本(推荐)
- 隐式转换函数:operator 内涵原始资源的类名() const {return 那个内含原始资源;}
下面是两种转换方式的实现:
// 原始资源
class FontHandle {};
// RAII类
class Font {
friend void releaseFont(FontHandle fh);
public:
explicit Font(FontHandle fh) // 构造函数是explicit函数,拒绝隐式转换
: f(fh)
{}
~Font() {releaseFont(f);}
FontHandle get() const {return f;} // 显式转换函数:到处都是get,但是安全
operator FontHandle() const {return f;} // 隐式转换函数:没get也能提取原始数据了
// 缺点:允许这样的写法存在:Font f1(getFont()); FontHandle f2 = f1;
// 这当然是客户写错了,但是f1隐式地转换为FontHandle对象,写错了还不会报错
private:
FontHandle f;
};
// 用于Font内涵原始资源初始化,Font的初始话当然也可以直接用static FontHandle对象完成
FontHandle getFont(){
static FontHandle fh;
return fh;
}
void releaseFont(FontHandle fh){
delete &fh;
} //释放资源
用这个函数来测试:
void changeFont(FontHandle f);
测试结果:两种转换方式均通过:
void test()
{
Font f(getFont());
changeFont(f.get()); // 显式转换
changeFont(f); // 隐式转换
}
说白了隐式转换就图一个省事好看,大家还是老老实实地用智能指针的get做显式转换吧,除非你不用智能指针另起炉灶。
🌈条款16:成功使用new和delete时要采取相同形式
你觉得下面的有错吗:
string* pusr = new string[100]; // 在堆区创建一个对象数组,并用一个指针指向它的开头
delete pusr; // 手动释放指针指向的内存
没错!一百个string对象它只删除了第一个。所以同样是堆区的:
删除单一对象:delete 单一对象;
删除数组对象:delete [ ] 数组对象;
总之,new的适合有[ ],delete的时候也要有[ ],这就是“采取相同形式”。
🌈条款17:以独立语句将newed对象置入智能指针
用法就是:
std::shared_ptr<T> Ptr(new T); // 可以参考更具体的条款13
然后使用Ptr