基于智能指针和RAII的对象内存管理设计

从C++ std::shared_ptr 原理来看看栈溢出的危害,我提及了C++的智能指针指向被管理对象的raw ptr会被栈内存溢出而破坏,而利用智能指针进行对象构造的管理和设计,可以衍生出和RAII的结合,今天就来谈谈这项技术。

什么是RAII?

RAIIResource Acquisition Is Initialization,简而言之就是将对一个资源的申请封装在一个对象的生命周期内的理念。这样做的好处就是,C++的对象势必在创建的时候会经过构造函数,而在销毁的时候会触发析构函数。

听起来有点绕是不是,让我们来简化一下其主要特点。

  • 所有的资源管理内聚在对象内部
  • 利用对象申请/释放的特性对资源同步进行对应的申请/释放
  • 自动管理对象

前两点都比较容易,那么第三点如何达到呢?

合理的利用局部变量。

绝大多数语言,比如C++,都居于块级作用域。当在创建的变量离开其所在的块级时候,就会触发释放。而这就可以达到我们所说的自动管理对象。

这其实就是压栈/出栈的高级语言表现。

而在C++领域,有一个比较经典的利用RAII特性的设计就是ScopeLock

template<class LockType>

class My_scope_lock
{

   public:

   My_scope_lock(LockType& _lock):m_lock(_lock)

   {

         m_lock.occupy();

    }

   ~My_scope_lock()

   {

        m_lock.relase();

   }

   protected:

   LockType    m_lock;
}

在这里,锁被看成是一种资源,他需要lock/unlock的配对操作,不然就会引发问题。

而上述代码,将锁保留在对象的构造函数和西沟函数中。这样,当我们在某个函数中需要操作临界区域的时候,就可以简洁明了的使用局部变量来操作锁:

void Data::Update()
{
     My_scope_lock l_lock(m_mutex_lock);
    // do some operation
}

基于智能指针的RAII

上文我们用锁的例子来举例说明了RAII的设计理念,那什么又是基于智能指针的RAII呢?

我们都知道,在编程过程中,我们必须和内存打交道,而内存分为了两种类型:栈上内存和堆上内存。栈上内存不仅和线程相关,同时空间大小也相对堆内存来说非常小。因此,当我们在处理一些大规模数据(以及对象规模不确定)的时候,比如使用几百个对象的数据等等,一般都采用堆上动态分配内存。

但是堆上内存,在诸多的语言中,都需要手动管理,比如C++。而一般处理不当,比如(new []和delete搭配),或者遗忘了释放,那么就会产生内存泄漏等严重问题。

为此,我们参考上节的设计,准备构建一个可以在对象的构造/析构函数中成对正确释放内存的设计思路。

先假设一个需要在堆上频发操作的对象Data

class Data {
    // 省略
}

如果直接使用,一般情况下是这样的代码:

Data *data = new Data();
delete data;

需要频繁的确认对堆内存的正确使用。现在我们给他加一个包装对象,DataHandle

class DataHandle {
    private:
        Data *m_data;
}

DataHandle::DataHandle():m_data(new Data())
{}

DataHandle::~DataHandle()
{
    delete m_data;
    m_data = NULL;
}

这样,我们后续每次使用,就可以简化成

{
    DataHandle handle;
}

但是,别忘记了,C++中海油拷贝构造和重载赋值等操作,一旦我们写出如下代码,就会引发double free的问题。

{
    DataHandle handle1(handle);
    handle1 = handle;
}

因此,我们需要对拷贝构造函数和重载赋值进行特别处理。这里有两种处理方式:

  • 对于拷贝/赋值,每次把内部指针m_data也拷贝new一次。
  • 对于m_data进行合理的计数记录。

一般情况下,我们期望DataHandle的行为和Data是一致的。 因此我们想使用第二种方式。

这个时候,C++shared_ptr就派上用场了。改写下DataHanle

class DataHandle{
    public:
        DataHandle();
        DataHandle(const DataHandle &handle);
        DataHandle& operator=(const DataHanlde &handle);
    private:
        std::shared_ptr<Data> m_dataS;
}

对于重载后的拷贝/复制函数,我们只要利用智能指针自身重载过的赋值操作赋,即可解决引用计数问题。

最后要特别注意的是,下述两种情况的代码,是完全不相同的含义。

// 第一种情况
Data *data = new Data();

std::shared_ptr<Data> s1 = std::shared_ptr(data);

std::shared_ptr<Data> s2 = s1;

// 第二种情况

Data *data = new Data();

std::shared_ptr<Data> s1 = std::shared_ptr(data);

std::shared_ptr<Data> s2 = std::shared_ptr(data);