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

[摘要]

c++被誉为最难学的编程语言,一方面是由于其功能过于强大、过于底层,导致语法灵活多变;另一方面是由于其内存管理极其复杂。其中,最主要的,被诟病最多的,就是其内存管理。
c++的内存结构中的使用频率超高的堆内存完全由程序员自己管理,这就导致c++这门语言对程序员的水平要求极高,一不小心就会导致内存泄漏,或者使用已释放的内存,进而导致程序输出与预期不符,甚至导致内存耗尽、程序崩溃等严重问题。c++程序界普遍遵守的“谁开辟,谁释放”也不总是那么有效,遇到非顺序执行程序,比如异常、跳转等,可能跳过释放步骤;另外某些共享资源,可能不是“我”开辟的,但是某些条件下,“我”需要释放。当这些情况遇上多线程,问题将变得更加复杂。
总之,c++程序员需要时刻与内存管理做斗争,劳心费力可能最后写出来的程序还是一堆bug,关键是程序员写程序没考虑到的地方,问题可能也比较难找。
本文主要目的就是建立一套自动内存管理方案,将c++程序员从繁重的内存管理工作中解放出来,在使用new和delete时再也不用担心内存释放和泄露的问题,妈妈再也不用担心我的内存管理问题啦!!!

1. 需求分析

在c++程序中,new是基本不存在什么问题的,发生问题一般是关于delete的。下面我们引入两个人物对话来阐明需求:Ethan大神(哈哈,就是小弟我啦,容许我自恋一下),小A(虚拟人物,不配有名字☺)。

场景1

小A:Ethan大神,你快来看看,我这段程序怎么崩溃了?
Ethan:让我看看你的代码。

小A写的代码:

int main()
{
	int a = 5;
	int* pA = &a;
	delete pA;
	pA = nullptr;
	return 0;
}

Ethan:你这个代码有问题啊,你的变量a是存于栈中,但是却试图用delete释放它,这当然错了,delete只能释放堆内存,所以导致崩溃啦。
小A:喔,我知道了,原来问题这么简单呀,可是,我现在还没办法用好指针,在写较为复杂的程序的时候,特别是类内保存的指针或者函数调用参数的指针,我怎么知道它是开辟在栈内存还是堆内存的呀,难保我不会用delete去释放它呀,有没有什么办法让我使用delete的时候自动帮我判断是否是堆指针,不是堆指针就直接跳过不释放啊。

好啦,需求1出来啦:

  • 需求1:可以对任意指针使用delete,delete需要自动判断是否是堆内存。

场景2

小A:Ethan大神,我这又出问题了,麻烦你帮忙看看好不?
Ethan:什么问题?
小A:你看这段代码,它又崩溃了。

小A的代码:

int main()
{
	int* pA = new int;
	int* pA1 = pA;
	delete pA;
	pA = nullptr;
	delete pA1;
	pA1 = nullptr;
	return 0;
}

Ethan:不止代码被你整崩溃了,我都被你整崩溃了,你这里只用new申请一次堆内存,但是你却对这个地址使用了两次delete。第二次delete释放一个早已经释放的地址,当然不允许啦。
小A:原来如此,我是新手嘛,没法避免这样的情况呀,对了,Ethan大神,有没有什么方法能避免这种情况,让我不管delete多少次,程序都能智能识别这部分内存是否早已经释放,这样我就不用每次使用delete的时候都小心翼翼了。

OK,需求2也出来啦:

  • 需求2:对指向同一对象的多个指针,可以delete多次,delete需要自动判断内存是否已经得到释放。
    注意:delete操作符首先是调用对象的析构函数进行析构,再调用operator delete释放内存,而我们在不创建新的内存管理函数条件下,可以改进的只是operator delete,因此,多次delete会多次调用析构函数,析构函数中有相关操作的需要注意是否已经操作过一次。

2. 解决方案

  1. 针对需求1,网上有方案是通过判断指针地址值来判断一个指针指向的地址是堆内存还是栈内存,但是都会存在多种问题;既然c++使用new来申请堆内存,那么我们是不是可以在申请堆内存的时候做点手脚,表示这是堆内存。自然想到的就是在申请的内存头部增加一段头部信息,申明这是堆内存。
  2. 针对需求2,最常见的是采用引用计数,delete一次,引用计数减少一次,直到减少为0,则真正释放内存;同时,为了让引用计数正确无误,在指针赋值的时候需要增加引用计数。但是赋值运算符 “=“无法进行全局重载,只能在类内重载,但是类内重载不具有普适性,同时需要对每个类重载”=”,相当麻烦。因此我们需要摒弃使用"="来为指针赋值,创建新的指针赋值函数或重载其他运算符作为指针赋值运算符。

3. 初次尝试

按照上述方案,我们先放一版代码出来,代码中有详细注释:

#include <memory>
#include <exception>

#define HEAP_SIGN_STR ("HeapYes")	///<堆内存标志
#define HEAP_FREE_SING_STR ("FreeYes") ///<堆内存释放标志
#define NUM_BYTE_HEAP_SIGN 8	///<堆内存标志大小,为HEAP_SIGN_STR字符串长度+1

void* operator new(size_t sz)
{
	//分配空间大小=对象大小sz+堆内存标志大小NUM_BYTE_HEAP_SIGN+引用计数区大小sizeof(int)
	void* p = malloc(sz + NUM_BYTE_HEAP_SIGN + sizeof(int));
	if (!p)
	{
		//分配失败,抛出异常
		throw std::bad_alloc();
	}
	else
	{
		//将堆内存标志拷贝到头部
		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 (strncmp(pStr, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN) != 0)
	{
		//如果堆内存标志不为HEAP_SIGN_STR,证明指针不是堆指针
		return;
	}
	if (--(*pCount) == 0)
	{
		memcpy(pStr , HEAP_FREE_SING_STR , NUM_BYTE_HEAP_SIGN);
		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;//堆内存标志区指针
	if (strncmp(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);
}

//指针复制方法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;
	}
private:
	T* m_p;
};

class Ptr :public PtrBase
{
public:
	template<typename T>
	Ptr(T* p)
	{
		m_ptr = (PtrWrapper<T>*)malloc(sizeof(PtrWrapper<T>));
		new(m_ptr) PtrWrapper<T>(p);
	}
	template<typename T>
	void operator &= (T*& pDst)
	{
		try
		{
			pDst = (dynamic_cast<PtrWrapper<T>*>(m_ptr))->Get();
			add_ref(pDst);
		}
		catch (const std::bad_cast& e)
		{
			throw e;
		}
	}
	~Ptr()
	{
		free(m_ptr);
	}
private:
	PtrBase* m_ptr;
};

int main()
{
	int* a = new int;	//只new一次,后面delete了两次
	int* b = nullptr;
	Ptr(a) &= b;		//重载运算符法复制指针,重载&=表示a并且b相等,指向相同位置。
	//ptr_copy(b, a);	//函数法复制指针,与上面的Ptr(a) &= b二选一
	delete a;			//delete堆内存。
	a = nullptr;
	delete b;			//再次delete相同的堆内存,不会出现问题。
	delete b;			//重复delete已经被释放的内存,不会出现问题。
	b = nullptr;
	return 0;
}

4. 总结

本篇最后放出的代码初步解决了本篇开始提出的两个需求,但是还远没有完全解决c++的内存问题,比如,上述代码必须满足delete次数比复制次数至少多一次,因为new的初始引用为1,这还是需要程序员控制delete使用的次数。
关于上述代码存在的问题,以及c++内存管理更深入的问题,我抽空写下一篇文章继续和大家探讨。

原创文章 33 获赞 99 访问量 2万+

猜你喜欢

转载自blog.csdn.net/HIVAN1/article/details/105807355