new 和動態數組
為了讓 new
分配一個對象數組,要在類型名之后跟一對方括號,在其中指明要分配的對象的數目,返回指向第一個對象的指針,方括號中的大小必須是整型,但不必是常量:
int* pia = new int[get_size()]; // pia 指向第一個int
也可以使用一個表示數組類型的類型別名來分配一個數組,new
表達式中就不需要方括號了:
typedef int arrT[42]; // arrT 表示42個int的數組類型
int* p = new arrT; // 分配一個42個int的數組,p指向第一個int
分配一個數組會得到一個元素類型的指針
雖然稱 new T[]
分配的內存稱為 “動態數組”,但是使用 new
分配一個數組時,但是我們並未得到一個數組類型的對象,而是得到一個數組元素類型的類型。
由於分配的內存並不是一個數組類型,因此不能對動態數組調用 begin
或 end
來返回指向首元素和尾后元素的指針。出於相同的原因,也不能使用范圍 for
語句來處理動態數組中的元素。
注意:
動態數組並不是數組類型。
初始化動態分配對象的數組
默認情況下,new 分配的對象,不管是單個分配的還是數組中的,都是默認初始化的。可以對數組中的元素進行值初始化,方法是在大小之后跟一對空括號:
int *pia = new int[10]; //10個未初始化的int
int *pia2 = new int[10](); //10個值初始化為0的int
string *psa = new string[10]; //10個空string
string *psa2 = new string[10](); //10個空string
在新標准中,可以提供一個原始初始化器的花括號列表:
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// 10個string,前4個用給定的初始化器初始化,剩余的進行值初始化
string *psa3 = new string[10]{"a","an","the",string(3,'x')};
如果初始化器數目小於元素數目,剩余元素將進行值初始化。如果初始化器大於元素數目,則 new表達式失敗,不會分配任何內存。
動態分配一個空數組時合法的
size_t n = get_size(); //get_size 返回需要的元素數目
int *p = new int[n]; //分配數組保存元素
for(int *q = p;q != p + n;++q)
{//處理數組}
上面的代碼,如果 get_size()
返回的是0 程序也能正常運行。雖然不能創建一個大小為0的靜態數組對象,但是調用 new[0]
是合法的:
char arr[0]; //錯誤,不能定義長度為0的靜態數組
char *cp = new char[10]; //正確,但是cp不能解引用
當用 new
分配一個大小為0的數組時,new
返回一個合法的非空指針。此指針保證與 new
返回的其它任何指針都不相同。對於零長度的數組來說,此指針就像尾后指針一樣,可以像使用尾后迭代器一樣使用這個指針。可以使用此指針進行比較操作,可以向此指針加上或者減去0,也可以從這個指針減去自從而得到0,但此指針不能解引用,因為它不指向任何元素。
釋放動態數組
為了釋放動態數組,delete
在指針前加一個空方括號對:
delete p; //p必須指向一個動態分配的對象或為空
delete [] pa; //pa 必須指向一個動態分配的數組或為空
第二個語句銷毀 pa
指向的數組中的元素,並釋放對應的內存,數組中的元素被按逆序銷毀,即最后一個元素首先被銷毀,然后是倒數第二個,以此類推。
當釋放一個指向數組的指針時,空方括號對是必須的:它指示編譯器此指針指向一個對象數組的第一個元素,如果在 delete
一個指向數組的指針時忽略了方括號,或者是 delete
一個指向單一對象的指針時使用了方括號,其行為是未定義的。
typedef int arrT[42];
int *p = new arrT;
delete [] p; //即使使用類型別名來定義一個數組類型時,delete時[]也是必須的
智能指針和動態數組
標准庫提供了一個可以管理 new 分配的數組的 unique_ptr 版本,為了用一個 unique_ptr 管理動態數組,我們必須在對象類型后面跟一對空的方括號:
unique_ptr<int []> up(new int[10]); //up指向一個包含10個未初始化int的數組
up.release(); //自動用delete銷毀其指針
類型說明符中的方括號 (<int []>)
指出 up 指向一個 int 數組而不是一個 int
,由於 up
指向一個數組,當 up
銷毀它管理的指針時,會自動調用 delete[]
。
指向數組的 unique_ptr 不能使用點和箭頭成員運算符,而應該使用下標運算符訪問數組中的元素:
for(size_t i = 10;i != 10;++i)
up[i] = i;
與 unique_ptr
不同,shared_ptr
不直接支持管理動態數組,如果希望使用 shared_ptr
管理一個動態數組,必須提供自己定義的刪除器:
shared_ptr<int> sp(new int[10],[](int *p){delete [] p;});
sp.reset(); //使用提供的lambda釋放數組
如果沒有提供刪除器,這段代碼將是未定義的。默認情況下, shared_ptr
使用 delete
銷毀它所指向的對象。如果此對象是一個動態數組,對其使用 delete
所產生的問題與釋放一個動態數組指針時忘記 []
產生的問題一樣。
shared_ptr
不直接支持動態數組管理這一特性會影響如何訪問數組中的元素:
for(size_t i = 0;i != 0;++i)
*(sp.get() + i) = i; //使用 get 獲取一個內置指針
shared_ptr
未定義下標運算符,而且智能指針類型不支持指針算術運算,因此,為了訪問數組中的元素,必須用 get
獲取一個內置指針,然后用它來訪問數組元素。
allocator 類
new
有一些靈活性上的局限,其中一方面表現在它將內存分配和對象構造組合在一起,類似的,delete
將對象析構和內存釋放組合在一起。一般當分配單個對象時,通常希望將內存分配和對象初始化組合在一起。
當分配一大塊內存時,並且在這塊內存上按需構造對象。在此情況下,希望將內存分配和對象構造分離,這意味着可以分配大塊內存,但只在真正需要時才真正執行對象創建操作。
一般情況下,將內存分配和對象構造組合在一起可能會導致不必要的浪費:
string *const p = new string[n]; //構造n個空string
string s;
string *q = p;
while(cin >> s && q != p + n)
*q++ = s;
const size_t size = q - p;
delete [] p ;
new
表達式分配並初始化了 n
個 string
,但是實際使用時可能不需要 n
個 string
,少量的 string
就足夠了,這樣,我們就創建了一些可能永遠也用不到的對象。而且,對於那些確實要使用的對象,在初始化之后會立即賦予新值,每個使用的 元素都被賦值兩次。
更重要的是,那些沒有默認構造函數的類就不能動態分配數組。
allocator 類
allocator
定義在頭文件 memory
中,它幫助我們將內存分配和對象構造分離,它提供了一種類型感知的內存分配方法,它分配的內存是原始的、未構造的。
allocate 是一個模板,為了定義一個 allocator 對象,必須指明這個 allocator 可以分配的對象類型。當一個 allocator 對象分配內存時,它會根據給定對象類型來確定恰當的內存大小和對齊位置:
allocator<string> alloc;
auto const p = alloc.allocate(n); //分配n個未初始化的 string
allocator 分配未構造的內存
allocator
分配的內存是未構造的,后期可以在此內存中按需構造對象。construct
成員函數接受一個指針和零個或多個額外參數,在給定位置構造一個元素,額外參數用來初始化構造的對象,這些額外參數必須是與構造的對象的類型相匹配的合法的初始化器:
auto q = p;
alloc.construct(q++); //*為空字符串
alloc.construct(q++,10,'c'); //*q為cccccccccc
alloc.construct(q++,"hi"); //*q為 hi
construct
只接受兩個參數:指向創建對象位置的指針和一個元素類型的值。
還未構造對象的情況下就使用原始內存是錯誤的:
cout<< *p <<endl; //正確,使用 string 的輸出運算符
cout<< *q <<endl; //災難,q指向未構造的內存
注意:
為了使用 allocate
返回的內存,必須使用 construct
構造對象,使用未構造的內存,其行為是未定義的。
當使用完對象后,必須對每個構造的元素調用 destroy
來銷毀它們,函數 destroy
接受一個指針,對指向的對象執行析構函數:
while (q != p)
alloc.destroy(--q);
在循環開始處,q
指向最后構造的元素之后的位置。在調用 destroy
之前對 q
進行了遞減操作。
釋放內存通過調用 deallocate
來完成:
alloc.deallocate(p,n);
傳遞給 deallocate
的指針不能為空,它必須指向由 deallocate
分配的內存,且傳遞給 deallocate
的大小參數必須與調用 allocate
分配內存時提供的大小參數值相等。
拷貝和填充未初始化內存的算法
allocator
類定義了兩個伴隨算法,可以在未初始化內存中創建對象,它們也定義在頭文件 memory
中。
auto p = alloc.allocate(vi.size() *2); //分配比vi中元素所占用空間大一倍的動態內存
auto q = uninitialized_copy(vi.begin(),vi.end(),p); //拷貝vi中的元素構造從p開始的元素
uninitialized_fill_n(q,vi.size(),42); //將剩余的元素初始化為42
uninitialized_copy
調用會返回一個指針,指向最后一個構造的元素之后的位置。