首页 > 代码库 > 内存泄露

内存泄露

内存泄露

根据定义,内存泄露是指在堆上分配了一些内存(在C++中,使用new操作符;在C中,使用malloc()或calloc()),然后把这块内存的地址赋值给一个指针,后来却丢失了这个值,这可能是由于这个指针因为离开了作用域而失效。

{
	MyClass* my_class_object = new MyClass;
	DoSomething(my_class_object);
}//内存泄露

或者是因为向它赋了其他值:

	MyClass* my_class_object = new MyClass;
	DoSomething(my_class_object);
	my_class_object = NULL;//内存泄露

另外还有一种情况,程序员一直分配新内存,并没有丢失指向它们的指针,但是一直保留着指向程序不再使用的对象的指针。后面这种情况一般并不能算是内存泄露,但它所导致的后果是一样的:程序将耗尽内存。我们把后面这种错误留给程序员来操心,我们的注意力主要是前面的那些情况,即“正式”的内存泄露。

考虑两个对象,它们包含了指向对方的指针,这种情况称为“循环引用”。


指向A和B的指针都存在,但是如果没有其他任何指针指向这两个对象,就没有办法回收指向任何一个对象的内存,因此导致了内存泄露。这两个对象将会一直存在,不会被销毁。现在考虑相反的例子。假设有一个类,它由一个在一个独立的线程中运行的方法:

class SelfResponsible : public Thread
{
public:
	virtual void Run()
	{
		DoSomethingImportantAndCommitSuicide();
	}

	void DoSomethingImportantAndCommitSuicide()
	{
		sleep(1000);
		delete this;
	}
};

我们在一个独立的线程中启动它的Run()方法,如下所示:

Thread* my_object = new SelfResponsible;
my_object->Start(); //在一个独立的线程中调用Run()方法
my_object = NULL;

我们向这个指针赋了NULL值,丢失了这个对象的地址,从而导致了前面所说的内存泄露。但是,我们深入观察DoSomethingImportantAndCommitSuicide()方法的内部,将会发现在执行了一些任务之后,这个对象将会删除自身,把它所占据的内存释放给堆,使之可以被复用。因此,它实际上并没有产生内存泄露。

对于上述这些例子,进一步定义内存泄露。即如果我们分配了内存(使用new操作符),必须由某物(某个对象)负载:

  • 删除这块内存
  • 采用正确的方法完成这个任务(使用正确的delete操作符,带方括号或不带方括号)
  • 这个任务只执行一次
  • 在完成了对这块内存的使用之后,应该尽快执行这项任务
这个删除内存的责任通常称为对象的所有权。在前面的例子中,对象具有它自身的所有权。因此我们可以总结如下:内存泄露是由于被分配的内存的所有权丢失了。

以下的示例代码:

void SomeFunction()
{
	MyClass* my_class_object = NULL;
	//一些代码.....
	if(SomeCondition1())
	{
		my_class_object = new MyClass;
	}
	//更多代码
	if(SomeCondition2())
	{
		DoSomething(my_class_object);
		delete my_class_object;
		return;
	}
	//更多代码
	if(SomeCondition3())
	{
		DoSomethingElse(my_class_object);
		delete my_class_object;
		return;
	}

	delete my_class_object;
	return;
}

我们从NULL指针开始讨论的原因是为了避免“为什么只能在堆栈上创建对象,这样就可以完全避免销毁对象的问题”这个问题。有许多原因导致不适合在堆栈上创建对象。例如,有时,对象的创建必须延迟到程序中的某个时刻,晚于程序在内存中的变量所创建的时间。或者,它是又其他工厂类创建的,我们所得到的是一个指向它的指针,并需要负责在不需要使用这个对象时将其删除。另外,我们也可能根本不知道是否将要创建这个对象,就如前面的例子一样

既然我们已经在堆上创建了一个对象,就要负责删除它。对于上段的代码,它存在一些问题。每当我们添加一条额外的return语句时,必须在返回之前删除这个对象。

但是,即使我们记得在每条return语句之前删除这个对象,还是没能解决我们的问题。如果这段代码所调用的任何函数可能抛出一个异常,实际上意味着我们可能从包含函数调用的任何代码行“返回”。因此,我们必须把这段代码放在try-catch语句中,并且当我们捕捉了一个异常时,不要忘了删除这个对象,然后抛出另一个异常。为了避免内存泄露,看上去需要做的工作有很多。如果这段代码中存在负责清理工作的语句,情况就变得更为复杂了,从而导致很难理解,程序员也很难把注意力集中到实际的工作中。

这个问题的解决方案是使用智能指针,这是C++中许多人使用的办法。有些模板类的行为和常规的指针非常相似,但它们拥有所指向的对象的所有权,从而解除了程序员的烦恼。在这种情况下,前面所描述的函数将变成:

void SomeFunction()
{
	SmartPointer<MyClass> my_class_object;
	//一些代码.....
	if(SomeCondition1())
	{
		my_class_object = new MyClass;
	}
	//更多代码
	if(SomeCondition2())
	{
		DoSomething(my_class_object);
		return;
	}
	//更多代码
	if(SomeCondition3())
	{
		DoSomethingElse(my_class_object);
		return;
	}

	return;
}

注意,我们并没有在任何地方删除被分配的对象,现在这个责任是由智能指针(my_class_object)所承担的。

这实际上是一个更为通用的C++模式的一种特殊情况。在这个模式中,一个对象获取了一些资源(通常在构造函数中,但并不一定),然后,这个对象就负责释放这些资源,并且这个任务是在它的析构函数中完成的。使用这个模式的一个例子是在进入一个函数的时候获取一个Mutex对象的锁:

void MyClass::MyMethod()
{
	MutexLock lock(&my_mutex_);
	//一些代码
}  //析构函数~MutexLock()被调用,因此释放了my_mutex_
在这个例子中,MyClass类具有一个称为my_mutex_的数据成员,它必须在一个方法开始的时候获取,并且在离开这个方法之前被释放。它是在构造函数中由MutexLock获取的,并在它的析构函数中被自动释放,因此我们可以保证不管My::MyMethod()函数内部的代码发生了什么(即,不管我们插入多少条return语句或者是否可能抛出异常),这个方法不会在返回之前忘了释放my_mutex_。

现在,回到内存泄露问题。解决方案是当我们分配心内存时,必须立即把指向这块内存的指针赋值给某个智能指针。这样,我们就不需要担心删除这块内存的问题了。这个任务完全由这个智能指针来负责。

此时我们会有以下疑问:

(1)是否允许对智能指针进行复制?

(2)如果是,在智能指针的多份拷贝中,到底由哪一个负责删除它们共同指向的对象?

(3)智能指针是否表示指向一个对象的指针,或者表示指向一个对象数组的指针(即它应该使用带方括号的还是不带方括号的delete操作符)?

(4)智能指针是否对于一个常量指针或一个非常量指针?

对于这些问题的答案,我们可能会面临许多种不同的智能指针。事实上,在C++的社区讨论中,有些人使用了大量的由不同的库提供的智能指针,例如,较为突出的是boost库,但是,多种不用的智能指针容易出现新的错误。例如,把一个指向一个对象的指针赋值给一个期望接受一个指向数组的智能指针(即它将使用带方括号的)就会出现问题。反之亦然。

其中一种智能指针auto_ptr<T>具有一个奇怪的属性,当我们拥有一个auto指针p1,并像下面这样创建了它的一份拷贝p2时:

auto_ptr<int> p1(new int);
auto_ptr<int> p2(p1);

指针p1就变成了NULL,这是非常不直观的,因此很容易产生错误。

一般有两种智能指针可以有效的放置内存泄露:

(1)引用计数指针(又称共享指针)

(2)作用域指针

这两种指针的不同之处在于引用计数指针可以被复制,而作用域指针不能被复制。但是,作用域指针的效率更高;