• 内存泄漏与检测


      动态分配的内存在程序结束后而一直未释放,就出现了内存泄漏。

    一般常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式释放的内存。

    应用程序一般使用malloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,

    否则,这块内存就不能被再次使用,就说这块内存泄漏了。

    接着来分析下面的C代码:

    void GetMemory(char *p)
    {
        p = (char *)malloc(100);
    }
    void Test(void)
    {
        char *str = NULL;
        GetMemory(str);
        strcpy(str, "hello world");
        printf(str);
    }
    请问运行Test 函数会有什么样的结果?
    分析:上面的代码试图使用指针作为参数,分配动态内存。该代码会存在两个问题:
    1. 内存泄漏。
    首先,通过指针作为参数无法成功申请一块动态分配的内存。这是因为,GetMemory()函数获得的是实参指针变量的一个拷贝。因此,它只是将新分配的内存赋给了形参(即实参指针的拷贝)。而实参并没有获得这块内存。在Test()函数中,发现并没有释放str指向内存的语句。但这不是内存泄露的根本原因。即使在程序后面加上一句: 
    free(str);
    内存依然会泄漏。这是因为,str根本没有获得这块内存,而是由形参获得了。而形参是一个栈上的变量。在函数执行之后就已经被系统收回了。也就是说,有一块内存在分配之后,被遗忘了,这是造成了内存泄漏的根本原因。 要想成功获得分配的内存,可以采用下面的两种方法:


    char* GetMemory(char *p)
    {
        p = (char *)malloc(100);
        return p;
    }
    上面的代码直接返回新分配的内存地址的指针。由于内存是在堆上而不是在栈上分配的,所以函数返回后不存在任何问题。



    void GetMemory(char **p, int num)
    {
        *p = (char *)malloc(num);
    }
    这种方法是通过指针的指针来分配内存。用这种方法分配内存,传递给函数的是指针地址的一个拷贝,那么*p就是指针本身。 因此新分配的内存成功的赋给了做实参的指针。
    2. NULL指针引用导致程序崩溃。
    由于str并没有获得这块内存,那么str的值依然为NULL,所以strcpy()函数访问了一个NULL指针,直接导致程序崩溃。
    void GetMemory(char **p, int num)
    {
        *p = (char *)malloc(num);
    }
    void Test(void)
    {
        char *str = NULL;
        GetMemory(&str, 100);
        strcpy(str, "hello");
        printf(str);
    }
    请问运行Test 函数会有什么样的结果?
    分析:上面的代码使用指针的指针来分配内存,str会成功获得分配的内存。 但是,该题在使用了指针后,却忘记了对内存的释放。所以应该在后面加上:
    free(str);
    str = NULL;
    内存泄漏本身影响并不大,一次普通的内存泄漏用户根本感觉不到内存泄漏的存在。真正影响大的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。因此,在平时编码之时应该提高警惕,在使用完动态分配的内存之后,及时释放掉。 
    那么如何防止内存泄漏呢?内存分配应该遵循下面的原则:
    1. 谁分配,谁释放。在写下new/malloc时,要马上写下配对的delete/free以此释放掉。
    2. 出错处理需释放。在函数错误处理分支中,记得释放掉已经分配的内存。
    为了应对这种复杂的出错处理逻辑,避免一不小心就忘记了释放分配的资源,可以采用出错处理模块化处理, 在函数的尾部增加错误处理模块。一旦出错,就利用goto语句跳转到出错处理模块集中处理出错情况下资源的释放。 此外,还可以用SHE __try__leave__except结果化异常处理机制来处理系统中的异常的发生时资源的释放。
    3. 网络上拷贝的代码,要仔细检查内存使用情况,预防内存泄漏。
    4. 对于复杂指针的使用,如果做不到“谁分配,谁释放”,那么可以使用引用计数来管理这块内存的使用。 引用计数方式来管理内存,即在类中增加一个引用计数,跟踪指针的使用情况。当计数为0了,就可以释放指针了。 此种方法适合于通过一个指针申请内存之后,会经过程序各种复杂引用的情况。
    下面是一个实际例子:
    class CXData

    public:
    CXData()
    {
        m_dwRefNum = 1; //引用计数赋初值
    }
    ULONG AddRef() //增加引用
    {
        ULONG num = InterlockedIncrement(&m_dwRefNum);
        return num;
    }
    ULONG Release() //减少引用
    {
        ULONG num = InterlockedDecrement(&m_dwRefNum);
        if(num == 0) //当计数为0了,就释放内存
        {
            delete this;
        }
        return num;
    }
    private: ULONG m_dwRefNum; //引用计数
    }
    使用实例:
    CXData *pXdata = new CXData;
    pXdata->AddRef(); //使用前增加计数
    pXdata->Release(); //使用后减少计数,如果计数为零,则释放内存

    此外,内存泄漏还可能由调用了不正确的系统API而造成。比如在Windows里的CreateThread函数。

    CreateThread:是Windows的API函数(SDK函数的标准形式,直截了当的创建方式,任何场合都可以使用),提供操作系统级别的创建线程的操作,且仅限于工作者线程。

     不调用MFC和RTL的函数时,可以用CreateThread,其它情况不要使用。因为:

    C Runtime中需要对多线程进行纪录和初始化,以保证C函数库工作正常。 MFC也需要知道新线程的创建,也需要做一些初始化工作。

    有些CRT的函数象malloc(),fopen(),_open(),strtok(),ctime(),或localtime()等函数需要专门的线程存储局部的数据块,这个数据块通常需要在创建线程的时候就建立,如果使用CreateThread,这个数据块就没有建立,但函数会自己建立一个,然后将其与线程联系在一起,这意味着如果你用CreateThread来创建线程,然后使用这样的函数,会有一块内存在不知不觉中创建,而且这些函数并不将其删除,而CreateThread和ExitThread也无法知道这件事,于是就会有Memory Leak,在线程频繁启动的软件中,迟早会让系统的内存资源耗尽。 

    思考题:

    如何设计一个跨平台的内存泄漏检测机制?也就是说,如果在不同的平台发生了内存泄漏,我们如何用统一的方法检测它?

  • 相关阅读:
    个人案例分析
    软工结对作业
    交点问题
    C语言复习
    【软件工程】提问回顾与个人总结
    【技术博客】Arxiv的新Paper获取和机翻
    【技术博客】动态面包屑导航
    对对碰 -- 软工结对编程博客
    交点计数 -- 软工个人项目作业
    面向对象的程序设计-模块四课程总结
  • 原文地址:https://www.cnblogs.com/fengxing999/p/11097408.html
Copyright © 2020-2023  润新知