zoukankan      html  css  js  c++  java
  • 浅析C++内存分配与释放操作过程——三种方式可以分配内存new operator, operator new,placement new

    引言:C++中总共有三种方式可以分配内存,new operator, operator new,placement new。 

     一,new operator 

    这就是我们最常使用的 new 操作符。查看汇编码可以看出:它不是一个函数,所以没有堆栈信息,而且它不能被重载。

    请看下面一段代码:

    [cpp] view plaincopy
     
    1. #include <iostream>  
    2. class A  
    3. {  
    4. public:  
    5.     A(int x):m_x(x)  
    6.     {  
    7.         std::cout << "constructor of A" << std::endl;  
    8.     }  
    9.     ~A()  
    10.     {  
    11.         std::cout << "destructor of A" << std::endl;  
    12.     }  
    13. private:  
    14.     int m_x;  
    15. };  
    16.   
    17. int main(int argc, char* argv[])  
    18. {  
    19.     A *pa = new A(1);  
    20.     delete pa;  
    21.     return 0;  
    22. }  

    调用 A *pa = new A(1); 的过程共总分为三步

    1,调用 void* operator new(size_t size)分配sizeof(A)大小的内存;

    2,在第一步返回的地址上调用A的构造函数;

    3,将第一步返回的地址赋值给pa;

    与 new operator 对应的是 delete operator,它也是操作符,同样不能被重载。

    调用 delete pa;的过程大致分为两步

    1,在 pa 所指的地址上调用A类的析构函数;

    2,调用void operator delete(void *pUserData)函数释放pa所指内存;

    如果A类没有声明析构函数,编译器也没有不要合成析构函数,上述delete过程就只有第二步。

    对基本数据类型也只有第二部。

    现在思考一个问题:考虑上诉代码,下面的代码会有内存泄漏吗?

    void *pvoid = pa;

    delete pvoid;

    上述问题可以表述为执行 delete pa 时释放的是sizeof(A)的内存吗?

    答案是肯定的,因为不管是 delete pa 还是 delete pvoid 最后执行的都是 void operator delete(void *pUserData),

    唯一的区别是delete pvoid 时不会调用A类的构造函数。

    二,operator new

    这个函数是 new opertor 必定会调用的,其定义为

    [cpp] view plaincopy
     
    1. void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)  
    2.         {       // try to allocate size bytes  
    3.         void *p;  
    4.         while ((p = malloc(size)) == 0)  
    5.                 if (_callnewh(size) == 0)  
    6.                 {       // report no memory  
    7.                 static const std::bad_alloc nomem;  
    8.                 _RAISE(nomem);  
    9.                 }  
    10.   
    11.   
    12.         return (p);  
    13.         }  

    其中 _RAISE 宏定义为 

    #define _RAISE(x) throw x

    可以看出,这个函数最终会调用C语言中的 malloc 函数,而且分配内存失败的话会抛出 std::bad_alloc 异常。

    与之对应的是 operator delete,其定义为

    [cpp] view plaincopy
     
    1. void operator delete(  
    2.         void *pUserData  
    3.         )  
    4. {  
    5.         _CrtMemBlockHeader * pHead;  
    6.   
    7.         RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));  
    8.   
    9.         if (pUserData == NULL)  
    10.             return;  
    11.   
    12.         _mlock(_HEAP_LOCK);  /* block other threads */  
    13.         __TRY  
    14.   
    15.             /* get a pointer to memory block header */  
    16.             pHead = pHdr(pUserData);  
    17.   
    18.              /* verify block type */  
    19.             _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));  
    20.   
    21.             _free_dbg( pUserData, pHead->nBlockUse );  
    22.   
    23.         __FINALLY  
    24.             _munlock(_HEAP_LOCK);  /* release other threads */  
    25.         __END_TRY_FINALLY  
    26.   
    27.         return;  
    28. }  

    可以看出,这个函数会调用 _free_dgb。再看看C语言中的 free 函数定义

    extern "C" _CRTIMP void __cdecl free(
            void * pUserData
            )
    {
            _free_dbg(pUserData, _NORMAL_BLOCK);
    }

    所以,operator delete 最终的释放过程和 free 函数相同。

    而且这两个函数都可以被重载

    [cpp] view plaincopy
     
    1. #include <iostream>  
    2. class A  
    3. {  
    4. public:  
    5.     A(int x):m_x(x)  
    6.     {  
    7.         std::cout << "constructor of A" << std::endl;  
    8.     }  
    9.     ~A()  
    10.     {  
    11.         std::cout << "destructor of A" << std::endl;  
    12.     }  
    13.     void* operator new(size_t size)  
    14.     {  
    15.         std::cout << "operator new of A" << std::endl;  
    16.         return ::operator new(size);  
    17.     }  
    18.     void operator delete(void* pUserData)  
    19.     {  
    20.         std::cout << "operator delete of A" << std::endl;  
    21.         ::operator delete(pUserData);  
    22.     }  
    23. private:  
    24.     int m_x;  
    25. };  
    26.   
    27. int main(int argc, char* argv[])  
    28. {  
    29.     A *pa = new A(1);  
    30.     delete pa;  
    31.     return 0;  
    32. }  

    需要注意的是,重载的时候要能保证其原有的行为,而且重载时不管有没有加static,这两个函数都是static类型的。

    现在思考两个问题:

    1,如何保证一个类不能在堆上创建。(比如考虑到性能因素)

    2,如何保证一个类对象不能被delete。(比如有时候全局作用域的单一实例不能被delete)

    答案是只需将类的operator new 和 operator delete 重载为 非public 类型的就可以了。这样的话这两行代码

    [cpp] view plaincopy
     
    1. A *pa = new A(1);  
    2. delete pa;  

    根本不能通过编译。

    顺便提另一个问题:如果保证一个类不能在栈上创建?

    答案同样是是将该类的析构函数声明为非 public 类型的就可以了。

    这样的话即便是创建该类的 global 对象也会导致编译失败。

    三,placement new

    这是一个全局函数,其定义为:

    [cpp] view plaincopy
     
    1. inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0()  
    2.     {   // construct array with placement at _Where  
    3.     return (_Where);  
    4.     }  

    它同样可以被重载

    [cpp] view plaincopy
     
    1. class A  
    2. {  
    3. public:  
    4.     A(int x):m_x(x)  
    5.     {  
    6.         std::cout << "constructor of A" << std::endl;  
    7.     }  
    8.     ~A()  
    9.     {  
    10.         std::cout << "destructor of A" << std::endl;  
    11.     }  
    12.     void *operator new(size_t size)  
    13.     {  
    14.         std::cout << "operator new of A" << std::endl;  
    15.         return ::operator new(size);  
    16.     }  
    17.   
    18.     void* operator new(size_t size, void* p)  
    19.     {  
    20.         std::cout << "placement new of A" << std::endl;  
    21.         return ::operator new(size, p);  
    22.     }  
    23.     void operator delete(void* pUserData)  
    24.     {  
    25.         std::cout << "operator delete of A" << std::endl;  
    26.         ::operator delete(pUserData);  
    27.     }  

    但需要注意的是,如果只重载了 void *operator new(size_t size) ,调用placement new 的时候将会调到编译错误。

    同样,如果如果只重载了 void *operator new(size_t size, void*p), 执行new 操作 的时候也会调到编译错误。

    placement new 的执行忽略了size_t参数,只返还第二个参数。其结果是允许用户把一个对象放到一个特定的地方,达到调用构造函数的效果。


    和其他普通的new不同的是,它在调用时括号里多了另外一个参数。比如:

    [cpp] view plaincopy
     
    1. void *ptr = operator new(sizeof(A));    //operator new  
    2. A *pa = new(ptr)A(2);           //placement new  
    [cpp] view plaincopy
     
    1. pa->~A();  

    括 号里的参数ptr是一个指针,它指向一个内存缓冲器,placement new 将在这个缓冲器上分配一个对象。

    placement new 的返回值是这 个被构造对象的地址(比如括号中的传递参数)。

    placement new是operator new重载的一个版本。它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。因此不能删除它,也没有对应的delete函数。

    但需要调用对象的析构函数。因此,最后应该需要执行pa->~A();

    placement new主要适用于:

    1,在对时间要求非常高的应用程序中,因为这些程序分配的时间是确定 的;

    2,长时间运行而不被打断的程序;

    3,执行一个垃圾收集器 (garbage collector)。

    还有一点需要注意的是,看如下代码中:

    [cpp] view plaincopy
     
    1. class A  
    2. {  
    3. public:  
    4.     A(int x = 1):m_x(x){}  
    5.     ~A()  
    6.     {  
    7.     }  
    8. private:  
    9.     int m_x;  
    10. };  
    11.   
    12. int main(int argc, char* argv[])  
    13. {  
    14.     const int N = 8;  
    15.     void *ptr = operator new(sizeof(A) * N);  
    16.     A *pa = new(ptr)A[N];     
    17.     return 0;  
    18. }  

    这是一段隐藏巨大错误的代码,很容易造成运行时内存错误。

    因为使用placement new分配自定义对象的数组时,如果该类定义了析构函数(这点很重要),应该多分配4个字节用于存储元素个数。

    所以上述main函数应该改成

    [cpp] view plaincopy
     
    1. int main(int argc, char* argv[])  
    2. {  
    3.     const int N = 8;  
    4.     void *ptr = operator new(sizeof(A) * N + sizeof(int));  
    5.     A *pa = new(ptr)A[N];     
    6.     return 0;  
    7. }  

    下面是我调试时的截图。

    ptr指向内存:

    pa指向内存:

    从中可以看出ptr所指内存的头4个字节存储元素个数。pa 实际指向 ptr + 4 的地址。

    总结:

    1,new 和 delete 最终都会调用C语言中的 malloc 和 free 过程。

    2,想要将对象构造在指定的内存地址上时可以使用placement new,但使用时要格外小心。

    3,只想分配指定大小的内存而不构造对象时,可以用operator new 取代 new operator。

    4,可以重载类的operator new 来跟踪类对象在堆上创建的过程及总数。

    5,在C++中应该尽量使用new operator,因为这个操作符自动检查需要分配的内存大小。

    http://blog.csdn.net/passion_wu128/article/details/9150261

  • 相关阅读:
    实验 7:OpenDaylight 实验——Python 中的 REST API 调用
    实验 6:OpenDaylight 实验——OpenDaylight 及 Postman实现流表下发
    实验 5:OpenFlow 协议分析和 OpenDaylight 安装
    实验 4:Open vSwitch 实验——Mininet 中使用 OVS 命令
    实验 3:Mininet 实验——测量路径的损耗率
    软件工程第一次作业——自我介绍
    实验 2:Mininet 实验——拓扑的命令脚本生成
    实验1、Mininet 源码安装和可视化拓扑工具
    第01组 Beta版本演示
    第01组 Beta冲刺(4/4)
  • 原文地址:https://www.cnblogs.com/findumars/p/5006177.html
Copyright © 2011-2022 走看看