关于内存泄露

首先:

我们要知道C++ IDE 中new至少可能代表以下三种含义:new operator、operator new、placement new。

 https://www.cnblogs.com/fnlingnzb-learner/p/8515183.html

以下关于重载的方法

原文链接:

https://blog.csdn.net/m_buddy/article/details/74999438

 

前言
之前已经介绍过了在Windows环境下和Linux环境下分别检测内存泄露使用的方法,但是这些内存检测的函数或是工具他们具体是依据什么样的原理来进行实现的呢?这里将在这篇文章中进行简单的分析和解读。

1. 动态分配的原理
当我们在程序中写下new和delete时,我们实际上调用的是C++语言内置的new operator和delete operator。所谓语言内置就是说我们不能更改其含义,它的功能总是一致的。以new operator为例,它总是先分配足够的内存,而后再调用相应的类型的构造函数初始化该内存。而delete operator总是先调用该类型的析构函数,而后释放内存。我们能够施加影响力的事实上就是new operator和delete operator执行过程中分配和释放内存的方法。

new operator为分配内存所调用的函数名字是operator new,其通常的形式是
void * operator new(size_t size)
其返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。参数size确定分配多少内存,你能增加额外的参数重载函数operator new,但是第一个参数类型必须是 size_t。
delete operator为释放内存所调用的函数名字是operator delete,其通常的形式是
void operator delete(void *memoryToBeDeallocated)
它释放传入的参数所指向的一片内存区。
这里有一个问题,就是当我们调用new operator分配内存时,有一个size参数表明需要分配多大的内存。但是当调用delete operator时,却没有类似的参数,那么delete operator如何能够知道需要释放该指针指向的内存块的大小呢?答案是:对于系统自有的数据类型,语言本身就能区分内存块的大小,而对于自定义数据类型(如我们自定义的类),则operator new和operator delete之间需要互相传递信息。

当我们使用operator new为一个自定义类型对象分配内存时,实际上我们得到的内存要比实际对象的内存大一些,这些内存除了要存储对象数据外,还需要记录这片内存的大小,此方法称为cookie。这一点上的实现依据不同的编译器不同。(例如MFC选择在所分配内存的头部存储对象实际数据,而后面的部分存储边界标志和内存大小信息。g++则采用在所分配内存的头4个自己存储相关信息,而后面的内存存储对象实际数据。)当我们使用delete operator进行内存释放操作时,delete operator就可以根据这些信息正确的释放指针所指向的内存块。

以上论述的是对于单个对象的内存分配/释放,当我们为数组分配/释放内存时,虽然我们仍然使用new operator和delete operator,但是其内部行为却有不同:new operator 调用了operator new 的数组版的兄弟- operator new[],而后针对每一个数组成员调用构造函数。而 delete operator 先对每一个数组成员调用析构函数,而后调用 operator delete[] 来释放内存。需要注意的是,当我们创建或释放由自定义数据类型所构成的数组时,编译器为了能够标识出在 operator delete[] 中所需释放的内存块的大小,也使用了编译器相关的 cookie 技术。

综上所述,如果我们想检测内存泄漏,就必须对程序中的内存分配和释放情况进行记录和分析,也就是说我们需要重载 operator new/operator new[];operator delete/operator delete[] 四个全局函数,以截获我们所需检验的内存操作信息。
2. 内存检测的基本实现原理
上文提到要想检测内存泄漏,就必须对程序中的内存分配和释放情况进行记录,所能够采取的办法就是重载所有形式的operator new 和 operator delete,截获 new operator 和 delete operator 执行过程中的内存操作信息。下面列出的就是重载形式
void* operator new( size_t nSize, char* pszFileName, int nLineNum )
void* operator new[]( size_t nSize, char* pszFileName, int nLineNum )
void operator delete( void *ptr )
void operator delete[]( void *ptr )
我们为 operator new 定义了一个新的版本,除了必须的 size_t nSize 参数外,还增加了文件名和行号,这里的文件名和行号就是这次 new operator 操作符被调用时所在的文件名和行号,这个信息将在发现内存泄漏时输出,以帮助用户定位泄漏具体位置。对于 operator delete,因为无法为之定义新的版本,我们直接覆盖了全局的 operator delete 的两个版本。
在重载的 operator new 函数版本中,我们将调用全局的 operator new 的相应的版本并将相应的 size_t 参数传入,而后,我们将全局 operator new 返回的指针值以及该次分配所在的文件名和行号信息记录下来,这里所采用的数据结构是一个 STL 的map,以指针值为 key 值。当 operator delete 被调用时,如果调用方式正确的话(调用方式不正确的情况将在后面详细描述),我们就能以传入的指针值在 map 中找到相应的数据项并将之删除,而后调用 free 将指针所指向的内存块释放。当程序退出的时候,map 中的剩余的数据项就是我们企图检测的内存泄漏信息--已经在堆上分配但是尚未释放的分配信息。
以上就是内存检测实现的基本原理,现在还有两个基本问题没有解决:
(1)如何取得内存分配代码所在的文件名和行号,并让 new operator 将之传递给我们重载的 operator new。
(2)我们何时创建用于存储内存数据的 map 数据结构,如何管理,何时打印内存泄漏信息。
先解决问题1。首先我们可以利用 C 的预编译宏 __FILE__ 和 __LINE__,这两个宏将在编译时在指定位置展开为该文件的文件名和该行的行号。而后我们需要将缺省的全局 new operator 替换为我们自定义的能够传入文件名和行号的版本,我们在子系统头文件 MemRecord.h 中定义:
#define DEBUG_NEW new(__FILE__, __LINE__ )
而后在所有需要使用内存检测的客户程序的所有的 cpp 文件的开头加入
#include "MemRecord.h"
#define new DEBUG_NEW
就可以将客户源文件中的对于全局缺省的 new operator 的调用替换为 new (__FILE__,__LINE__) 调用,而该形式的new operator将调用我们的operator new (size_t nSize, char* pszFileName, int nLineNum),其中 nSize 是由 new operator 计算并传入的,而 new 调用点的文件名和行号是由我们自定义版本的 new operator 传入的。我们建议在所有用户自己的源代码文件中都加入上述宏,如果有的文件中使用内存检测子系统而有的没有,则子系统将可能因无法监控整个系统而输出一些泄漏警告。
再说第二个问题。我们用于管理客户信息的这个 map 必须在客户程序第一次调用 new operator 或者 delete operator 之前被创建,而且在最后一个 new operator 和 delete operator 调用之后进行泄漏信息的打印,也就是说它需要先于客户程序而出生,而在客户程序退出之后进行分析。能够包容客户程序生命周期的确有一人--全局对象(appMemory)。我们可以设计一个类来封装这个 map 以及这对它的插入删除操作,然后构造这个类的一个全局对象(appMemory),在全局对象(appMemory)的构造函数中创建并初始化这个数据结构,而在其析构函数中对数据结构中剩余数据进行分析和输出。Operator new 中将调用这个全局对象(appMemory)的 insert 接口将指针、文件名、行号、内存块大小等信息以指针值为 key 记录到 map 中,在 operator delete 中调用 erase 接口将对应指针值的 map 中的数据项删除,注意不要忘了对 map 的访问需要进行互斥同步,因为同一时间可能会有多个线程进行堆上的内存操作。

详解C++三种new操作符

链接:

https://baijiahao.baidu.com/s?id=1679268916296965797&wfr=spider&for=pc

new存在三种操作符,其含义和应用的场景都不同, 这三种操作符分别是new operator, operator new, placement new。 

1.new operator指的就是new操作符,我们平常使用的操作符,它经历两个阶段的操作:

调用::operator new申请内存(operator new后面将进行详细说明,这里理解为C语言中的malloc)调用类的构造函数。

2.operator new操作符单纯申请内存,并且是可以重载的函数。

(注意:::operator new 和 ::operator delete前面加上::表示全局)

3.placement new操作符是重载operator new的一个版本,该函数的执行忽略了size_t参数,只返还第二个参数,该函数允许在已经构建好的内存中创建对象

 

new操作符内部原理

 

原文链接:

https://blog.51cto.com/masefee/814065

写了这样一段代码,我们来做分析:

#include <iostream>

int main( void )
{
    int* ptr = new int[ 5 ];
    for ( int i = 0; i < 5; i++ )
    {
        ptr[ i ] = i + 1;
        std::cout << ptr[ i ] << std::endl;
    }
}

 

我们new了5个数据的int类型空间,我们然后来看内存里面的分布:

 

 

 

  

      我这里已经提前标记好了各个部分。通常我们在查看内存的时候,通过指针所指向的地址来看内存里面的分配。 此时我们通常看的是上图绿色部分的数据。当然我们指针所指向的内存也是:0x003831b0。

      但是我们多测试几次会发现数据区前后怎么都有4个字节的数据存放的是:0xfdfdfdfd, 于是我们便产生联想,难道这是new内部这样固定实现的?为了追究其根本,我们便单步跟踪到了new的内部,结果欣然发现它使用了这个结构体:

 

#define nNoMansLandSize 4

typedef struct _CrtMemBlockHeader
{
        struct _CrtMemBlockHeader * pBlockHeaderNext;
        struct _CrtMemBlockHeader * pBlockHeaderPrev;
        char *                      szFileName;
        int                         nLine;
#ifdef _WIN64
        /* These items are reversed on Win64 to eliminate gaps in the struct
         * and ensure that sizeof(struct)%16 == 0, so 16-byte alignment is
         * maintained in the debug heap.
         */
        int                         nBlockUse;
        size_t                      nDataSize;
#else  /* _WIN64 */
        size_t                      nDataSize;
        int                         nBlockUse;
#endif  /* _WIN64 */
        long                        lRequest;
        unsigned char               gap[nNoMansLandSize];
        /* followed by:
         *  unsigned char           data[nDataSize];
         *  unsigned char           anotherGap[nNoMansLandSize];
         */
} _CrtMemBlockHeader;

 

     在调试版本里面,每当我们new一个heap空间时,系统都会给我们new的数据块加上这么一个块头。可以用于调试,边界溢出等检查。

     这下一下子清晰了,上面内存的图片显示块头的各个数据及占用空间。块头大小为32Byte。数据段的前后都有0xfdfdfdfd, 我们便可以运用这两个来进行边界溢出检查,大致的代码可以如下:

 

int _CrtCheckMem( const void *_memory, int _size )
{
    if ( _memory == NULL )
        return 0;

    // 这里采用反向寻址定位到块头
    _CrtMemBlockHeader *pHeader = ( _CrtMemBlockHeader * ) & ( ( const __int8 * )_memory )[ -( __int32 )sizeof( _CrtMemBlockHeader ) ];

    // 这里可以用来验证size是否合法
    if ( _size != 0 )
    { 
        if ( _size != pHeader->nDataSize )
        {
            //....
            return 0;
        }
    }

    unsigned char *gap = pHeader->gap;
    if ( !cmpgap( gap ) )  // 前边界溢出
    {
        //....
        return 0;
    }

    gap += pHeader->nDataSize + nNoMansLandSize;

    if ( !cmpgap( gap ) )  // 后边界溢出
    {
        //.....        
        return 0;
    }

    return 1;
}

 

   代码里面的bool cmpgap( void* p )你可以想想怎么去实现吧,这里我给个参考:

 

bool cmpgap( void *p )
{
    __asm 
    {
        mov ecx, p
        mov eax, [ecx]
        cmp eax, 0xfdfdfdfd
        jne _disp

        mov eax, 1
        jmp _exit

_disp:
        int 3
        mov eax, 0

_exit:
    }
}
-----------------------------------

在重载过程中碰到的问题

1. error LNK2005: "void __cdecl operator delete(void *)" (??3@YAXPAX@Z) 已经在 uafxcw.lib(afxmem.obj) [以及AboutDlg.obj等] 中定义

解决方法:修改project属性:配置属性->常规->MFC使用:选择 在动态DLL中使用MFC

分析:可能是静态链接的时候已将MFC代码链接上去,失去了动态绑定的特性

2.vs2019似乎能自动侦测内存泄漏,并带有内存块的编号,这时我们可以这样调试:

a. 在程序开始启动的地方(足够前的地方,只要在泄漏的内存分配的前面)使用代码:

  _CrtSetBreakAlloc(98500); //98500为上面内存泄漏的块号.

b. 然后debug运行,程序自动断点在"内存块98500"分配的位置:

找到位置后继续解决就ok了。

原文链接:https://blog.csdn.net/u011430225/article/details/47840647

 

 

 

posted @ 2021-10-25 09:07  IceArrow  阅读(158)  评论(0编辑  收藏  举报