C++ Primer學習筆記——$20 內存分配

導讀:
  題記:本系列學習筆記(C++ Primer學習筆記)主要目的是討論一些容易被大家忽略或者容易形成錯誤認識的內容。只適合於有了一定的C++基礎的讀者(至少學完一本C++教程)。
  
  作者: tyc611, 2007-03-03
  本文主要討論C++的內存分配機制,operator new和operator delete函數的重載等內容。
  如果文中有錯誤或遺漏之處,敬請指出,謝謝!
  C++中,內存分配和對象構造緊密相關,就像對象析構和內存回收一樣。使用new表達式的時候,分配內存,並在該內存中構造一個對象;使用delete表達式的時候,調用析構函數撤銷對象,並將對象所用內存還給系統。
  
  如用戶程序要接管內存分配,就必須處理這兩個任務。分配原始內存時,必須在內存中構造對象;在釋放內存之前,必須保證適當地撤銷這些對象。對未構造的內存中的對象進行賦值而不是初始化(嚴格地說,此時對象根本就不存在),其行爲是未定義的。對許多類而言,這樣做引起運行時崩潰。賦值涉及刪除現存對象,如果沒有現存對象,賦值操作符中的動作就會有災難性效果。
  
  C++中的內存分配
  
  C++提供下面兩種方法分配和釋放未構造的原始內存:
  1)allocator類,它提供可感知類型的內存分配。這個類支持一個抽象接口,以分配內存並隨後使用該內存保存對象;
  2)標準庫中的 operator new 和 operator delete ,它們分配和釋放需要大小的原始的、未類型化的內存。
  
  C++還提供不同的方法在原始內存中構造和撤銷對象:
  1)allocator類定義了名爲construct和destroy的成員,其操作正如它們名字所指示的那樣:construct成員在未構造內存中初始化對象,destroy成員在對象上運行適當的析構函數;
  2)定位new表達式(placement new expression)接受指向未構造內存的指針,並在該空間中初始化一個對象或一個數組;
  3)可以直接調用對象的析構函數來撤銷對象,運行析構函數並不釋放對象所在的內存;
  4)算法uninitialized_fill和uninitialized_copy像fill和copy算法一樣執行,除了它們在目的地構造對象而不是給對象賦值之外。
  
  注:現代的C++程序一般應該使用allocator類來分配內存,它更安全更靈活。但是,在構造對象的時候,用new表達式比allocator::construct成員更靈活。有幾種情況下必須使用new。
  
  allocator類
  
  allocator類是一個模板,它提供類型化的內存分配以及對象構造與撤銷,頭文件爲。相關操作如下:
  
  allocator a; 定義名爲a的allocator對象,可以分配內存或構造T類型對象
  a.allocate(n) 分配原始的未構造內存以保存T類型的n個對象,返回指向首地址的指針
  a.deallocate(p, n) 釋放內存,這段內存的起始地址爲T*指針p,共有n個T類型的對象。在調用deallocate之前,調用在該內存中構造的任意對象的destroy是用戶的責任
  a.construct(p, t) 在T*指針p所指內存中構造一個新元素。調用T類型的拷貝構造函數初始化該對象爲t的副本
  a.destroy(p) 運行T*指針p所指對象的析構函數
  uninitialized_copy(b, e, b2) 從迭代器b和e標識的輸入範圍將元素拷貝到從迭代器b2開始的未構造的原始內存中。該函數在目的地構造元素,而不是給它們賦值。假定由b2指出的目的地足以保存輸入範圍中元素的副本
  uninitialized_fill(b, e, t) 將由迭代器b和e指出的範圍中的對象初始化爲t的副本。假定該範圍是未構造的原始內存,使用拷貝構造函數構造對象
  uninitialized_fill(b, e, t, n) 將由迭代器b和e指出的範圍中至多n個對象初始化爲t的副本。假定範圍內至少爲n個元素大小,使用拷貝構造函數構造對象
  
  allocator類將內存分配和對象構造分開。當allocator對象分配內存的時候,它分配適當大小並排列成保存給定類型對象的空間。但是,它分配的內存是未構造的,allocator用戶必須分別construct和destroy放置在該內存中的對象。
  
  我們拿vector類舉例。爲了獲得可接受的性能,vector預先分配比所需元素空間更大的元素空間。每個將元素加到容器中的vector成員檢查是否有可用空間以容納另一元素。如果有,該成員在預分配內存中下一個可用位置初始化一個對象;如果沒有空的元素空間可用,就重新分配vector;vector獲取新的空間,將現存元素拷貝到該空間,增加新元素,並釋放舊空間。vector所用存儲開始是未構造內存,它還沒有保存任何對象。將元素拷貝或增加到這個預分配空間的時候,必須使用allocator類的construct成員構造元素。
  
  爲了更詳細地說明,我們試着實現一個小型的vector demo,將之命名爲Vector,以區別於標準類vector:
  template class Vector {
  public:
  Vector(): elements(0), first_free(0), end(0) { }
  void push_back(const T&);
  // ...
  private:
  static std::allocator alloc; // object to get raw memory
  void reallocate(); // get more space and copy existing elements
  T* elements; // pointer to first element in the array
  T* first_free; // pointer to first free element in the array
  T* end; // pointer to one past the end of the array
  };
  
  注意,上面的alloc成員是static,因爲我們只需要使用它的一些成員函數,其內部並不保存我們的數據。它唯一擁有的信息是它的分配類型T,而這與Vector模板的任何特定實例化類型都是一一對應的。
  
  類Vector中三個指針的含義如下圖所示:
  070303113058.jpg
  
  
  成員push_back實現如下:
  template
  void Vector::push_back(const T& t)
  {
  if (first_free == end)
  reallocate(); // gets more space and copies existing elements ot it
  alloc.construct(first_free, t);
  ++first_free;
  }
  
  成員reallocate實現如下,這是Vector類最重要的一個成員:
  template
  void Vector::reallocate()
  {
  // compute size of current array and allocate space for twice as many elements
  std::ptrdiff_t size = first_free - elements;
  std::ptrdiff_t newcapacity = 2 * max(size, 1);
  // allocate space to hold newcapacitynumber of elements of type T
  T* newelements = alloc.allocate(newcapacity);
  
  // construct copies of the existing elements in the new space
  uninitialized_copy(elements, first_free, newelements);
  
  // destroy the old elements in reverse order
  for (T *p = first_free; p != elements; /* empty */ )
  alloc.destroy(--p);
  // deallocate cannot be called on 0 pointer
  if (elements)
  // deallocate the memory that held the elements
  alloc.deallocate(elements, end - elements);
  
  // make our data structure point to the new elements
  elements = newelements;
  first_free = elements + size;
  end = elements + newcapacity;
  }
  
  operator new 函數 和 operator delete 函數
  
  首先,需要對new和delete表達式怎樣工作有清楚的理解。當使用new表達式
  string *sp = new string("initialized"); // 注意這裏的new是操作符,不是函數
  的時候,實際上發生三個步驟:首先,該表達式調用名爲operator new的標準庫函數,分配足夠大的原始的未類型化的內存,以保存指定類型的一個對象;接下來,運行該類型的一個構造函數,用指定初始化式構造對象;最後,返回指向新分配並構造的對象的指針。
  當使用delete表達式
  delete sp; // 注意這裏的delete是操作符,不是函數
  刪除動態分配對象的時候,發生兩個步驟:首先,對sp指向的對象運行適當的析構函數;然後,通過調用名爲operator delete的標準庫函數釋放該對象所用內存。
  
  注意:標準庫函數operator new和operator delete的命名容易讓人誤解。與其他operator函數不同,這些函數沒有重載new或delete表達式(操作符),實際上,我們不能重定義new和delete表達式的行爲。通過調用operator new函數執行new表達式獲得內存,並接着在該內存中構造一個對象,通過執行delete表達式撤銷一個對象,並接着調用operator delete函數,以釋放內存。即,標準庫函數operator new只是分配空間而不負責構造對象,operator delete函數只是釋放空間而不負責撤銷對象。
  
  operator new 和 operator delete函數有兩個重載版本,每個版本支持相關的new表達式和delete表達式:
  void* operator new (size_t); // allocate an object
  void* operator new [] (size_t); // allocate an array
  
  void operator delete (void*); // free an oject
  void operator delete [] (void*); // free an array
  
  雖然operator new 和 operator delete函數的設計意圖是供new表達式使用,但它們通常是標準庫中的可用函數。可以使用它們獲得未構造內存,它們有點類似allocator類的allocate和deallocate成員。例如,代替使用allocator對象,可以在Vector類中使用operator new 和 operator delete函數。在分配新空間時我們曾經編寫:
  T* newelements = alloc.allocate(newcapacity);
  這裏可以改寫爲:
  T* newelements = static_cast (operator new[] (newcapacity * sizeof(T)));
  類似地,在重新分配由Vector成員elements指向的舊空間的時候,我們曾經編寫:
  alloc.deallocate(elements, end - elements);
  這裏可以改寫爲:
  operator delete[] (elements);
  這些函數的表現與allocator類的allocate和deallocate成員類似,但在一個重要方面不同:它們在void*指針而不是類型化的指針上進行操作。
  
  一般而言,使用allocator類比直接使用operator new 和 operator delete函數更爲類型安全。allocate成員分配類型化的內存,所以使用它的程序可以不必計算以字節爲單位的所需內存量,它們也可以避免對operator new的返回值進行強制類型轉換。類似地,deallocate釋放特定類型的內存,也不必轉換爲void*。
  
  定位new表達式和顯式析構函數的調用
  
  標準庫函數operator new 和 operator delete是allocator的allocate和deallocate成員的低級版本,它們都只分配但不初始化內存。
  
  allocator的成員construct和destroy也有兩個低級選擇,這些成員在由allocator對象分配的空間中初始化和撤銷對象。
  
  類似於construct成員,有第三種new表達式,稱爲定位new(placement new)。定位new表達式在已分配的原始內存中初始化一個對象,它與new的其他版本的不同之處在於,它不分配內存。相反,它接受指向已分配但未構造內存的指針,並在該內存中初始化一個對象。實際上,定位new表達式使我們能夠在特定的、預分配的內存地址構造一個對象。
  
  定位new表達式的形式是:
  new (place-address)type
  new (place_address) type(initializer_list)
  其中place_address必須是一個指針,而initializer_list提供了(可能爲空的)初始化列表,以便在構造新分配的對象時使用。
  
  可以使用定位new表達式代替Vector實現中的construct調用。原來的代碼:
  alloc.construct (first_free, t);
  可以改寫爲等價的定位new表達式代替:
  new (first_free) T(t);
  
  定位new表達式比allocator類的construct成員更靈活。定位new表達式初始化一個對象的時候,它可以使用任何構造函數,並直接建立對象。construct函數總是使用拷貝構造函數。
  
  正如定位new表達式是調用allocator類的construct成員的低級選擇,我們可以使用析構函數的顯式調用作爲調用destroy函數的低級選擇。
  
  在使用allocator對象的Vector版本中,通過使用destroy函數清除每個元素:
  for (T *p = first_free; p != elements; /* empty */ )
  alloc.destroy(--p);
  可以改寫爲:
  for (T *p = first_free; p != elements; /* empty */ )
  (--p)->~T(); // call the destructor
  
  類特定的new和delete
  
  默認情況下,new和delete表達式通過調用標準庫定義的operator new和operator delete版本分析內存和釋放內存。類也可以通過定義自己的名爲operator new和operator delete成員來優化管理自身類型的內存分配和釋放。
  
  編譯器看到類類型的new或delete表達式的時候,它查看該類是否有operator new和operator delete成員,如果類定義(或繼承)了自己的成員new和delete函數,則使用那些函數爲對象分配和釋放內存;否則,調用這些函數的標準庫版本。
  
  當通過這種方式來優化new和delete的行爲時,只需要定義operator new和operator delete的新版本,new和delete表達式自己負責對象的構造和撤銷。如果類定義了這兩個函數中的一個,它也應該定義另一個。
  
  類成員operator new函數必須具有返回類型爲void*,並接受size_t類型的參數。在new表達式中調用operator new函數時,new表達式用以字節計算的分配內存量初始化函數的size_t參數。
  類成員operator delete函數必須具有返回類型void。它可以定義爲接受單個void*類型形參,也可以定義爲接受兩個形參,即void*和size_t類型。在delete表達式中調用operator delete函數時,delete表達式用被delete的指針初始化void*形參,該指針可以爲空指針。如果提供了size_t形參,就由編譯器用第一個形參所指對象的字節大小自動初始化size_t形參。
  除非類是某繼承層次的一部分,否則形參size_t不是必需的。當delete指向繼承層次中類型的指針時,指針可以指向某基類對象,也可以指向派生類對象。派生類對象的大小一般比基類對象大。如果基類有virtual析構函數,則傳給operator delete的大小將根據被刪除指針所指對象的動態類型而變化;如果基類沒有virtual析構函數,那麼,通過基類指針刪除指向派生類對象的指針的行爲,跟往常一樣是未定義的。
  
  operator new和operator delete函數隱式地爲靜態函數,不必顯式地將它們聲明爲static,雖然這樣做是合法的。成員new和delete函數必須是靜態的,因爲它們要麼在構造對象之前使用(operator new),要麼在撤銷對象之後使用(operator delete),因此,這些函數沒有成員數據可操縱。像任意其他靜態成員函數一樣,new和delete只能直接訪問所屬類的靜態成員。
  
  也可以定義成員operator new[]和operator delete[]來管理類類型的數組。如果這些operator函數存在,編譯器就使用它們代替全局版本。
  
  類成員operator new[]必須具有返回類型void*,並且接受第一個形參類型爲size_t。new表達式用存儲該數組所需的字節數自動初始化operatore new[]的size_t形參。
  類成員operator delete[]必須具有返回類型爲void,並且第一個參數爲void*類型。delete表達式用表示該數組的起始地址自動初始化operator delete[]的void*形參。類的operator delete[]也可以有兩個形參,第二個形參爲size_t類型。如果提供了這個附加形參,由編譯器用數組所需存儲量的字節數自動初始化這個形參。
  
  如果類定義了自己的成員new和delete,類的用戶仍可以通過使用全局作用域操作符強制new或delete表達式使用全局的庫函數。例如:
  Type *p = ::new Type; // uses global operator new
  ::delete p; // uses global operator delete
  注意:在定義或調用new和delete時,它們始終應該配對出現。比如,要麼都調用全局的,要麼都調用類成員;定義一個應當同時定義另一個。
  
  示例代碼如下:
  
  #include
  classFoo {
  public:
  void*operatornew(std::size_tsize){
  std::cout<<"Foo::operator new"<  return::operatornew(size)
  }
  void*operatornew[](std::size_tsize){
  std::cout<<"Foo::operator new[]"<  return::operatornew[](size)
  }
  voidoperatordelete(void*p){
  std::cout<<"Foo::operator delete"<<?xml:namespace prefix = std />  ::operatordelete(p)
  }
  voidoperatordelete[](void*p){
  std::cout<<"Foo::operator delete[]"<  ::operatordelete[](p)
  }
  }
  intmain (){
  std::cout<<"> Class member"<  Foo *p =newFoo
  deletep
  Foo *pa =newFoo[5]
  delete[] pa std::cout<<"> Global"<  Foo *p2 =::newFoo
  ::deletep2
  Foo *pa2 =::newFoo[5]
  ::delete[] pa2
  
  return0
  }
  運行結果爲:
  >Class member
  Foo::operator new
  Foo::operator delete
  Foo::operator new[]
  Foo::operator delete[]
  >Global
  Terminated with return code 0
  Press any key to continue ...
  
  如果文中有錯誤或遺漏之處,敬請指出,謝謝!
  參考文獻:
  [1] C++ Primer(Edition 4)
  [2] Thinking in C++(Volume Two, Edition 2)
  [3] International Standard:ISO/IEC 14882:1998

本文轉自
http://blog.chinaunix.net/u/18517/showart_252790.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章