C++内存管理学习笔记(2)

时间:2022-11-26 17:01:51

/****************************************************************/

/*            学习是合作和分享式的!

/* Author:Atlas                    Email:wdzxl198@163.com

/*  转载请注明本文出处:

*   http://blog.csdn.net/wdzxl198/article/details/9059883

/****************************************************************/

上节内容回顾:C++内存管理学习笔记(2)传送门

1.C++内存管理

1.1c语言和C++内存分配

1.2区分堆、栈、静态存储区

1.3控制C++的内存分配

在C++中一种常见的问题是对内存的分配,重点是new和delete的使用不当而失控。一般来说,C++对内存的管理非常的容易和安全,当一个对象被消除时,它的析构函数能够安全的释放所分配的内存。所以频繁的使用new和delete动态分配会出现一些问题和堆破碎的风险。

所以,当你必须要使用new 和delete时,你不得不控制C++中的内存分配。你需要用一个全局的new 和delete来代替系统的内存分配符,并且一个类一个类的重载new 和delete。

  一个防止堆破碎的通用方法是从不同固定大小的内存池中分配不同类型的对象。对每个类重载new和delete就提供了这样的控制。

小问:为什么需要重载operator::new和operator::delete?

虽然C++标准库已经为我们提供了new与delete操作符的标准实现,但是由于缺乏对具体对象的具体分析,系统默认提供的分配器在时间和空间两方面都存在着一些问题:分配器速度较慢,而且在分配小型对象时空间浪费比较严重,特别是在一些对效率或内存有较大限制的特殊应用中。比如说在嵌入式的系统中,由于内存限制,频繁地进行不定大小的内存动态分配很可能会引起严重问题,甚至出现堆破碎的风险;再比如在游戏设计中,效率绝对是一个必须要考虑的问题,而标准new与delete操作符的实现却存在着天生的效率缺陷。此时,我们可以求助于new与delete操作符的重载,它们给程序带来更灵活的内存分配控制。除了改善效率,重载new与delete还可能存在以下几点原因:

a)检测代码中的内存错误。

b)性能优化

b)获得内存使用的统计数据。

注意:C++中new和delete操作符不可以重载,但是在operator::new可以被重载,这个操作只是用来申请内存,不调用构造初始化对象!切记,如果需要详细阅读,请查看相关文档。

在《Effective c++》一书中也有讲解为什么需要重载,似乎重载是必须的?对于何种情况下需要重载new和delete的问题,以及如何重载的问题,需要继续研究学习。

详见文章-建议33:小心翼翼地重载operator new/ operator delete

(1)重载全局的new和delete

以下代码为《c++内存管理技术内幕》中是的,只限于简单原理学习

   1: void * operator new(size_t size)
   2: {
   3:     void *p = malloc(size);
   4:     return (p);
   5: }
   6: void operator delete(void *p)
   7: {
   8:     free(p);
   9: } 

这段代码可以代替默认的操作符来满足内存分配的请求。出于解释C++的目的,我们也可以直接调用malloc()和free()。

也可以对单个类的new 和 delete 操作符重载。这是你能灵活的控制对象的内存分配。

   1: class TestClass {
   2: public:
   3:     void * operator new(size_t size);
   4:     void operator delete(void *p);
   5:     // .. other members here ...
   6: };
   7: void *TestClass::operator new(size_t size)
   8: {
   9:     void *p = malloc(size); // Replace this with alternative allocator
  10:     return (p);
  11: }
  12: void TestClass::operator delete(void *p)
  13: {
  14:     free(p); // Replace this with alternative de-allocator
  15: }

所有TestClass 对象的内存分配都采用这段代码。更进一步,任何从TestClass 继承的类也都采用这一方式,除非它自己也重载了new 和 delete 操作符。通过重载new 和 delete 操作符的方法,你可以*地采用不同的分配策略,从不同的内存池中分配不同的类对象。

(2)为单个类重载new[ ]和delete[ ]

必须小心对象数组的分配。你可能希望调用到被你重载过的new 和 delete 操作符,但并不如此。内存的请求被定向到全局的new[ ]和delete[ ] 操作符,而这些内存来自于系统堆。

C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这种方式,你同样需要重载new[ ] 和 delete[ ]操作符。

   1: class TestClass {
   2: public:
   3:     void * operator new[ ](size_t size);
   4:     void operator delete[ ](void *p);
   5:     // .. other members here ..
   6: };
   7: void *TestClass::operator new[ ](size_t size)
   8: {
   9:     void *p = malloc(size);
  10:     return (p);
  11: }
  12: void TestClass::operator delete[ ](void *p)
  13: {
  14:     free(p);
  15: }
  16: int main(void)
  17: {
  18:     TestClass *p = new TestClass[10];
  19:     // ... etc ...
  20:     delete[ ] p;
  21: } 

但是注意:对于多数C++的实现,new[]操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在你的内存分配机制重要考虑的这一点。你应该尽量避免分配对象数组,从而使你的内存分配策略简单。

另:对于重载new和delete或者new[ ] 和delete[ ],需要考虑诸多事宜,比如错误处理机制,继承、多态等问题。限于篇幅,将在以后的文章中详细讲解,在此买一个伏笔。

(可以参考一篇文章new、delete(new[]、delete[])操作符的重载)。

1.4 内存管理的基本要求

如果只考虑分配和释放,内存管理基本要求是“不重不漏”:既不重复 delete,也不漏掉 delete。也就说我们常说的 new/delete 要配对,“配对”不仅是个数相等,还隐含了 new 和 delete 的调用本身要匹配,不要“东家借的东西西家还”。例如:

  • 用系统默认的 malloc() 分配的内存要交给系统默认的 free() 去释放;
  • 用系统默认的 new 表达式创建的对象要交给系统默认的 delete 表达式去析构并释放;
  • 用系统默认的 new[] 表达式创建的对象要交给系统默认的 delete[] 表达式去析构并释放;
  • 用系统默认的 ::operator new() 分配的的内存要交给系统默认的 ::operator delete() 去释放;
  • 用 placement new 创建的对象要用 placement delete (为了表述方便,姑且这么说吧)去析构(其实就是直接调用析构函数);
  • 从某个内存池 A 分配的内存要还给这个内存池。

如果定制 new/delete,那么要按规矩来。见 Effective C++ 相关条款。做到以上是每个 C++ 开发人员的基本功。

1.5常见的内存错误及其对策

发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。

常见的内存错误如下:

(1)内存分配未成功,却使用了它。

  编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

(2) 内存分配虽然成功,但是尚未初始化就引用它。

  犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

(3)内存分配成功并且已经初始化,但操作越过了内存的边界。

  例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

(4)忘记了释放内存,造成内存泄露。

  含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

(5)释放了内存却继续使用它。

  有三种情况:

  (a)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

  (b)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

  (c)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

常见的内存错误对策如下:

【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

  【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

  【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

  【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

  【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。


这部分学习笔记,其中有很多可以深究的知识,比如new和delete的重载、内存错误情况及处理机制等等,学无止境。


参考文献:c++内存管理学习纲要

Atlas

Edit:2013/6/8    22:31