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,以滿足不同的具體需求。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章