malloc realloc and new



realloc

   原型:extern void *realloc(void *mem_address, unsigned int newsize);
   用法:#include <stdlib.h> 有些编译器需要#include <malloc.h>
   功能:改变mem_address所指内存区域的大小为newsize长度。
   说明:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
  当内存不再使用时,应使用free()函数将内存块释放。
  注意:这里原始内存中的数据还是保持不变的。
   realloc的语法是:指针名=(数据类型*)realloc(newsize),(数据类型*)表示指针.
  举例

  // realloc.c
  #include <syslib.h>
  #include <alloc.h>
  main()
  {
  char *p;
  clrscr(); // clear screen
  p=(char *)malloc(100);
  if(p)
  printf("Memory Allocated at: %x",p);
  else
  printf("Not Enough Memory!/n");
  getchar();
  p=(char *)realloc(p,256);
  if(p)
  printf("Memory Reallocated at: %x",p);
  else
  printf("Not Enough Memory!/n");
  free(p);
  getchar();
  return 0;
  }
  详细说明及注意要点:
  1、如果有足够空间用于扩大mem_address指向的内存块,则分配额外内存,并返回mem_address
  这里说的是“扩大”,我们知道,realloc是从堆上分配内存的,当扩大一块内存空间时, realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平。也就是说,如果原先的内存大小后面还有足够的空闲空间用来分配,加上原来的空间大小= newsize。那么就ok。得到的是一块连续的内存。
  2、如果原先的内存大小后面没有足够的空闲空间用来分配,那么从堆中另外找一块newsize大小的内存。
  并把原来大小内存空间中的内容复制到newsize中。返回新的mem_address指针。(数据被移动了)。
  老块被放回堆上。
  例如:
  #include <malloc.h>
  void main()
  {
  char *p,*q;
  p = (char * ) malloc (10);
  q=p;
  p = (char * ) realloc (p,20); //A
  …………………………
  }
  在这段程序中我们增加了指针q,用它记录了原来的内存地址p。这段程序可以编译通过,但在执行到A行时,如果原有内存后面没有足够空间将原有空间扩展成一个连续的新大小的话,realloc函数就会以第二种方式分配内存,此时数据发生了移动,那么所记录的原来的内存地址q所指向的内存空间实际上已经放回到堆上了!这样就会产生q指针的指针悬挂,如果再用q指针进行操作就可能发生意想不到的问题。所以在应用realloc函数是应当格外注意这种情况。
  3、返回情况
  返回的是一个void类型的指针,调用成功。(这就再你需要的时候进行强制类型转换)
  返回NULL,当需要扩展的大小(第二个参数)为0并且第一个参数不为NULL,此时原内存变成了“freed(游离)”的了。
  返回NULL,当没有足够的空间可供扩展的时候,此时,原内存空间的大小维持不变。
  4、特殊情况
  如果mem_address为null,则realloc()和malloc()类似。分配一个newsize的内存块,返回一个指向该内存块的指针。
  如果newsize大小为0,那么释放mem_address指向的内存,并返回null。
  如果没有足够可用的内存用来完成重新分配(扩大原来的内存块或者分配新的内存块),则返回null.而原来的内存块保持不变。

malloc

   原型:extern void *malloc(unsigned int num_bytes);
   用法:#include <malloc.h>
  或#include<stdlib.h>
   功能:分配长度为num_bytes字节的内存块
   说明:如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
  当内存不再使用时,应使用free()函数将内存块释放。
   malloc的语法是:指针名=(数据类型*)malloc(长度),(数据类型*)表示指针.
  举例

  // malloc.c
  #include <syslib.h>
  #include <malloc.h>
  main()
  {
  char *p;
  clrscr(); // clear screen
  p=(char *)malloc(100);
  if(p)
  printf("Memory Allocated at: %x",p);
  else
  printf("Not Enough Memory!/n");
  
  if(p)
  free(p);
  getchar();
  return 0;
  }
  

malloc()函数的工作机制

  malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。

浅析malloc()的几种实现方式

  malloc()是C语言中动态存储管理的一组标准库函数之一。其作用是在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值是一个指向所分配的连续存储域的起始地址的指针。
  动态内存分配就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。本文简单介绍动态内存分配函数malloc()及几种实现方法。
  1. 简介
  malloc()是C语言中动态存储管理的一组标准库函数之一。其作用是在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值是一个指向所分配的连续存储域的起始地址的指针。还有一点必须注意的是,当函数未能成功分配存储空间(如内存不足)就会返回一个NULL指针。所以在调用该函数时应该检测返回值是否为NULL并执行相应的操作。
  2. 函数说明
  C语言的动态存储管理由一组标准库函数实现,其原型在标准文件<stdlib.h>里描述,需要用这些功能时应包含这个文件。与动态存储分配有关的函数共有四个,其中就包括存储分配函数malloc()。函数原型是:void *malloc (size_t n);这里的size_t是标准库里定义的一个类型,它是一个无符号整型。这个整型能够满足所有对存储块大小描述的需要,具体相当于哪个整型由具体的C系统确定。malloc的返回值为(void *)类型(这是通用指针的一个重要用途),它分配一片能存放大小为n的数据的存储块,返回对应的指针值;如果不能满足申请(找不到能满足要求的存储块)就返回NULL。在使用时,应该把malloc的返回值转换到特定指针类型,赋给一个指针。
  注意,虽然这里的存储块是通过动态分配得到的,但是它的大小也是确定的,同样不允许越界使用。例如上面程序段分配的块里能存n个双精度数据,随后的使用就必须在这个范围内进行。越界使用动态分配的存储块,尤其是越界赋值,可能引起非常严重的后果,通常会破坏程序的运行系统,可能造成本程序或者整个计算机系统垮台。
  下例是一个动态分配的例子:
  #include <stdio.h>
  #include<stdlib.h>
  main()
  {
  int count,*array; /*count是一个计数器,array是一个整型指针,也可以理解为指向一个整型数组的首地址*/
  count=100;
  if((array=(int *)malloc(count*sizeof(int))) == NULL)
  {
  printf("不能成功分配存储空间。");
  exit(1);
  }
  for(count=0;count<10;count++) /*给数组赋值*/
  array[count]=count;
  for(count=0;count<10;count++) /*打印数组元素*/
  printf("%2d",array[count]);
  }
  上例中动态分配了10个整型存储区域,然后进行赋值并打印。例中if((array(int *) malloc (10*sizeof(int)))==NULL)语句可以分为以下几步:
  1)分配10个整型的连续存储空间,并返回一个指向其起始地址的整型指针
  2)把此整型指针地址赋给array
  3)检测返回值是否为NULL
  例2,本例用于说明malloc()的返回值的情况
  下面的代码片段的输出是什么,为什么?
  char *ptr;
  if ((ptr = (char *)malloc(0)) == NULL)
  puts( "Got a null pointer ");
  else
  puts( "Got a valid pointer ");
  把0值传给了函数malloc,得到了一个合法的指针之后,这就是上面的代码,该代码的输出是 "Got a valid pointer "
  3. malloc()工作机制
  malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。
  4. malloc()在操作系统中的实现
  在 C 程序中,多次使用malloc () 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。
  在大部分操作系统中,内存分配由以下两个简单的函数来处理:
  void *malloc (long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
  void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。
  malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量:
  清单 1. 我们的简单分配程序的全局变量
  int has_initialized = 0;
  void *managed_memory_start;
  void *last_valid_address;
  如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX? 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:
  清单 2. 分配程序初始化函数
  /* Include the sbrk function */
  #include
  void malloc_init()
  {
  /* grab the last valid address from the OS */
  last_valid_address = sbrk(0);
  /* we don't have any memory to manage yet, so
  *just set the beginning to be last_valid_address
  */
  managed_memory_start = last_valid_address;
  /* Okay, we're initialized and ready to go */
  has_initialized = 1;
  }
  现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:
  清单 3. 内存控制块结构定义
  struct mem_control_block {
  int is_available;
  int size;
  };
  现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。
  在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:
  清单 4. 解除分配函数
  void free(void *firstbyte) {
  struct mem_control_block *mcb;
  /* Backup from the given pointer to find the
  * mem_control_block
  */
  mcb = firstbyte - sizeof(struct mem_control_block);
  /* Mark the block as being available */
  mcb->is_available = 1;
  /* That's It! We're done. */
  return;
  }
  如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述:
  清单 5. 主分配程序的伪代码
  1. If our allocator has not been initialized, initialize it.
  2. Add sizeof(struct mem_control_block) to the size requested.
  3. start at managed_memory_start.
  4. Are we at last_valid address?
  5. If we are:
  A. We didn't find any existing space that was large enough
  -- ask the operating system for more and return that.
  6. Otherwise:
  A. Is the current space available (check is_available from
  the mem_control_block)?
  B. If it is:
  i) Is it large enough (check "size" from the
  mem_control_block)?
  ii) If so:
  a. Mark it as unavailable
  b. Move past mem_control_block and return the
  pointer
  iii) Otherwise:
  a. Move forward "size" bytes
  b. Go back go step 4
  C. Otherwise:
  i) Move forward "size" bytes
  ii) Go back to step 4
  我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:
  清单 6. 主分配程序
  void *malloc(long numbytes) {
  /* Holds where we are looking in memory */
  void *current_location;
  /* This is the same as current_location, but cast to a
  * memory_control_block
  */
  struct mem_control_block *current_location_mcb;
  /* This is the memory location we will return. It will
  * be set to 0 until we find something suitable
  */
  void *memory_location;
  /* Initialize if we haven't already done so */
  if(! has_initialized) {
  malloc_init();
  }
  /* The memory we search for has to include the memory
  * control block, but the users of malloc don't need
  * to know this, so we'll just add it in for them.
  */
  numbytes = numbytes + sizeof(struct mem_control_block);
  /* Set memory_location to 0 until we find a suitable
  * location
  */
  memory_location = 0;
  /* Begin searching at the start of managed memory */
  current_location = managed_memory_start;
  /* Keep going until we have searched all allocated space */
  while(current_location != last_valid_address)
  {
  /* current_location and current_location_mcb point
  * to the same address. However, current_location_mcb
  * is of the correct type, so we can use it as a struct.
  * current_location is a void pointer so we can use it
  * to calculate addresses.
  */
  current_location_mcb =
  (struct mem_control_block *)current_location;
  if(current_location_mcb->is_available)
  {
  if(current_location_mcb->size >= numbytes)
  {
  /* Woohoo! We've found an open,
  * appropriately-size location.
  */
  /* It is no longer available */
  current_location_mcb->is_available = 0;
  /* We own it */
  memory_location = current_location;
  /* Leave the loop */
  break;
  }
  }
  /* If we made it here, it's because the Current memory
  * block not suitable; move to the next one
  */
  current_location = current_location +
  current_location_mcb->size;
  }
  /* If we still don't have a valid location, we'll
  * have to ask the operating system for more memory
  */
  if(! memory_location)
  {
  /* Move the program break numbytes further */
  sbrk(numbytes);
  /* The new memory will be where the last valid
  * address left off
  */
  memory_location = last_valid_address;
  /* We'll move the last valid address forward
  * numbytes
  */
  last_valid_address = last_valid_address + numbytes;
  /* We need to initialize the mem_control_block */
  current_location_mcb = memory_location;
  current_location_mcb->is_available = 0;
  current_location_mcb->size = numbytes;
  }
  /* Now, no matter what (well, except for error conditions),
  * memory_location has the address of the memory, including
  * the mem_control_block
  */
  /* Move the pointer past the mem_control_block */
  memory_location = memory_location + sizeof(struct mem_control_block);
  /* Return the pointer */
  return memory_location;
  }
  这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。
  5. malloc()的其他实现
  malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括:
  分配的速度。
  回收的速度。
  有线程的环境的行为。
  内存将要被用光时的行为。
  局部缓存。
  簿记(Bookkeeping)内存开销。
  虚拟内存环境中的行为。
  小的或者大的对象。
  实时保证。
  每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。
  还有其他许多分配程序可以使用。其中包括:
  Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。
  BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。
  Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。
  众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。
  6. 结束语
  前面已经提过,多次调用malloc()后空闲内存被切成很多的小内存片段,这就使得用户在申请内存使用时,由于找不到足够大的内存空间,malloc()需要进行内存整理,使得函数的性能越来越低。聪明的程序员通过总是分配大小为2的幂的内存块,而最大限度地降低潜在的malloc性能丧失。也就是说,所分配的内存块大小为4字节、8字节、16字节、18446744073709551616字节,等等。这样做最大限度地减少了进入空闲链的怪异片段(各种尺寸的小片段都有)的数量。尽管看起来这好像浪费了空间,但也容易看出浪费的空间永远不会超过50%。

NEW

  从堆中划分一块区域,动态创建一个类型的数据,最后返回该区域的指针.该数据类型可以是标准数据类型,也可以是用户自定义类型.数据使用完后,应调用delete运算符来释放动态申请的内存(在堆中).
  1.VB
  New 关键字引入 New 子句,该子句创建一个新的对象实例。New 子句必须指定一个可以用来创建实例的已定义类。可以在声明语句或赋值语句中使用 New。执行该语句时,它将调用指定类的构造函数,传递您提供的所有参数:
  Dim Obj As Object
  Obj = New SomeClass("String required by constructor")
  ' ...
  Dim MyLabel As New Label()
  由于数组是类,因此 New 可以创建新的数组实例:
  Dim MyArray As Integer()
  MyArray = New Integer() {0, 1, 2, 3}
  如果内存不足,无法创建新的实例,公共语言运行库将引发 OutOfMemoryException 错误。
  2.C++
  (1)new可用来生成动态无名变量,
  如 int *p=new int;
  int *p[10]=new int [10]; //动态数组的大小可以是变量或常量;而一般直接声明数组时,数组大小必须是常量
  对于生成二维及更高维的数组,应使用多维指针,以二维指针为例
  int **p=new int* [row]; //row是二维数组的行,p是指向一个指针数组的指针
  for(int i=0;i<10;i++)
  p=new int [col]; //col是二维数组的列,p是指向一个int数组的指针
  (2)使用完动态无名变量后应该及时释放,要用到 delete 运算符
  delete p; //释放单个变量
  delete []p; //释放数组变量(不论数组是几维)
  相比于一般的变量声明,使用new和delete 运算符可方便的使用变量.
  以下关于“new”的详细分析
  “new”是C++的一个关键字,同时也是操作符。关于new的话题非常多,因为它确实比较复杂,也非常神秘,下面我将把我了解到的与new有关的内容做一个总结。
  new的过程
  当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间、调用构造函数、返回正确的指针。当然,如果我们创建的是简单类型的变量,那么第二步会被省略。假如我们定义了如下一个类A:
  class A
  {
  int i;
  public:
  A(int _i) :i(_i*_i) {}
  void Say() { printf("i=%dn", i); }
  };
  //调用new:
  A* pa = new A(3);
  那么上述动态创建一个对象的过程大致相当于以下三句话(只是大致上):
  A* pa = (A*)malloc(sizeof(A));
  pa->A::A(3);
  return pa;
  虽然从效果上看,这三句话也得到了一个有效的指向堆上的A对象的指针pa,但区别在于,当malloc失败时,它不会调用分配内存失败处理程序new_handler,而使用new的话会的。因此我们还是要尽可能的使用new,除非有一些特殊的需求。
  new的三种形态
  到目前为止,本文所提到的new都是指的“new operator”或称为“new expression”,但事实上在C++中一提到new,至少可能代表以下三种含义:new operator、operator new、placement new。
  new operator就是我们平时所使用的new,其行为就是前面所说的三个步骤,我们不能更改它。但具体到某一步骤中的行为,如果它不满足我们的具体要求 时,我们是有可能更改它的。三个步骤中最后一步只是简单的做一个指针的类型转换,没什么可说的,并且在编译出的代码中也并不需要这种转换,只是人为的认识 罢了。但前两步就有些内容了。
  new operator的第一步分配内存实际上是通过调用operator new来完成的,这里的new实际上是像加减乘除一样的操作符,因此也是可以重载的。operator new默认情况下首先调用分配内存的代码,尝试得到一段堆上的空间,如果成功就返回,如果失败,则转而去调用一个new_hander,然后继续重复前面 过程。如果我们对这个过程不满意,就可以重载operator new,来设置我们希望的行为。例如:
  class A
  {
  public:
  void* operator new(size_t size)
  {
  printf("operator new calledn");
  return ::operator new(size);
  }
  };
  A* a = new A();
  这里通过::operator new调用了原有的全局的new,实现了在分配内存之前输出一句话。全局的operator new也是可以重载的,但这样一来就不能再递归的使用new来分配内存,而只能使用malloc了:
  void* operator new(size_t size)
  {
  printf("global newn");
  return malloc(size);
  }
  相应的,delete也有delete operator和operator delete之分,后者也是可以重载的。并且,如果重载了operator new,就应该也相应的重载operator delete,这是良好的编程习惯。
  new的第三种形态——placement new是用来实现定位构造的,因此可以实现new operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在这块内存上构造一个对象,这有点类似于前面代码中的“p- >A::A(3);”这句话,但这并不是一个标准的写法,正确的写法是使用placement new:
  #include <new.h>
  void main()
  {
  char s[sizeof(A)];
  A* p = (A*)s;
  new(p) A(3); //p->A::A(3);
  p->Say();
  }
  对头文件<new>或<new.h>的引用是必须的,这样才 可以使用placement new。这里“new(p) A(3)”这种奇怪的写法便是placement new了,它实现了在指定内存地址上用指定类型的构造函数来构造一个对象的功能,后面A(3)就是对构造函数的显式调用。这里不难发现,这块指定的地址既 可以是栈,又可以是堆,placement对此不加区分。但是,除非特别必要,不要直接使用placement new ,这毕竟不是用来构造对象的正式写法,只不过是new operator的一个步骤而已。使用new operator地编译器会自动生成对placement new的调用的代码,因此也会相应的生成使用delete时调用析构函数的代码。如果是像上面那样在栈上使用了placement new,则必须手工调用析构函数,这也是显式调用析构函数的唯一情况:
  p->~A();
  当我们觉得默认的new operator对内存的管理不能满足我们的需要,而希望自己手工的管理内存时,placement new就有用了。STL中的allocator就使用了这种方式,借助placement new来实现更灵活有效的内存管理。
  处理内存分配异常
  正如前面所说,operator new的默认行为是请求分配内存,如果成功则返回此内存地址,如果失败则调用一个new_handler,然后再重复此过程。于是,想要从operator new的执行过程中返回,则必然需要满足下列条件之一:
  l 分配内存成功
  l new_handler中抛出bad_alloc异常
  l new_handler中调用exit()或类似的函数,使程序结束
  于是,我们可以假设默认情况下operator new的行为是这样的:
  void* operator new(size_t size)
  {
  void* p = null
  while(!(p = malloc(size)))
  {
  if(null == new_handler)
  throw bad_alloc();
  try
  {
  new_handler();
  }
  catch(bad_alloc e)
  {
  throw e;
  }
  catch(…)
  {}
  }
  return p;
  }
  在默认情况下,new_handler的行为是抛出一个bad_alloc异常,因此 上述循环只会执行一次。但如果我们不希望使用默认行为,可以自定义一个new_handler,并使用std::set_new_handler函数使其 生效。在自定义的new_handler中,我们可以抛出异常,可以结束程序,也可以运行一些代码使得有可能有内存被空闲出来,从而下一次分配时也许会成 功,也可以通过set_new_handler来安装另一个可能更有效的new_handler。例如:
  void MyNewHandler()
  {
  printf(“New handler called!n”);
  throw std::bad_alloc();
  }
  std::set_new_handler(MyNewHandler);
  这里new_handler程序在抛出异常之前会输出一句话。应该注意,在 new_handler的代码里应该注意避免再嵌套有对new的调用,因为如果这里调用new再失败的话,可能会再导致对new_handler的调用, 从而导致无限递归调用。——这是我猜的,并没有尝试过。
  在编程时我们应该注意到对new的调用是有可能有异常被抛出的,因此在new的代码周围应该注意保持其事务性,即不能因为调用new失败抛出异常来导致不正确的程序逻辑或数据结构的出现。例如:
  class SomeClass
  {
  static int count;
  SomeClass() {}
  public:
  static SomeClass* GetNewInstance()
  {
  count++;
  return new SomeClass();
  }
  };
  静态变量count用于记录此类型生成的实例的个数,在上述代码中,如果因new分配内存失败而抛出异常,那么其实例个数并没有增加,但count变量的值却已经多了一个,从而数据结构被破坏。正确的写法是:
  static SomeClass* GetNewInstance()
  {
  SomeClass* p = new SomeClass();
  count++;
  return p;
  }
  这样一来,如果new失败则直接抛出异常,count的值不会增加。类似的,在处理线程同步时,也要注意类似的问题:
  void SomeFunc()
  {
  lock(someMutex); //加一个锁
  delete p;
  p = new SomeClass();
  unlock(someMutex);
  }
  此时,如果new失败,unlock将不会被执行,于是不仅造成了一个指向不正确地址的指针p的存在,还将导致someMutex永远不会被解锁。这种情况是要注意避免的。(参考:C++箴言:争取异常安全的代码)
  STL的内存分配与traits技巧
  在《STL原码剖析》一书中详细分析了SGI STL的内存分配器的行为。与直接使用new operator不同的是,SGI STL并不依赖C++默认的内存分配方式,而是使用一套自行实现的方案。首先SGI STL将可用内存整块的分配,使之成为当前进程可用的内存,当程序中确实需要分配内存时,先从这些已请求好的大内存块中尝试取得内存,如果失败的话再尝试 整块的分配大内存。这种做法有效的避免了大量内存碎片的出现,提高了内存管理效率。
  为了实现这种方式,STL使用了placement new,通过在自己管理的内存空间上使用placement new来构造对象,以达到原有new operator所具有的功能。
  template <class T1, class T2>
  inline void construct(T1* p, const T2& value)
  {
  new(p) T1(value);
  }
  此函数接收一个已构造的对象,通过拷贝构造的方式在给定的内存地址p上构造一个新对 象,代码中后半截T1(value)便是placement new语法中调用构造函数的写法,如果传入的对象value正是所要求的类型T1,那么这里就相当于调用拷贝构造函数。类似的,因使用了 placement new,编译器不会自动产生调用析构函数的代码,需要手工的实现:
  template <class T>
  inline void destory(T* pointer)
  {
  pointer->~T();
  }
  与此同时,STL中还有一个接收两个迭代器的destory版本,可将某容器上指定范 围内的对象全部销毁。典型的实现方式就是通过一个循环来对此范围内的对象逐一调用析构函数。如果所传入的对象是非简单类型,这样做是必要的,但如果传入的 是简单类型,或者根本没有必要调用析构函数的自定义类型(例如只包含数个int成员的结构体),那么再逐一调用析构函数是没有必要的,也浪费了时间。为 此,STL使用了一种称为“type traits”的技巧,在编译器就判断出所传入的类型是否需要调用析构函数:
  template <class ForwardIterator>
  inline void destory(ForwardIterator first, ForwardIterator last)
  {
  __destory(first, last, value_type(first));
  }
  其中value_type()用于取出迭代器所指向的对象的类型信息,于是:
  template<class ForwardIterator, class T>
  inline void __destory(ForwardIterator first, ForwardIterator last, T*)
  {
  typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
  __destory_aux(first, last, trivial_destructor());
  }
  //如果需要调用析构函数:
  template<class ForwardIterator>
  inline void __destory_aux(ForwardIterator first, ForwardIterator last, __false_type)
  {
  for(; first < last; ++first)
  destory(&*first); //因first是迭代器,*first取出其真正内容,然后再用&取地址
  }
  //如果不需要,就什么也不做:
  tempalte<class ForwardIterator>
  inline void __destory_aux(ForwardIterator first, ForwardIterator last, __true_type)
  {}
  因上述函数全都是inline的,所以多层的函数调用并不会对性能造成影响,最终编译 的结果根据具体的类型就只是一个for循环或者什么都没有。这里的关键在于__type_traits<T>这个模板类上,它根据不同的T类 型定义出不同的has_trivial_destructor的结果,如果T是简单类型,就定义为__true_type类型,否则就定义为 __false_type类型。其中__true_type、__false_type只不过是两个没有任何内容的类,对程序的执行结果没有什么意义,但 在编译器看来它对模板如何特化就具有非常重要的指导意义了,正如上面代码所示的那样。__type_traits<T>也是特化了的一系列模 板类:
  struct __true_type {};
  struct __false_type {};
  template <class T>
  struct __type_traits
  {
  public:
  typedef __false _type has_trivial_destructor;
  ……
  };
  template<> //模板特化
  struct __type_traits<int> //int的特化版本
  {
  public:
  typedef __true_type has_trivial_destructor;
  ……
  };
  …… //其他简单类型的特化版本
  如果要把一个自定义的类型MyClass也定义为不调用析构函数,只需要相应的定义__type_traits<T>的一个特化版本即可:
  template<>
  struct __type_traits<MyClass>
  {
  public:
  typedef __true_type has_trivial_destructor;
  ……
  };
  模板是比较高级的C++编程技巧,模板特化、模板偏特化就更是技巧性很强的东西, STL中的type_traits充分借助模板特化的功能,实现了在程序编译期通过编译器来决定为每一处调用使用哪个特化版本,于是在不增加编程复杂性的 前提下大大提高了程序的运行效率。更详细的内容可参考《STL源码剖析》第二、三章中的相关内容。
  带有“[]”的new和delete
  我们经常会通过new来动态创建一个数组,例如:
  char* s = new char[100];
  ……
  delete s;
  严格的说,上述代码是不正确的,因为我们在分配内存时使用的是new[],而并不是简单的new,但释放内存时却用的是delete。正确的写法是使用delete[]:
  delete[] s;
  但是,上述错误的代码似乎也能编译执行,并不会带来什么错误。事实上,new与new[]、delete与delete[]是有区别的,特别是当用来操作复杂类型时。假如针对一个我们自定义的类MyClass使用new[]:
  MyClass* p = new MyClass[10];
  上述代码的结果是在堆上分配了10个连续的MyClass实例,并且已经对它们依次调 用了构造函数,于是我们得到了10个可用的对象,这一点与Java、C#有区别的,Java、C#中这样的结果只是得到了10个null。换句话说,使用 这种写法时MyClass必须拥有不带参数的构造函数,否则会发现编译期错误,因为编译器无法调用有参数的构造函数。
  当这样构造成功后,我们可以再将其释放,释放时使用delete[]:
  delete[] p;
  当我们对动态分配的数组调用delete[]时,其行为根据所申请的变量类型会有所不 同。如果p指向简单类型,如int、char等,其结果只不过是这块内存被回收,此时使用delete[]与delete没有区别,但如果p指向的是复杂 类型,delete[]会针对动态分配得到的每个对象调用析构函数,然后再释放内存。因此,如果我们对上述分配得到的p指针直接使用delete来回收, 虽然编译期不报什么错误(因为编译器根本看不出来这个指针p是如何分配的),但在运行时(DEBUG情况下)会给出一个Debug assertion failed提示。
  到这里,我们很容易提出一个问题——delete[]是如何知道要为多少个对象调用析构函数的?要回答这个问题,我们可以首先看一看new[]的重载。
  class MyClass
  {
  int a;
  public:
  MyClass() { printf("ctorn"); }
  ~MyClass() { printf("dtorn"); }
  };
  void* operator new[](size_t size)
  {
  void* p = operator new(size);
  printf("calling new[] with size=%d address=%pn", size, p);
  return p;
  }
  // 主函数
  MyClass* mc = new MyClass[3];
  printf("address of mc=%pn", mc);
  delete[] mc;
  运行此段代码,得到的结果为:(VC2005)
  calling new[] with size=16 address=003A5A58
  ctor
  ctor
  ctor
  address of mc=003A5A5C
  dtor
  dtor
  dtor
  虽然对构造函数和析构函数的调用结果都在预料之中,但所申请的内存空间大小以及地址的 数值却出现了问题。我们的类MyClass的大小显然是4个字节,并且申请的数组中有3个元素,那么应该一共申请12个字节才对,但事实上系统却为我们申 请了16字节,并且在operator new[]返后我们得到的内存地址是实际申请得到的内存地址值加4的结果。也就是说,当为复杂类型动态分配数组时,系统自动在最终得到的内存地址前空出了 4个字节,我们有理由相信这4个字节的内容与动态分配数组的长度有关。通过单步跟踪,很容易发现这4个字节对应的int值为0x00000003,也就是 说记录的是我们分配的对象的个数。改变一下分配的个数然后再次观察的结果证实了我的想法。于是,我们也有理由认为new[] operator的行为相当于下面的伪代码:
  template <class T>
  T* New[](int count)
  {
  int size = sizeof(T) * count + 4;
  void* p = T::operator new[](size);
  *(int*)p = count;
  T* pt = (T*)((int)p + 4);
  for(int i = 0; i < count; i++)
  new(&pt) T();
  return pt;
  }
  上述示意性的代码省略了异常处理的部分,只是展示当我们对一个复杂类型使用new[] 来动态分配数组时其真正的行为是什么,从中可以看到它分配了比预期多4个字节的内存并用它来保存对象的个数,然后对于后面每一块空间使用 placement new来调用无参构造函数,这也就解释了为什么这种情况下类必须有无参构造函数,最后再将首地址返回。类似的,我们很容易写出相应的delete[]的实 现代码:
  template <class T>
  void Delete[](T* pt)
  {
  int count = ((int*)pt)[-1];
  for(int i = 0; i < count; i++)
  pt.~T();
  void* p = (void*)((int)pt – 4);
  T::operator delete[](p);
  }
  由此可见,在默认情况下operator new[]与operator new的行为是相同的,operator delete[]与operator delete也是,不同的是new operator与new[] operator、delete operator与delete[] operator。当然,我们可以根据不同的需要来选择重载带有和不带有“[]”的operator new和delete,以满足不同的具体需求。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值