内存泄漏几则

最后更新于 2024-07-20 5874 次阅读


概念

内存泄漏,是指在程序代码中动态申请的、堆上的内存 由于某种原因、在使用后没有被释放,进而造成内存的浪费。

少部分的内存泄漏不会影响程序的正常运行,不过如果是持续的内存泄漏会耗光系统内存,最终会导致程序卡死甚至系统崩溃。为了避免系统崩溃,在无法申请到内存的时候,要果断调用exit()函数主动杀死进程,而不是试图挽救这个进程。

如何察觉

如果程序在正常地使用过程中,占用的内存随着时间推移不断增长,一般就说明存在内存泄漏的情况。也可以使用专门的工具来检测程序中的内存泄漏。

产生的原因几则

malloc/new申请的内存没有主动释放

使用 malloc 申请的内存要主动调用 free,new 申请的内存要主动调用 delete,否则就会导致内存泄漏。例如下面代码中的内存 ptr 在申请后没有被释放就造成了内存泄漏。

int main(){
    void* ptr = malloc(1);
    // use ptr ...
    return 0;
}

使用free释放new申请的内存

malloc/free以及new/delete必须是各自成对出现,如果混用,就会导致意想不到的情况出现。另外,如果用delete释放void指针指向的对象同样也会造成内存泄露。

使用delete去删除数组

使用 new 申请的数组,释放的时候要用 delete[] 删除,如果错误地使用 delete 删除,就会造成内存泄漏。

int main(){
    int* ptr = new int[2];
    // usr ptr ...
    // delete ptr; // 错误!释放数组要用 delete[]
    delete[] ptr; // 正确!
    return 0;
}

基类的析构函数没有定义为虚函数

当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。例如下面代码中析构函数 ~A() 不是virtual,在调用 delete pa 的时候就不会调用子类 B 的析构函数,该对象中子类 B 中的内存无法被释放干净。

class A
{
public:
	A(){}
	~A(){}
}
 
class B : public A 
{
public:
	B(){}
	~B(){}
private:
	int num;
}
 
void main(){
	A* pa = new B();
	delete pa;
}

如何避免内存泄漏?

谨慎使用动态内存

在编写代码的时候,对动态内存保持警惕,保证每一块儿申请的内存都要得到释放。特别是在每个 return 之前,要想一想是否还有内存没有被释放,如果这里不释放,在其他地方是否会正常释放。

这是一种靠脑袋的方式,需要编写代码的时候时刻保持敏感,但是脑袋往往是不可靠的。最好选用其他的方式来保障。

使用RAII

RAII,全称资源获取即初始化(英语:Resource Acquisition Is Initialization),通过对象的初始化实现资源获取,通过对象的销毁实现资源的释放,我们所说的资源就是动态内存。

例如下面的例子中,通过 MemBlock 类的构造函数分配内存,通过析构函数释放内存,在需要使用动态内存的地方只需要定义一个 MemBlock 对象 buff,通过成员函数 get() 获取内存地址,使用之后无需手动释放内存,在 buff 离开作用域的时候,buff 会被自动释放(调用析构函数),在析构函数中调用 free 释放 buff 所持有的内存。如果在所有需要使用内存的地方都用这种方法,只要保证 MemBlock 对象能被析构,就不会造成内存泄漏。

class MemBlock
{
public:
MemBlock(size_t size)
{
_ptr = malloc(size);
}
~MemBlock()
{
free(_ptr);
}
public:
void* ptr()
{
return _ptr;
}
protected:
void* _ptr;
};
void main(){
MemBlock buff(1024);
memset(buff.ptr(), 0x00, 1024);
// 使用内存后无需主动释放
}

使用智能指针

使用智能指针直接管理动态内存,在使用之后不需要手动释放,当这段内存不再被引用的时候,这段内存会被调用 free 函数来释放,free函数是作为自定义的释放函数传给智能指针的,如果是其他类型的对象,不需要传入释放函数,会默认调用类型的析构函数来释放。