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

一,new operator

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

请看下面一段代码:

  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. int main(int argc, char* argv[])
  17. {
  18. A *pa = new A(1);
  19. delete pa;
  20. return 0;
  21. }

调用 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 必定会调用的,其定义为

  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. return (p);
  11. }

其中 _RAISE 宏定义为

#define _RAISE(x) throw x

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

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

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

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

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

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

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

  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. int main(int argc, char* argv[])
  27. {
  28. A *pa = new A(1);
  29. delete pa;
  30. return 0;
  31. }

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

现在思考两个问题:

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

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

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

  1. A *pa = new A(1);
  2. delete pa;

根本不能通过编译。

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

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

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

三,placement new

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

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

它同样可以被重载

  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. void* operator new(size_t size, void* p)
  18. {
  19. std::cout << "placement new of A" << std::endl;
  20. return ::operator new(size, p);
  21. }
  22. void operator delete(void* pUserData)
  23. {
  24. std::cout << "operator delete of A" << std::endl;
  25. ::operator delete(pUserData);
  26. }

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

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

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

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

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

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

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

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

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

placement new主要适用于:

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

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

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

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

  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. int main(int argc, char* argv[])
  12. {
  13. const int N = 8;
  14. void *ptr = operator new(sizeof(A) * N);
  15. A *pa = new(ptr)A[N];
  16. return 0;
  17. }

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

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

所以上述main函数应该改成

  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

浅析C++内存分配与释放操作过程——三种方式可以分配内存new operator, operator new,placement new的更多相关文章

  1. Linux就这个范儿 第15章 七种武器 linux 同步IO: sync、fsync与fdatasync Linux中的内存大页面huge page/large page David Cutler Linux读写内存数据的三种方式

    Linux就这个范儿 第15章 七种武器  linux 同步IO: sync.fsync与fdatasync   Linux中的内存大页面huge page/large page  David Cut ...

  2. Linux就这个范儿 第18章 这里也是鼓乐笙箫 Linux读写内存数据的三种方式

    Linux就这个范儿 第18章  这里也是鼓乐笙箫  Linux读写内存数据的三种方式 P703 Linux读写内存数据的三种方式 1.read  ,write方式会在用户空间和内核空间不断拷贝数据, ...

  3. 对象的notify方法的含义和对象锁释放的三种情况

    1,notify的含义     (1)notify一次只随机通知一个线程进行唤醒 (2)在执行了notify方法之后,当前线程不会马上释放该对象锁,呈wait状态的线程也不能马上获得该对象锁, 要等到 ...

  4. JavaScript声明全局变量的三种方式

    JavaScript声明全局变量的三种方式   JS中声明全局变量主要分为显式声明或者隐式声明下面分别介绍. 声明方式一: 使用var(关键字)+变量名(标识符)的方式在function外部声明,即为 ...

  5. C++创建对象的三种方式

    C++在创建对象的时候,有三种方式: #include <iostream> using namespace std; class A { private: int n; public: ...

  6. [UE4]C++创建对象的三种方式

    #include <iostream> using namespace std; class A { private: int n; public: A(int m):n(m) { } ~ ...

  7. C#批量插入数据到Sqlserver中的三种方式

    本篇,我将来讲解一下在Sqlserver中批量插入数据. 先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的是GUID,表中没有创建任何索引.GUID必然是比自增长要快的,因为你生 成 ...

  8. js学习-DOM之动态创建元素的三种方式、插入元素、onkeydown与onkeyup两个事件整理

    动态创建元素的三种方式: 第一种: Document.write(); <body> <input type="button" id="btn" ...

  9. 三种方式上传文件-Java

    前言:负责,因为该项目他(jetty嵌入式开始SpringMvc)实现文件上传的必要性,并拥有java文件上传这一块还没有被曝光.并 Http 更多晦涩协议.因此,这种渐进的方式来学习和实践上载文件的 ...

随机推荐

  1. python运维开发(十)----IO多路复用线程基本使用

    内容目录: python作用域 python2.7和python3.5的多继承区别 IO多路复用 socketserver模块源分析 多线程.进程.协程 python作用域  python中无块级作用 ...

  2. UVA 12563 Jin Ge Jin Qu hao

    dp-背包 开始用普通dp写了一发发现没法确定最大时间... 后来看到大牛机智的写法,嗯...dp表示当前状态能否成立:然后从条件最好的状态开始遍历,直到这个状态成立然后退出遍历. 具体的看代码吧.. ...

  3. PHP配置xdebug

    其实已经做PHP超过2年了,但是今天特别有感触,所以把过程写在这里 环境是win7+apache2.2+php5.3,因为某种原因,必须使用这个版本. 然后就死活配置不出来.apache日志如下: [ ...

  4. webapi文档

    webapi文档描述-swagger 最近做的项目使用mvc+webapi,采取前后端分离的方式,后台提供API接口给前端开发人员.这个过程中遇到一个问题后台开发人员怎么提供接口说明文档给前端开发人员 ...

  5. android 手势滑动

    1.概述, 两次都是画曲线统计图用到手势滑动.左滑动,右滑动曲线图翻页 2.直接上代码 3.注: 第一次使用的时候是implement了 OnTouchListener 接口,是在画图布局上layou ...

  6. haproxy path_end不能忽略

    C:\>ping www.zjtest7.com 正在 Ping www.zjtest7.com [192.168.32.82] 具有 32 字节的数据: 来自 192.168.32.82 的回 ...

  7. Unix/Linux环境C编程入门教程(25) C/C++字符测试那些事儿

    isalnum isalpha isascii iscntrl isdigit isgraph isislower isprint isspace ispunct isupper isxdigit介绍 ...

  8. UVA 400 Unix ls by sixleaves

    题目其实很简单,答题意思就是从管道读取一组文件名,并且按照字典序排列,但是输入的时候按列先输出,再输出行.而且每一行最多60个字符.而每个文件名所占的宽度为最大文件名的长度加2,除了输出在最右边的文件 ...

  9. 【LeetCode练习题】First Missing Positive

    First Missing Positive Given an unsorted integer array, find the first missing positive integer. For ...

  10. spring 定时任务(3)--配置多个定时任务

    <!-- 定义调用对象和调用对象的方法 --> <!-- 定时任务 A start --> <bean id="jobtask" class=&quo ...