1. 前情回顾
前篇最后,我们为消除内存泄漏、野指针等问题所做的代码尝试还是存在问题,本篇我们来讨论一下进一步的改进。
2. 需求总结
根据上一篇,总体上我们还需解决以下问题:
- 用户只new开辟,但是忘记使用delete释放,程序需要定期自动清理内存。
- 在之前的内存管理中,部分指针需要用delete,部分指针不能用delete,这导致使用者的困惑。后续改进中,需要支持对所有指针使用delete,程序自动判断被delete的指针之前是否触发过引用计数,若没触发,则不减少引用计数。
3. 问题分析
目前,现有的内存管理方案都是如下图所示:
多个指针指向同一个内存,他们都知道自己指向谁,但是堆内存却不知道谁指向自己,因此,在自己被释放的时候无法进行控制。
针对上述需求1,我们需要让堆内存知道谁指向自己,定期查询其是否还指向自己,如果不指向自己,则减少引用。
针对上述需求2,我们在拷贝指针的时候需要向堆内存注册该指针,让堆内存记住该指针,在内存释放的时候经过注册的指针才有资格释放。这样可以避免内存被没有权限的指针释放。
要做的事情已经清晰了,但是,这里还有一个问题,operator new和operator delete的各种形式本身并不具备反向地址传递,也就是说,operator new和operator delete天生无法知道到底是哪个指针指向对内存。因此,若要解决上述问题,内存开辟和释放就不能使用new和delete了。
4. 解决方案
我们先看代码:
#include <memory>
#include <exception>
#include <mutex>
#include <vector>
#define HEAP_SIGN_STR ("HeapYes") ///<堆内存标志
#define NUM_BYTE_HEAP_SIGN 8 ///<堆内存标志大小,为HEAP_SIGN_STR字符串长度+1
static std::mutex memUseLock;
class MemoryManager
{
MemoryManager(const MemoryManager&) = delete;
MemoryManager& operator = (const MemoryManager&) = delete;
MemoryManager() {}
public:
static MemoryManager& GetInstance()
{
static MemoryManager instance;
return instance;
}
//提交指针至内存管理系统
template<typename T>
void commit(T*& p)
{
for (auto iter_ptr = m_ptr.begin(); iter_ptr != m_ptr.end(); iter_ptr++)
{
if ((void*)(&p) == (*iter_ptr))
{
return; //已经存在,无法重复提交同一个指针
}
}
m_ptr.push_back((void*)(&p));
}
//从内存管理系统移除指针
template<typename T>
void remove(T*& p)
{
for (auto iter_ptr = m_ptr.begin(); iter_ptr != m_ptr.end();)
{
if ((void*)(&p) == (*iter_ptr))
{
iter_ptr = m_ptr.erase(iter_ptr);
delete p;
return;
}
else
{
iter_ptr++;
}
}
return;
}
private:
std::vector<void*> m_ptr;
};
template<typename T, typename ...Args>
void New(T*& p, Args&&... args)
{
p = new T(std::forward<Args>(args)...);
MemoryManager::GetInstance().commit(p);
}
template<typename T>
void Delete(T*& p)
{
MemoryManager::GetInstance().remove(p);
p = nullptr;
}
void* operator new(size_t sz)
{
//分配空间大小=对象大小sz+堆内存标志大小NUM_BYTE_HEAP_SIGN+引用计数区大小sizeof(int)
void* p;
while ((p = malloc(sz + NUM_BYTE_HEAP_SIGN + sizeof(int))) == 0)
{
if (_callnewh(sz + NUM_BYTE_HEAP_SIGN + sizeof(int)) == 0)
{
//分配失败,抛出异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
//将堆内存标志拷贝到头部
memcpy(p, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN);
p = (char*)p + NUM_BYTE_HEAP_SIGN;
//初始化引用计数为1
*((int*)p) = 1;
//将指针指向对象数据区,并返回该指针,后续对象在该数据区构造存储
p = (char*)p + sizeof(int);
return p;
}
void operator delete(void* p)
{
int* pCount = (int*)((char*)p - sizeof(int)); //引用计数区指针
char* pStr = (char*)pCount - NUM_BYTE_HEAP_SIGN;//堆内存标志区指针
if (memcmp(pStr, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN) != 0)
{
//如果堆内存标志不为HEAP_SIGN_STR,证明指针不是堆指针
return;
}
std::lock_guard<std::mutex> lock(memUseLock); //加锁防止重复释放内存
if (--(*pCount) == 0)
{
free((void*)pStr);
return;
}
//!=0时,要么是已经释放过了;要么是还有引用,不应该释放;
//两种情况都应该直接返回。
return;
}
//增加引用计数,指针复制时自动调用,对用户透明
inline void add_ref(void* p)
{
int* pCount = (int*)((char*)p - sizeof(int)); //引用计数区指针
char* pStr = (char*)pCount - NUM_BYTE_HEAP_SIGN;//堆内存标志区指针
std::lock_guard<std::mutex> lock(memUseLock);
if (memcmp(pStr, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN) != 0)
{
//如果堆内存标志不为HEAP_SIGN_STR,证明指针不是堆指针
return;
}
++(*pCount);
}
//指针复制方法1:函数法。
template<typename Tx, typename Ty>
inline void ptr_copy(Tx*& pDst, Ty* pSrc)
{
pDst = pSrc;
add_ref((void*)pSrc);
MemoryManager::GetInstance().commit(pDst);
}
//指针复制方法2:重载操作符
class PtrBase
{
public:
virtual ~PtrBase() {}
PtrBase() {}
PtrBase(const PtrBase&) = delete;
PtrBase& operator = (const PtrBase&) = delete;
};
template<typename T>
class PtrWrapper :public PtrBase
{
public:
PtrWrapper(T* p)
{
m_p = p;
}
T* Get()
{
return m_p;
}
~PtrWrapper()
{
delete m_p;
}
private:
T* m_p;
};
class Ptr :public PtrBase
{
public:
template<typename T>
Ptr(T*& p)
{
m_ptr = new PtrWrapper<T>(p);
add_ref(p);
}
template<typename T>
Ptr(T*&& p)
{
m_ptr = new PtrWrapper<T>(p);
}
template<typename T>
void operator &= (T*& pDst)
{
try
{
pDst = (dynamic_cast<PtrWrapper<T>*>(m_ptr))->Get();
add_ref(pDst);
MemoryManager::GetInstance().commit(pDst);
}
catch (const std::bad_cast & e)
{
throw e;
}
}
~Ptr()
{
delete m_ptr;
}
private:
PtrBase* m_ptr;
};
int main()
{
int* z = nullptr;
int* y = nullptr;
New(z); //带参数时可用New(z, 1);
Ptr(z) &= y;
Delete(z);
Delete(y);
}
5. 代码分析
上述代码在之前的基础上简单实现了一个内存管理的单例类,配合新的内存分配和释放函数New和Delete使用,New开辟内存时,将指针和内存进行双向绑定,Delete时,将指针和内存解绑定。Ptr类也会在指针拷贝时,将新指针和内存双向绑定。
5.1 作用
这样,用户可以对任意指针使用Delete,如果Delete的堆指针是函数参数指针或者是用户通过 “=”赋值的指针,这些指针未提交到MemoryManager中,因此,不会真正释放内存。用户需要关注的只是,一旦你的代码中有指针,在超出作用域的时候,记得Delete它就可以了。
注意:使用时,需要遵循:使用New函数代替new表达式开辟内存,使用Delete函数代替delete表达式。
5.2 问题总结
上述代码只是简单实现,它还存在如下问题:
- New和Delete为新增函数,不符合new和delete的使用习惯;
- MemoryManager类中所有指针统一存到vector中,将其按堆内存地址分组存放比较好,指向同一块堆内存的指针地址放到一起,这样甚至operator new和operator delete中都不需要引用计数,因为MemoryManager中相当于进行了引用计数。
- 上述代码还没有解决忘记Delete或者程序中发生异常,处理异常的代码中未Delete带来的内存泄露问题。解决这个需要在2的基础上,在MemoryManager类增加一个线程函数,定期扫描所有指针是否依然有效,移除无效的指针,并减少引用计数。
- 还有数组开辟和释放的operator new[]以及operator delete[]没有改造。
总之,上述方案还不是尽善尽美,毕竟,我们为了保留操作习惯,最终得到的还是原始指针,c++原始指针和"="赋值运算符以及new,delete不具备自动内存管理的功能。本次改进,暂时告一段落,有兴趣的同学可以自己继续改进。
6. 后记
至于上述总结的问题为什么我不解决掉,我个人觉得,c++内存管理是一个很复杂的东西,c++本身又以高效著称。正常情况下,上述方案已经将内存管理的复杂度降低到了我能掌控的程度,再增加一个线程来管理内存,收益不大,反而降低效率,而且,还会带来未尽的问题。
其实,我觉得,在new和delete中增加对内存标志字符串HEAP_SIGN_STR都没有必要,因为栈指针一般生存期很短,大多是函数局部变量指针,这种情况很容易管理,我们不太可能会忘记其到底是不是堆内存指针,不太可能对其误用delete。
最后,由于增加了New和Delete函数,其实使用起来不如new和delete习惯,在都不习惯的条件下,其实我们可以使用c++11新增的智能指针来自动管理内存的。也就不需要向上面这样一顿折腾。