内存泄漏
根据定义,内存泄漏是指一些内存分配在堆上(在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库。可是,多种不用的智能指针easy出现新的错误。比如,把一个指向一个对象的指针赋值给一个期望接受一个指向数组的智能指针(即它将使用带方括号的)就会出现故障。反之亦然。
当中一种智能指针auto_ptr<T>具有一个奇怪的属性,当我们拥有一个auto指针p1,并像以下这样创建了它的一份拷贝p2时:
auto_ptr<int> p1(new int); auto_ptr<int> p2(p1);
指针p1就变成了NULL。这是非常不直观的。因此非常easy产生错误。
一般有两种智能指针能够有效的放置内存泄露:
(1)引用计数指针(又称共享指针)
(2)作用域指针
这两种指针的不同之处在于引用计数指针能够被复制,范围指针不能复制。
但,更有效的范围指针。
版权声明:本文博客原创文章。博客,未经同意,不得转载。