zoukankan      html  css  js  c++  java
  • 【转载】C++内存分配

    原文:C++内存分配

    内存泄露相信对C++程序员来说都不陌生。解决内存泄露的方案多种多样,大部分方案以追踪检测为主,这种方法实现起来容易,使用方便,也比较安全。

             首先我们要确定这个模块的主要功能:

    1. 能追踪内存的分配和释放过程。
    2. 要能显示内存分配的相关信息,比如内存块大小,代码所在文件所在行等。
    3. 在发现内存泄露时及时给出相关信息。
    4. 能正确处理一些异常情况,比如内存不足,对象初始化失败等等。
    5. 是线程安全的。[*这个还没有实现]

            有了一些基本功能需求,我们需要考虑每种功能怎么去实现。首先,我们可以通过重载的方式来追踪new,delete.malloc和free,C++给我提供了这样的特性。因为本文主要针对C++,所以主要讲重载new,delete的方法,malloc和free的重载实现于此类似,最终版本的程序中也实现了malloc和free的重载。

    1.重载new和delete

            首先我们要了解一下new和delete是怎么工作的。C++中的操作符最终都会被转换成函数形式,例如"new int"会变成"opetaor new(sizeof(int))",而"new double[10]"会变成"operator new(sizeof(double)*10)",同样“delete p”就变成了"operator delete(p)"。另外一个需要特别注意的地方是,new对于用户定义的数据类型(即你的自定义类)会自动调用该类型的构造函数,如果构造函数没有抛出异常,则正确分配,否则会中断分配操作,将异常传递给用户。默认情况下,new可以对象构造异常进行捕获。另外一个版本的new就是不带捕获异常功能的的了,所以C++系统提供的new和delete有:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void* operator new(size_t size)throw(std::bad_alloc);
    void* operator new[](size_t size) throw(std::bad_alloc);
    void* operator new(size_t,std::nothrow_t&)throw();
    void* operator new[](size_t,std::nothrow_t&)throw();
     
    void  operator delete(void* pointer);
    void  operator delete[](void* pointer);
    void  operator delete(void* pointer,std::nothrow_t&);
    void  operator delete[](void* pointer,std::nothrow_t&);<br>

            其中,nothrow_t是一个空结构体“struct nothrow_t{}",它的一个实例就是nothrow,C++用它来区分可以捕获异常的new和不可捕获异常的new。我们不能直接修改内部函数的行为,但是我们可以重载它们。为了实现提供内存分配信息的功能,我们给重载的函数加上几个参数。得到以下函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void* operator new(size_t size);
    void* operator new[](size_t size);
    void* operator new(size_t,std::nothrow_t&)throw();
    void* operator new[](size_t,std::nothrow_t&)throw();
    void* operator new(size_t size,const char* file,const char* func,const int line)throw(std::bad_alloc);
    void* operator new[](size_t size,const char* file,const char* func,const int line) throw(std::bad_alloc);
    void* operator delete(void* pointer);
    void* operator delete[](void* pointer);
    /*******Placement Delete********/
    void  operator delete(void* pointer,const char* file,const char* func,const int line);
    void  operator delete[](void* pointer,const char* file,const char* func,const int line);
    void  operator delete(void* pointer,std::nothrow_t&);
    void  operator delete[](void* pointer,std::nothrow_t&);
    /*******************************/

             中间的几个函数,就是我们主要需要重载的函数,模块的大部分工作也都由着几个函数完成。这些函数参数中,file表示分配代码所在的文件名,func表示代码所在的函数名,line表示代码行号。这几个参数信息我们可以通过编译器预定义好的几个宏来获得:__FILE__,__FUNCTION__,__LINE__。也就是说可以将"new ..."替换成"new(__FILE__,__FUNCTION__,__LINE__) ...",最终成为"operator new(sizeof(...),__FILE__,__FUNCTION__,__LINE__)"的形式,也就达到了我们的目的。关于 placement delete将在下面详细解释。

    2.空间分配

            接下来我们要考虑内存分配信息的组织问题了。我们先来了解一下编译器是怎么组织的。在大部分编译器中,new所分配的空间都要大于实际申请的空间,大出来的部分就是编译器定义的内存块的信息,包括了内存块的大小还有一些其他信息。如下图所示:

            我们把包含内存分配信息的部分叫做cookie数据。为了方便,我们把cookie数据放在分配的内存的起始位置,之后紧接有效数据区。我们还需要把返回给调用者的指针和new分配的数据区联系起来,原本想用性能比较好的STL的map数据结构来储存这些数据,但是map内部同样也使用new来分配内存,所以如果直接使用map来储存,就会陷入死循环中。所以这里我们必须自己现实一个数据结构。我们可以对返回给调用者的地址进行Hash,得到hash 表中的地址,具有相同Hash值的数据我们用一个单向链表连接起来。最终的数据结构如下图所示:

    2.1.构造函数中的异常

            另外一个必须要注意的一点是,new操作符会先分配空间然后调用用户自定义类型的构造函数,如果构造函数抛出异常,需要用户手动释放已分配的内存。问题在于释放这样的内存不能用一般的delete操作符,可以用一个例子来说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include <stdio.h>
    #include <stdexcept>
     
    void* operator new(size_t size, int line) {
        printf("Allocate %u bytes on line %d\n", size, line);
        return operator new(size);
    }
     
    class UserClass {
    public:
        UserClass(int n)throw(){
            if(n<=0){
                 throw std::runtime_error("n must be positive");
            }
        }
    };
     
    int main(){
        try{
            UserClass* myobj=new(__LINE__) UserClass(-10);
            delete myobj; //doesn't work if placement was not defined
        catch(const std::runtime_error& e) {
            fprintf(stderr,"Exception: %s ",e.what());
        }
        return 0;
    }<br>

            这里,虽然在new过后试图使用delete释放已经分配的内存,但是实际上不会释放。也许你的编译器会给出这样一条消息:

            “no matching operator delete found”

            为了正确处理这种情况,并给用户提供相关的信息,我们需要定义placement delete操作符。placement delete是C++98标准中才有的一个特性,所以对于某些老的编译器(大致可以认为是指那些98年以前编写的编译器)不支持这个特性。这需要在模块中添加宏定义让用户可以关闭placement delete的定义,以便模块能在较老的编译器上编译。以下就是需要定义的placement delete操作符:

    1
    2
    3
    4
    void  operator delete(void* pointer,const char* file,const char* func,const int line);
    void  operator delete[](void* pointer,const char* file,const char* func,const int line);
    void  operator delete(void* pointer,std::nothrow_t&);
    void  operator delete[](void* pointer,std::nothrow_t&);<br>

    3.检查内存泄露

            有了上面的实现,我们可以方便的手动检测内存泄露。通过一个函数来实现,它会检索整个hash表,如果表不为空则存在内存泄露。

            为了达到在最后程序退出时检查内存泄露的目的,我们需要在所有对象调用析构函数后进行内存泄露检测,这是因为某些用户类型在构造函数里调用new而在析构函数里调用delete。这样做能大大的减小误报的概率。而且因为对象的析构函数的调用往往在主函数main()执行结束之后进行,所以我们也不能直接在主函数里进行内存泄露检测。这里我们利用一个全局对象来实现这种检测。首先我们定义一个类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MemCheck{
        public:
            MemCheck(){
                memset(pTable,0,sizeof(mc_block_node_t*) * MC_HASHTABLESIZE);
            }
            ~MemCheck(){
                if(mc_checkmem()){
                    abort();
                }
            }
    };

            这里的构造函数初始化Hash表。析构函数检测内存泄露。然后定义一个MemCheck的全局静态对象,这样当程序运行之前会初始化hash表,程序退出时检测内存泄露。可是问题又来了,如果一个程序中有多个全局静态对象会怎样?不幸的是,对于全局静态对象的构造顺序和析构顺序是C++标准中的一个未定义问题,也就是说,这个顺序取决于编译器的具体实现。考虑,绝大多数平台使用VC和GCC编译器,我们可以针对这两种编译器来控制全局对象的构造和解析顺序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #ifdef _MSC_VER
    #pragma init_seg(lib)
    #endif
     
    static MemCheck mc_autocheck_object
    #ifdef __GNUC__
    __attribute__((init_priority (101)))
    #endif
    ;

            这里的宏定义部分都是编译器的选项。

  • 相关阅读:
    斐波那契数列
    旋转数组的最小数字
    用两个栈实现队列
    重建二叉树
    从尾到头打印链表
    2020/01/11,人活着是为了一口气
    2020/01/11,放肆和克制,敏感层次
    2020/01/11,记忆单元
    2020/01/11,经济基础决定高层建筑和个性
    git
  • 原文地址:https://www.cnblogs.com/zhehan54/p/5656913.html
Copyright © 2011-2022 走看看