如果你想在类中实例化一个对象,而且随着这个类一直存在,就必须考虑这个对象的生命周期,避免超出局部作用域就被销毁了。常见错误:
ClassB.hpp:
class ClassB
{
public:
ClassB(int a)
{
}
}
想在ClassA中用ClassB对象:
#include "ClassB.hpp"
class ClassA
{
public:
ClassA(int a)
{
ClassB obj(a);
}
}
这里obj实例化放在了ClassA的构造函数里,其实ClassA构造完后,obj就被销毁了!
想要obj生命周期和ClassA一样长,就需要把obj作为ClassA的一个成员变量:
#include "ClassB.hpp"
class ClassA
{
public:
ClassA(int a)
{
}
private:
ClassB obj(1);
}
但这又带来了新问题,我们希望obj的构造(初始化)是在ClassA的构造函数内进行,而不是在变量定义时就构造。这对于构造函数有参数的obj尤其重要。有两种解决方案:
一、用std::shared_ptr来实例化对象
当然你也可以直接用原始的指针,但是指针的销毁需要手动处理,所以我们这里用std::shared_ptr智能指针,其对象的析构是随着退出作用域而自动进行的。
#include "ClassB.hpp"
#include <memory> // for std::shared_ptr
class ClassA
{
public:
ClassA(int a)
{
obj_ptr = std::make_shared_ptr<ClassB>(a);
}
private:
std::shared_ptr<ClassB> obj_ptr;
}
用std::make_shared_ptr就可以实例化对象了,由于obj_ptr是ClassA的成员变量,所以生成的obj对象也是跟ClassA生命周期一样,直到ClassA被销毁才会析构。
这里不用std::unique_ptr是因为unique_ptr很严格只能在一个地方使用,不能赋值(=)只能转移(move),所以很多地方用到obj_ptr等号赋值的时候会报错。
二、用impl_ptr来赋值和拷贝构造
如果你封装的ClassB不想用起来还得单独用shared_ptr,而是想和int、float这些基础变量一样用如下的方法分开定义和赋值:
#include "ClassB.hpp"
class ClassA
{
public:
ClassA(int a)
{
obj = ClassB(a); //带参数构造,并赋值给obj
}
private:
ClassB obj; //没有带参数构造
}
那么你可以用impl_ptr(impl是implemente的缩写)方法,重新定义构造、拷贝构造、和赋值函数,本质也是用shared_ptr,只不过把shared_ptr用impl_ptr提前封装在内部了:
ClassB.hpp:
#include <memory> //for std::shared_ptr
class ClassB
{
private:
class Impl
{
public:
Impl(int a) {}
void func() {}
}
std::shared_ptr<Impl> impl_ptr_;
public:
ClassB() {} // 无参数的构造函数
ClassB(int a) // 有参数的构造函数
{
impl_ptr_ = std::make_shared<Impl>(a);
}
/* 重写的拷贝构造函数 */
ClassB(const ClassB& rhs)
{
impl_ptr_ = rhs.impl_ptr_; // 拷贝的其实是内部的shared_ptr指针
}
/* 重载赋值函数为拷贝构造,default就是拷贝构造 */
ClassB& operator=(const ClassB& other) = default;
void func()
{
impl_ptr_->func();
}
}
可以看到,这里解决了几个问题:
1、通过同时编写无参数和有参数的构造函数,让定义ClassB obj没有参数也行。
2、用impl_ptr封装了ClassB的所有功能,ClassB只是一个露在外面的空壳,实际功能都是impl_ptr实现的。
3、重载拷贝构造函数为impl_ptr的复制,并将赋值函数(=)重载为拷贝构造函数,实现了用等号就可以移动赋值 obj = ClassB(a)。一般等号赋值是有问题的,因为普通的赋值函数只会拷贝类里一些基础的类型变量,一些自定义的方法和成员变量并不会拷贝过去。通过拷贝内部的shared_ptr巧妙地解决了这个问题。
这是我在阅读ROS1源码时学到的方法,ROS1的subscriber、publisher、timer都是这种构造方式。