c++彻底消灭——内存泄漏、野指针(下篇)

1. 前情回顾

前篇最后,我们为消除内存泄漏、野指针等问题所做的代码尝试还是存在问题,本篇我们来讨论一下进一步的改进。

2. 需求总结

根据上一篇,总体上我们还需解决以下问题:

  1. 用户只new开辟,但是忘记使用delete释放,程序需要定期自动清理内存。
  2. 在之前的内存管理中,部分指针需要用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 问题总结

上述代码只是简单实现,它还存在如下问题:

  1. New和Delete为新增函数,不符合new和delete的使用习惯;
  2. MemoryManager类中所有指针统一存到vector中,将其按堆内存地址分组存放比较好,指向同一块堆内存的指针地址放到一起,这样甚至operator new和operator delete中都不需要引用计数,因为MemoryManager中相当于进行了引用计数。
  3. 上述代码还没有解决忘记Delete或者程序中发生异常,处理异常的代码中未Delete带来的内存泄露问题。解决这个需要在2的基础上,在MemoryManager类增加一个线程函数,定期扫描所有指针是否依然有效,移除无效的指针,并减少引用计数。
  4. 还有数组开辟和释放的operator new[]以及operator delete[]没有改造。

总之,上述方案还不是尽善尽美,毕竟,我们为了保留操作习惯,最终得到的还是原始指针,c++原始指针和"="赋值运算符以及new,delete不具备自动内存管理的功能。本次改进,暂时告一段落,有兴趣的同学可以自己继续改进。

6. 后记

至于上述总结的问题为什么我不解决掉,我个人觉得,c++内存管理是一个很复杂的东西,c++本身又以高效著称。正常情况下,上述方案已经将内存管理的复杂度降低到了我能掌控的程度,再增加一个线程来管理内存,收益不大,反而降低效率,而且,还会带来未尽的问题。
其实,我觉得,在new和delete中增加对内存标志字符串HEAP_SIGN_STR都没有必要,因为栈指针一般生存期很短,大多是函数局部变量指针,这种情况很容易管理,我们不太可能会忘记其到底是不是堆内存指针,不太可能对其误用delete。
最后,由于增加了New和Delete函数,其实使用起来不如new和delete习惯,在都不习惯的条件下,其实我们可以使用c++11新增的智能指针来自动管理内存的。也就不需要向上面这样一顿折腾。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值