C++自修入門實境秀、C++ Primer 5版研讀秀 100-3/ ~12.3.1.~練習12.27-中文版重譯、複習動態記憶體與智慧指標~頁484



12.2 Dynamic Arrays

12.2動態陣列(其實是指向元素型別的指標)

動態陣列(其實是指標,指向元素型別)

第100集3:25:54 動態陣列其實就是動態配置的多個物件(動態配置物件(dynamically allocated object)的複數)

故亦可名之為「由new配置出來的N個元素所組成的記憶體區塊」(參見後文)。

增加程式碼標色的程式:字型改定為Consolas字型。

重譯中文版:

new和delete運算子一次(同時間)只能配置一個物件。有些運作卻需要在同時間配置好多個物件;比如,vector和string這類容器都需要在它們每次被建置時(whenever the container has to be reallocated)一次性地在連續的記憶體位置上配置多個元素。(§9.4,頁355)

為了支援這項功能,C++和其程式庫就提供了2種方式來達成一次性配置好這類陣列式物件的目的。首先,是C++語言定義了另一種new的語法(expression表達式)來配置及初始化陣列物件。其次,則是程式庫方面也定義了一個叫作allocator的模板類別將配置與初始化的工作分開來。由於我們會在§12.2.2(頁481)時談到的理由,若能利用alloctor的話,常會有更好的效能表現,對記憶體的管控也能更加靈活。

許多,甚至可說是在大多數的情況下,並沒有直接需要用到動態陣列之處。當必須用上(application)不定量的物件時(即不確定要用到多少物件的時候),像我們先前利用vector這類的程式庫容器來配置Strblob這樣的做法,就可以說是最容易、也最快且最妥善(safer)的應用(application)方式。尤其在新標準下(我們會在§13.6(頁531)時解釋為什麼)用程式庫所提供的容器來處理這類的操作,其優勢就更加顯著。而且符合新標準規範的程式庫,其表現也較舊版本傑出得多。

養成好習慣:在實務上(applications)也應該盡量使用程式庫容器,而不要去動到動態陣列。利用程式庫容器來寫我們要用的程式碼、達成我們想要做好的工作,不但容易上手得多,效能也會更好,且對記憶體的管控也較不會出錯(less likely to contain memory-management bugs)。

3:32:21

就如我們所見,那些利用程式庫容器的類別,都能直接將這些容器定義(default; use the default versions of the operations for)的運算方式,拿來利用,諸如對其物件的拷貝、指定與解構(§7.1.5,頁267)等,無所不可。(如前面我們用了vector的push_back運算來實作StrBlob的pusu_back成員函式就是: )而若是用到動態陣列的類別,就必須對這類的操作先定義好,才能在拷貝、指定與解構其類別物件時有效地管控所配置的記憶體。

警告:在學會13章的技能前,千萬不要在你的類別中,用到動態陣列。

再重譯一次:

new和delete運算子一次只能配置一個物件,有些時候會需要一次為許多物件同時在記憶體內配置儲存區。比如說vector和string會將它們的元素儲存在連續的記憶體位址中,並且必須在每次容器需要重新配置(§9.4)時,一次性地配置好多個元素。

為了要滿足這種需求,所以C++語言和其程式庫就提供了兩種方式來一次性地配置陣列物件(因為陣列一定是由一至多個元素組成的集合/群集。多個同質(型別)元素的組合或集合、成為一個整體,就叫「陣列」)。第一種方式,是C++定義了第二種new的表達式來配置和初始化一個陣列物件。第二種方式則是程式庫納入了一個模板類別,名為allocator,它讓我們可以將配置與初始化物件的工作分開來執行。因為在§12.2.2(頁481)中會提到的因素,我們若能多利用allocator來配置和初始化物件的話,通常會得到更佳的效能,且對記憶體的管理也靈巧得多。

在實務上,甚至可以說是絕大多數的實際應用上,動態陣列(dynamic arrays)是很罕用到的。當需要用到數目不定的物件時,若能以我們處理過StrBlob那樣的方式來進行物件的配置與管控,可以說是更為容易、且更快速、更安全的作法;也就是盡量利用程式庫容器,諸如vector,來管控我們所需用到的物件。因為§13.6(頁531)中會談到的原因,在新標準下若能利用程式庫容器,其好處決定是更加明顯的。尤其是符合新標準規範的程式庫,在執行效能的表現上通常會比之前的版本亮眼許多。

養成好習慣:應該盡量使用程式庫容器,而非動態配置的陣列,來管理眾多物件(元素)。使用程式庫容器不但相較於動態陣列容易得多,且也比較不會產生記憶體管理上的問題,甚至非常有(and)可能得到更好的效能。

如我們見過的,使用了容器類別,就可運用它現成的拷貝、指定及解構(§7.1.5,頁267)等運算。然而,配置動態陣列的類別就必須定義它們自己版本的這些運算,以便在物件拷貝、指定或摧毀時,管理關聯的記憶體。

警告:在閲讀第13章前,你寫的程式絕對不要在類別中配置動態陣列。

頁477

3:41:20

12.2.1 new and Arrays

new運算子和陣列

怎麼用new來配置動態陣列

在用new運算子來配置物件陣列時,我們可以在一個型別名稱後面的一對方括號(方括弧、中括號、中括弧)中指定要配置物件的數量,用這樣的表達式來配置物件陣列。這樣做後,new就會依所指定的數量來配置,只要配置成功,就會回傳一個指向該陣列第一個元素的指標。

// 調用get_size函式來決定有多少ints要作配置

int*pia = new int[get_size()]; //pia是一個指向第一個int的指標(pia:pointer int array)

所以動態陣列與內建陣列的差別就在於:方括號中的值只要是整數型的型別即可,但不必是常值。(一般陣列就須是常值,且其值為其型別的一部分)

也可以用型別別名(type alias)來定義一個陣列(allocate an array by using a type alias (§ 2.5.1, p. 67)),用以表示這個別名是一個陣列型別。在用這種型別別名來定義一個動態陣列,就可省掉方括號;因為方括號並不是型別別名的,而是型別別名的原型別的。3:46:00第100集因為「大小」是陣列型別的一部分,所以在定義型別別名(type alias)的時候,就該包括大小在內,如下:

typedef int arrT[42];//arrT就表示是一個含有42個int元素的陣列型別

arrT就是int[42]

int *p = new arrT;//建置(配置+建構(初始化))一個具有42個int元素的陣列,使得p指向這個陣列的第一個元素。總之「new」出來的就是動態配置的!

因此所謂的省略方括號,實際上是在宣告型別別名(type alias)時用掉了,在使用此別名時當然就可以「省略」了;其實是因為方括號並不是型別別名的,而是型別別名的原型別的。

也就是一個「arrT」即代表了「int[42]」

像這樣,new就會配置一個元素為int的陣列,且建置成功後會回傳一個指向該陣列第一元素的指標。即使在利用型別別名(type alias)來定義動態陣列時可以省略掉方括號,但編譯器在編譯這個表達式時仍是用「new[]」來完成的。也就是說,編譯器其實是用下列的方式來編譯這段程式碼(expression)的:

int* p=new int[42];

3:58:59

Allocating an Array Yields a Pointer to the Element Type 當用new配置某型別元素的陣列時,能取得的其實是一個其元素型別的指標

「指向其元素型別的指標」的意思是:元素是什麼型別,這個指標就是指向那個型別的指標。所以「Pointer to the Element Type」應該翻成「可以指向元素型別的指標」

雖然一般都將用new T[](T表元素型別)這樣的表達式配置出來的物件叫做動態陣列,但這種叫法不盡正確,甚至誤導了我們。當我們用一個new去配置一群物件一個陣列時,我們實際上得到的並不會是一個陣列型別的物件,而是得到一個指標型別的物件;這個指標是指向該「陣列」元素型別的指標。即使我們企圖用型別別名(type alias)去定義出一個陣列型別,也是枉然。因為new並不會真的去配置一個型別為陣列的東西(object)出來,在這種情況下,我們配置出來的其實是一個不存在的陣列(可是不可否認:確實有配置出來一群同型別的物件元素!),也就是說,並沒有一個具備陣列雛形的[num]這樣的實體存在,new只會回傳一個指向元素型別的指標,而不是陣列。



4:7:30

因為配置出來的物件型別並不是陣列型別,我們就不能對其用上begin或end(§3.5.3, p. 118)這樣的函式(只是不能用begin或end來存取「動態陣列」而已嘛,但new建置完回傳的,不就與begin函式取得的東西是一樣的嗎?)。像begin、end這些函式是利用了陣列的大小(元素數,dimension),也就是作為陣列的型別的一部分的那個值來分別回傳指向該陣列第一和末端位置的指標。同樣的,我們也無法對一個(號為)動態陣列的東西,作range for的運算,來對它的各個元素進行操作。因為這些操作都必須在該陣列具備「dimension」這種常值的屬性下,才有可能。(這也只是證明了range for只能套用在大小固定的陣列上,你不能削足適履,反倒頭來,說這個new T[]出來的就不是陣列啊!動態陣列,就是大小不固定的陣列而已。「陣列」之名到底是為了遷就這三個操作指令(begin、end、range for),才叫做陣列,還是一群同型別物件的集合,就可叫作陣列?!反而因為這3個不能套用,就說它不是陣列?倒果為因,喧賓奪主,可以這樣嗎?)

正因為「動態陣列」的元素量(dimension)是不定的(非常值),所以就不能對其進行begin、end、for諸如此類一般陣列適用的運算。

警告:我們必須搞清楚,所謂的「動態陣列」,其實並不是一個陣列(它並非陣列型別)!

It is important to remember that what we call a dynamic array does not have an array type.

we=「我們……所」;或「所……的」=what

Initializing an Array of Dynamically Allocated Objects

初始化由動態配置的物件所組成的一個陣列

4:25:20

初始化動態陣列

前面講配置,這裡要講初始化了

通常,由new動態配置出來的物件,不管是單一物件或一個陣列,都會用預設初始化的方式來初始化該物件。我們也可以用值初始化(value initialize)的方式來初始化陣列的元素值,就是在該陣列的大小標記位置(即方括號)後面再接著指定一對空的小括號(圓括號)。

頁478

4:27:03

int *pia = new int[10]; // block of ten uninitialized ints 由10個未經初始化的元素值構成的陣列(block,記憶體區。一塊,就是一個集合、群體。為什麼不用「陣列」二字,因為前面提到的,並沒有所謂的動態陣列,有的,也只有是指向其元素/物件型別的指標,故此故意不用陣列二字(array),而用block)

pia:p=poiter、i=int、a=array;psa:s=string



int *pia2 = new int[10](); // block of ten ints value initialized to 0 由10個經由值初始化為0的int元素組成的陣列

string *psa = new string[10]; // block of ten empty strings 由10個空字串(string)構成的陣列(記憶體區塊block);可見內建型別在預設初始化時並不會幫我們初始化,故其值是未定義的(undefined),猶仍是未經初始化的;而程式庫型別則其預設初始化,就會用它預設的值來做值初始化,故其預設初始化會是有定義的、預設的值。(應該說是某類別只要有預設建構器,並在預設建構器下定義初始器串列(initializer list),將其資料成員做了妥善地初始化,那麼它的型別物件就可以得到預設初始化。)

string *psa2 = new string[10](); // block of ten empty strings也是由10個空字串組成的記憶體區塊;就因為程式庫型別(尤其這裡的string型別),其預設初始化是利用了預設值來做值的初始化,故其預設初始化的結果與其值初始化的結果是一樣的

在新標準發布後,我們也可以用一個由大括號圍起來的元素初始器串列(由初始器構成的串列)來對new配置出來的物件進行初始化的工作。只要把它放在表示元素多少(陣列大小)的方括號後就可以了(用來初始化的,就叫初始器initializer。如下例,0就是第一個元素的初始器、1就是第二個……):

// block of ten ints each initialized from the corresponding initializer 由10個經由其對應的初始器來初始化的int元素所組成的記憶體區塊

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};



// block of ten strings; the first four are initialized from the given initializers

// remaining elements are value initialized 由new 配置出來的10個string元素構成的記憶體區塊(就是所謂的「動態陣列」),其中前4個元素,是經由初始器串列(initializer list)來初始化的,而剩下的則交由值初始化(value initialize)來進行其值的初始化

string *psa3 = new string[10]{"a", "an", "the",string(3,'x')};

就如同我們在對一般內建型別的陣列進行串列初始化一樣(list initialization,§3.5.1,頁114),在這裡提供的初始器串列也會用於這個動態陣列前面元素的初始化上。如果所提供的初始器串列的初始器數量比陣列的元素數還少,那麼剩餘的元素就會由值初始化來進行其初始化。如果所提供的初始器串列的初始器數多過元素的個數,那麼new的運算就會失敗,也不會配置出任何的物件。(也就是提供用來初始化元素值的初始器,只能少於、等於元素個數,不能多於!我想其中原故,亦如下標給定的索引值大過或等於其元素數一樣。超出範圍下標了。)

如果new的運算失敗,就會丟出一個bad_array_new_length型別的錯誤(例外情形)。這種錯誤類別和bad_alloc都是定義在new標頭檔中。

雖然可以用一對空的圓括號來將動態陣列的元素加以值初始化,但卻不能在括號中提供單獨一個初始器來做元素的初始化。不能在括號中提供單一初始值,這就意謂著我們無法在配置一個動態陣列時,應用auto來判斷它會配置出來的型別是什麼。(§12.1.2,頁459)

Although we can use empty parentheses to value initialize the elements of an array, we cannot supply an element initializer inside the parentheses. The fact that we cannot supply an initial value inside the parentheses means that we cannot use auto to allocate an array (§ 12.1.2, p. 459).

It Is Legal to Dynamically Allocate an Empty Array 在動態配置下,即使要定義一個沒有任何元素的陣列也是可以的

可以用任何可以接受的值(即非負數的整數型的值)來指定我們需要配置多少個物件—即使那個值是0(We can use an arbitrary expression to determine the number of objects to allocate:)

size_t n = get_size(); // get_size returns the number of elements needed get_size函式會回傳需要配置的元素量

int* p = new int[n]; // allocate an array to hold the elements 配置出一個陣列來儲存這些元素

我們拿這個n(get_size回傳的值)來作為要配置的數量值

for (int* q = p; q != p + n; ++q)//不能對p作解參考後下標(因為解參考p後得到的是p所指向的元素),但卻可以遞增、遞減p

/* process the array */ ;處理這個陣列

在這種情況下,即使get_size回傳的是「0」,也會照常執行。因為雖然我們不可能創建出一個大小為0的陣列,但用「0」這個值來作為new運算的引數卻是可以被接受的。如以下所列,有「new」就可以是「[0]」:

第100集4:48:31

char arr[0]; // error: cannot define a zero-length array 發生錯誤:因為沒有用到new,不是動態配置的,就不行!我們不能用0這個大小來定義一個普通陣列(內建陣列)——因為並不存在沒有任何元素的這樣一種陣列,只要是陣列,就必然含有至少一個元素。

char *cp = new char[0];//ok: but cp can't be dereferenced 這是可以的,因為是用new來配置一個動態陣列;但是這個cp卻是不能被解參考的,因為既然所配置的陣列大小是0,就表示並沒有任何元素存在,當然也就沒有cp所指向的元素存在!c=char、p=pointer。

當我們用new來配置一個不含任何元素的陣列時,new就會回傳一個無效且非0的指標(即非null指標,不是nullptr。這種指標其實就是懸置指標(dangling pointer),懸置指標就是不指向任何存在物件的指標。指標的無效是說它所指的物件是無效=不存在的)。這個指標決定不會與任何new成功配置後所回傳的指標混淆。對一個沒有元素的陣列而言,這個無效的懸置指標作用就如同它的尾端後指標指針(off-the-end pointer,§3.5.3,頁119)一樣。我們是怎麼使用尾端後指標的,就可以那麼用這個指標。(所以它並不是真正的懸置指標(dangling pointer))因此,這個指標就可以放在迴圈的開頭作為一個比較判斷式的一部分。可以對它加0,或者對它減0,(此加0、減0要配合下文讀)也可以對它執行遞減的運算。然而,這個指標它畢竟指向的是一個不存在的元素,當然就不能對它進行解參考(dereference)的運算了。

在前述假設的迴圈中:

for (int* q = p; q != p + n; ++q)

如果get_size回傳的是0,那麼n這個變數就是0,對new傳入n這個0的引數就會配置出0個物件(zero objects,這裡指元素)。如此for迴圈上的判斷式就會回傳false,因為此時q+n就是q+0,就還是q;而q初始就等於p(int* q = p,即用p給q初始化),所以p和q還是相等的,故這個迴圈根本就不會執行。(這個迴圈在q與p不相等時,才會執行)

頁479

Freeing Dynamic Arrays

解構動態陣列——如何清除動態陣列、釋放動態陣列所佔用的記憶體資源



4:57:31

有特殊形式的new,就有別致另類的delete。要清除動態配置的陣列,就必須用這種形式的delete表述式,這種另類別致型的delete表述式(expression)就是包含了一對空方括號的delete述句。new表述式(expression)中有方括號的話,其所對應的delete也就須有方括號:

delete p;//p必須是空指標,或是一個指向動態配置物件的指標

delete []pa;//a=array。pa(=pointer array)必須是一個指向動態陣列的指標或是一個空指標。這個表述式的意思應該就是:「用pa來delete,以清除[]」。一般的陣列(內建型別的陣列)須指定陣列大小,作為其陣列型別的一部分來宣告,而動態陣列,就是保留那指定大小的部分(即方括號內),把它空下來、保持空白,表示任何可接受的值(包括0,參見前)都是可以代入的。

空的方括號其實就是動態陣列(不定數目物件)的代表記號。上述第二個表述式(述句,statement)會摧毀所有pa指向的陣列元素,然後釋放掉它們佔用的記憶體資源。而這些陣列中的元素在被刪除時是倒著(倒序,反向的順序)來抹除的;意思就是陣列中最後一個元素會最先被刪除,然後是倒數第二個……,如此類推。

當我們對一個指向動態陣列的指標進行delete的動作時,那對空的方括號是必須的。5:4:02因為它會告訴編譯器這些元素中第一個的指標值(addresses)是什麼,萬一省略了這對方括號(或者在對一個指向單一物件的指標進行delete時卻沒清除這對方括號),那麼這樣的delete作為在C++語境中就是毫無意義的(undefined)。也就是說加了[]即表示我後面帶的指標只是一群指標的第一個,要把它的狐群狗黨給我全都找出來清除才行,不是只清除我指明的這一個。



雖然用陣列型別的型別別名(type alias)來作new的動態陣列配置操作時,可以省略掉那對方括號,但在對這樣配置出來的動態陣列執行delete時,仍然必須寫出那對方括號才行。因為別名省略只是格式上省略的善巧方便,並不是實質上的,前面也有提到即使用了型別別名來定義動態陣列,編譯時一樣會照沒有省略的語法來作編譯!別名只是提供編程者在編程時,寫作與讀碼的方便與便利的權巧措施而已,並不代表任何實質上的改變。這裡的型別別名就已經代表「元素型別[]」了,當然方括號[]就不必再寫出來了,與省不省略方括號有什麼關係?!根本就是不相干的兩回事嘛。

typedef int arrT[42];//先定義arrT來作為int[42]這樣陣列型別的型別別名(type alias);arrT現在就會是一個由42個int元素組成的陣列這樣的型別的別名。

int* p=new arrT;//應用型別別名搭配new運算子來配置這樣的動態陣列,讓p成為指向這個陣列(由42個int元素組成的陣列)第一個元素的指標

delete[]p;//因為配置的是陣列(多個動態物件,而非單一動態物件),所以空的方括號是必須的,以告知編譯器這個p只是指向一組元素物件(動態陣列)中第一個元素的指標。

儘管這裡的p實際上指向的只是陣列中的第一個元素,而不是陣列本身(即似乎只是指向單一物件的指標),但p決定不是一個指向單一arrT型別物件的指標。因此,我們仍然必須寫出[]空的方括號,才不會讓編譯器誤以為我們要delete的是一個指向單一物件的指標。

警告:編譯器通常在我們對動態陣列進行delete時忘了附上空的方括號——或者在我們對一個指向單一物件的指標進行delete時,卻錯加上了[]一對空的方括號——並不會提出警告。編譯器未必會發現這樣的錯誤。因此在編譯完成後、實際執行程式碼時,這樣的程式,往往就會在沒有任何預警的警訊下,卻頻頻出錯(is apt to misbehave)。不要到時候找不著問題出在哪裡。

Smart Pointers and Dynamic Arrays 智慧指標與動態陣列的關係

C++程式庫還定義了另外一種另類的unique_ptr類別,這種unique_ptr可以控管new出來的動態陣列。要用這樣的unique_ptr來管控動態陣列,我們就必須在對這種unique_ptr類別物件下定義時,在其指向的元素型別名稱後接上一對空的方括號,來表示要管控的是一個動態陣列。(可見「動態陣列」一定與「空的方括號」有不可分割的關係!)5:17:27

//up:u=unique、p=pointer;up指向一個由new配置出來的10個未經初始化的int所組成的陣列。可以把「[]」理解成「有n個元素」

unique_ptr<int[]>up(new int[10]);//前面學的非陣列(單一物件)是用()圓括號,不是用方括號[]。因此,用方括號即表是多個物件(即陣列),而圓括弧來與new搭配就是指僅只配置出單一一個動態物件。

up.reset();//reset會在up這樣的unique_ptr上調用delete[]運算以刪除它底層指向10個未初始化的int所構成的陣列

在型別指定式(type specifier:<int[]>這個意思就是有好多個、不只一個int)中所寫的空的方括號就指出up是一個指向由多個int所組成的陣列,而不是單一一個int的物件。因為up指向的是一個動態陣列,所以當up摧毀它底層的指標時,就會自然調用delete[]這樣的運算——這是unique_ptr類別才有的定義,shared_ptr類別是沒有的(詳下)。


指向動態陣列的unique_ptr支援的運算和我們在前面§12.1.5(頁470)見到的稍有不同。5:23:22這些不同的運算會在下頁表12.6中列出。當unique_ptr指向的是一個動態陣列時,就不能使用成員存取運算子(不論是「.」點運算子,或「->」箭號運算子 arrow operator:-> operator)來存取其成員了。因為這樣的unique_ptr指向的是一個陣列(同型別元素、或同類資料的集合),並不是單一物件(不是類別物件)(型別物件,就有類別成員;而陣列並沒有成員member,只有element元素),5:30:00對它作成員存取是毫無意義的事。然而,當unique_ptr指向的是一個陣列,那我們就可以對它指向動態陣列的unique_ptr作下標運算來存取它所指向陣列中的指定元素;因為這種unique_ptr是具備了也就是定義了下標運算子的運算的(詳後Table 12.6. unique_ptrs to Arrays 表12.6 指向動態陣列的unique_ptr特有的運算)。

for(size_t i=0;i!=10;++i)

up[i]=i;//i=index。unique_ptr在對動態陣列時定義了它自己的下標運算子([]運算子,subscript operator)運算,故不需再解參考。解參考運算子似乎指向動態陣列的unique_ptr也不支援。Table 12.6. unique_ptrs to Arrays 表12.6 指向動態陣列的unique_ptr特有的運算

將up所指陣列的每個元素,指定為i此變數之值。

頁480

Table 12.6. unique_ptrs to Arrays

表12.6 指向動態陣列的unique_ptr才有的運算



除了成員存取運算子不能用外,其他的unique_ptr運算依然有效

疑解參考運算子也不支援!

像前面的StrBlob是由shared_ptr指向的一個vector,當然也可由unique_ptr來指向。此時,可對unique_ptr作成員存取運算,也僅僅是因為它指向的是一個vector型別的物件,當然具有成員,自然可以做成員存取的運算了。而指向動態陣列的unique_ptr,既然指向的是「陣列」(或者精準地說,是一群物件集合),當然就不具備「成員」member(不管是資料成員、成員函式、型別成員……什麼的,都沒有!),沒有成員,又要存取成員作啥呢?!

unique_ptr<T[]>u; u這個指標可指向一個元素型別為T的動態配置陣列。

unique_ptr<T[]>u(p); u這個指標指向的陣列即是內建指標(built-in pointer)p所指向的,它是由T型別元素組成的一個動態陣列。p必須能夠被轉型為T*(對T的指標,§4.11.2,頁161)。

u[i] 會回傳u指向的陣列在i索引值位置上的元素值(物件)。u必須指向動態陣列(動態配置的多個物件),不能指向單一物件

不同於unique_ptr,shared_ptr類別的智慧指標就不直接支援對動態陣列的運算。如果想要用shared_ptr來管控動態陣列,就必須自行提供刪除器(deleter),以備清除之用,也就是shared_ptr並沒有像unique_ptr定義了對動態陣列的delete[]運算,shared_ptr只有delete,沒有delete[]這種!

動態陣列其實就是運算子new動態配置出來的多個物件(由方括號就可以判斷),而非單一物件。

shared_ptr<int> sp(new int[10],[](int *p){delete[] p});//「[](int *p){delete[] p}」是可呼叫物件(callable object)lambda;最前端的「[]」是lambda的捕捉串列(capture list)(頁388),而「delete[]」的「[]」才是指動態陣列(就是new配置出來的多個物件)。

sp.reset();//用我們在建構sp時所指定的lambda來調用delete[]以清除sp所指向的動態陣列(也就是以sp開頭的同時new出來的多個物件。sp和up一樣,指向的動態陣列本身,而不是其內第一個元素!)

在此例中我們將一個lambda(§10.3.2,頁388)當作刪除器(deleter)引數,傳給shared_ptr的建構器來建構一個指向動態陣列的shared_ptr。而這個lambda是用delete[]來清除這個shared_ptr所指向的動態陣列(其實就是多個物件)。也就是以delete[]來覆寫shared_ptr預設調用的delete運算。

如果在定義shared_ptr時忘了像這裡這樣提供lambda這類的刪除器(deleter),寫出來的程式碼就會是毫無意義的了。因為在沒有提供刪除器(deleter)時,shared_ptr就會直接調用delete運算子(而不是delete[])來刪除它所指向的單一物件。可是現在shard_ptr指向的並不是一個單一物件,而是多個物件(就是多個「元素」)組成的陣列(記憶體區塊block),要刪除這樣的陣列(多個物件),若在調用delete時忘了提供[]就是根本的失誤!(了不異人意,未讀到即知道課本要說什麼 5:46:20 第100集)(§12.1.1,頁479)

shared_ptr不能直接對動態陣列操作的根本原因就在於它沒有提供對這樣動態陣列適用的刪除器(deleter),它的這種特性也影響了存取其所指陣列元素的方式:

//shared_ptr類別並沒有提供下標運算子([]運算子,subscript operator),且也不支援指標算術(pointer arithmetic),可是普通指標有,所以須先以get取得其底層所存放的普通指標,再對其進行指標算術:

for(size_t i=0;i!=10;++i)

*(sp.get()+i)=i;//所以要利用get成員函式。get成員函式會回傳一個指向sp所指動態陣列第一元素的內建指標(built-in pointer,普通指標 plain pointer、ordinary pointer)。對它下「+」運算子,就是執行指標算術(pointer arithmetic),就是advance(推進)這個指標到指定的元素位址。再將「+」算術所得的結果指標解參考,即取得該位置上的元素(物件),再將區域變數i的值指定給這個物件元素。

而shared_ptr類別並未定義下標運算子(unique_ptr就有,見前頁479末行),且智慧指標也不支援一般指標的指標算術(pointer arithmetic,§3.5.3,頁119)。所以若要進行指標的推移,以便存取陣列中的元素,就必須將智慧指標轉回普通指標才行。因此,get成員函式在此就扮演了不可或缺的角色。

練習12.23

5:54:00 當初高中補張耀元老師的數學,自修開竅,就是如老師傳口訣「不見題目只見關鍵字」,如此題,看到「動態」,就要想到「new」!

寫一個程式來串接兩個字串字面值,將結果放到一個動態配置的char陣列中。再寫一個程式來串接兩個程式庫string物件,它們的值跟第一個程式中使用的字面值一樣。

寫一個程式來將二個字串字面值(string literal)串接起來,將結果放到一個經由動態配置的char陣列中。再寫一個串接兩個程式庫型別string字串的程式,將兩個與前一程式有相同值的string字串串接在一塊。

#include<iostream>

#include<memory>



using namespace std;



void concatenate_two_string_literals() {

unique_ptr<char[]>up(new char[7]);

const char* ch1 = "good"; const char* ch2 = "bye";

for (size_t i=0; i != 4; ++i)

up[i] = *(ch1 + i);

for (size_t i = 4; i != 4+3; ++i)

up[i] = *(ch2 + i-4);

for (size_t i = 0; i != 4+3; ++i)

cout<<up[i]<<",";

cout << endl;

cout<<up<<endl;

}



void concatenate_two_string() {

string s1 = "good"; string s2 = "bye";

//for (size_t i = 0; i != s1.size(); ++i)

// up[i] = s1[i];

//for (size_t i = 0; i != s2.size(); ++i)

// up[i + s1.size()] = s2[i];

string concatenateS = s1 + s2;

//size_t sz = sizeof(up) / sizeof(up[0]);

size_t sz = concatenateS.size();

unique_ptr<char[]>up(new char[sz]);

for (size_t i = 0; i != sz; ++i)

up[i] = concatenateS[i];

for (size_t i = 0; i != sz; ++i)

cout << up[i] << ",";

cout << endl;

cout << up << endl;

}



int main() {

concatenate_two_string_literals();

concatenate_two_string();

}



練習12.24

寫一個程式從標準輸入讀取一個字串到一個動態配置的字元陣列中。描述你的程式如何處理長短不定的輸入。給一個超過你程式所配置的陣列大小的長字串,來測試它。

寫一個從標準輸入讀取string字串儲存到一個動態配置的字元陣列的程式。描述一下這個程式是怎麼處理可變資料大小的輸入的。用一個比您所配置的陣列大小還大的string資料來測試一下您所寫的這個程式。

string s;//當輸入的資料長過動態配置的陣列大小時,可以輸入儲存,但在delete[]時卻會出錯

cin >> s;

//size_t sz = s.size()-1;

size_t sz = s.size();

unique_ptr<char[]>up(new char[sz]);//size回傳的非常值,故可應付動態長度的資料輸入

for (size_t i = 0; i != sz; ++i)

up[i] = s[i];

cout <<up<< endl;

for (size_t i = 0; i != sz; ++i)

cout<<up[i]<<",";

cout << endl;

//delete [] up.get();//當輸入的資料若大過陣列能接受的範圍這樣也是沒有用的。[]中指定陣列大小也無用

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise12_24/prog1/prog1.cpp


練習12.25

如下列的new宣告/定義(expression),您會如何對pa進行delete運算?

int* pa = new int[10];

delete[] pa;

頁481

第100集6:2 50

12.2.2 The allocator Class

配置器類別(分配器類別)【配置什麼?分配什麼?記憶體! ⑧找對主詞 】

12.2.2 allocator 類別

new運算子配置動態物件的運算之所以無法廣泛應用(limits its flexibility)有其局限性,就是因為它將配置與建構這兩項工作雜揉在一塊了:配置是對所需要用到的記憶體空間來配置適當容量的資源,而建構則是在所配置出來的記憶體空間區塊中,進行實際物件的建構。同樣地,delete運算子也將解構(destruction)與釋放(清除deallocation)的工作揉雜在了一塊。若要配置的只是單一物件,那麼將初始化(建構)物件和配置記憶體資源這兩項工作結合在一塊做,6:5:56確實能滿足我們所望,也不會影響到資源的利用與效能的表現。若能在事先就確定了(certainly)知道(should)要配置出來的單一物件會有什麼型別、什麼值,才適合將建構與配置這兩項工作結合起來。

結合「建構」與「配置」的操作可合稱為「建置」

block關鍵字出現了!(不見題目只見關鍵字)

然而當配置(allocate)出了一個記憶體區塊(記憶體區域,block of memory)供我們備用後,通常我們都會在需要實際用到東西(物件)的時候才會在那個區塊上去建構(construct)出來我們要用到的東西(物件)。若是有像這種情況的需求(即先配置、後建構(初始化)),我們就會想要去試著把配置記憶體資源與物件建構的工作給分割開來。將物件建構與記憶體配置分割開來,這意謂著我們知道我們有足夠的資源可以在所配置出來的記憶體中,來作足夠彈性的配置,在我們真的需要創建(create建構)物件的時候,有足夠的資源來供我們差遣。之所以會想要將建構與配置分開來做,是因為通常將這兩個工作結合起來,是非常耗費運算資源成本的,舉例來說:

string *const p = new string[n]; //建置(配置且建構)了n個空的string字串(預設初始化)

string s;

string *q = p; //q指向了p動態陣列(就是new出來的n個string物件)中的第1個字串元素

while (cin >> s && q != p + n) //「cin>>s」:讀取標準輸入到s

*q++ = s; //將一個新值(=s)指定給q指向的元素

const size_t size = q - p; //記下已經讀取了多少個動態陣列p中的字串元素物件

//……

//對這個動態陣列(new出來的多個物件)的使用

delete[] p; //這句話可以這樣來讀:將p開頭的一組[]n個同類物件全部刪除delete掉

//因為普通常值(恆定)指標(內建型別為對string的指標)p指向的是一個動態陣列,所以對它delete必須加上[]空的方括號

6:25:30在new述句中配置並初始化(建構,其實是預設初始化)了n個string,然而這些n個string也許不會全都用到;甚至可能只需要其中的一部分就好。因此(於是),在這種情況下,這樣的安排可能就會浪費太多的記憶體資源製造出太多的垃圾(caeated objcets that are never used)、及運算時間;甚至,即使是對那些我們會用到的字串物件來說,我們也可能會再次指定一個全新的值給它,而不曾用到之前經過初始化運算後的字串值。也就是說,即使用得上的物件,在用new這樣的建置(配置建構)方式來定義的話,至少得做上兩次的指定值運算:第一次是當它們被預設初始化時,另一次則是在指定新值給它們的時候。

更有甚者,那些沒有定義自己預設建構器(即無參數的建構器constructor)的類別是無法被動態配置成一個陣列的。就是無法一次用new建置多個此類別型別(類型)的物件出來

More importantly, classes that do not have default constructors cannot be dynamically allocated as an array.

這就很明顯new應該是利用類別程式庫、自訂的預設建構器來達成它的建置(配置+建構)工作裡的建構部分。就如shared_ptr是預設利用delete來進行所指物件的刪除一樣。應該是new只專管配置,而其建構工作,其實是假借類別的預設建構器所提供的初始器串列來完成配置出的物件的初始化的。

6:29:00嚴格來說型別是C++語言內建的,而類別是在這些內建型別的基礎上自定義(自訂)的,如程式庫類別。

是不是因為有預設建構器的類別可以用類別的預設初始化來初始化其型別物件,而無者則需再「指定」初始器來初始化其型別物件之值?

new本身的一個機制限制了它的彈性,這個機制就是:new綁住了記憶體的配置、以及在那個記憶體中建構物件,這兩個工作。同樣地,delete也將解構(destruction)和釋放(deallocation)這兩個動作綁在一起執行。在配置單一物件時,若將配置和初始化(就是建構)一起來做,通常就是我們所需要的(所望的)。在這情況下,我們應可確定那些要建置出來的物件會有什麼值。

當我們配置出一個記憶體區以便利用時,我們通常會在必要的時候,才打算在那個區域中建構出我們想要用到的物件。既然會想要這麼做,我們就應該會想到能否將記憶體的配置和物件的建構工作區分開來。將建構與配置二者分離開來,就意味著我們有足夠的記憶體資本(資源成本)來供我們差遣(遣用),(此意可參見Table 12.8 allocator Algorithms:「這個記憶區必須大到容得下……」;即所配置出來的記憶體空間,必須夠用!)在實際需要派上用場時,才給付建構物件需要用到的資源成本。【就像new,若給太多初始器會出現bad_array_new_length型別的(或因為記憶體不足而導致bad_alloc)錯誤一樣,可見new運算子的工作是先配置,再以其計算元所提供的初始器來對配置出來的物件作初始化——也就是建構完成】

一般說來,將配置和建構結合在一起,常會浪費不必要的資本(資源成本)。舉例來說:

string *const p = new string[n]; //建構 n 個空的 string

string s;

string *q = p; // q指向第一個 string

while (cin >> s && q != p + n)

*q++ = s; //指定一個新的值給*q

const size_t size = q - p; //記住我們讀取了多少個 string

//使用此陣列

delete[] p; // p指向一個陣列;必須記得使用delete[]

這個new運算式配置並初始化了n個string。然而,我們可能不需要用到n個string,而是少一點就夠用了。因此我們在用new來建置動態物件時,可能會建構出完全用不上的物件,而且可能還不在少數;這不就浪費了許多記憶體資源及運算成本了嗎?此外,對於我們確實會用到的物件,我們可能也會在它們經過初始化後不久就又指定一個新的值來蓋過它之前被初始化的值。所以,即使是對有用上的動態配置出來的物件元素而言,在實際運算時也常會不可避免地被重複寫入至少兩次:第一次是在預設初始化時,第二次則是在後續對它們指定值時。

甚至,當一個類別沒有預設建構器時,也就無法用new配置出一個以此類別型別為元素的動態陣列。

以上就是new運算子在建置動態物件時的局限性。new建置的一定都是動態配置的物件。所謂的「動態」是與new不可分割的!

The allocator Class

配置器類別

allocator類別

C++的程式庫在memory標頭檔中定義了一個叫做allocator的類別,這種類別足以滿足將配置和建構分離開來的需求。它會配置出一個原初(原生、生的raw)的已知型別而未經任何物件建構的記憶體區塊

It provides type-aware allocation of raw, unconstructed,memory.

可見要配置記憶體一定要提供型別!

下頁的表12.7列出了allocator類別支援的運算/操作。在現在我們這一章這一區段的課文中,我們會先討論allocator的運算,在下一章(§13.5,頁525)則會看到這個allocator類別實際運作的情形是怎樣的(In § 13.5 (p. 524), we’ll see an example of how this class is typically used. 一般應用是如何使用到這個類別的。)



就像vector,allocator類別也是一個模板類別(template,§3.3,頁96)。要定義一個allocator的類別物件(型別物件)就必須指定想要讓這個allocator去配置(allocate)的物件其型別是什麼。當allocator型別物件在配置記憶體時,它會分配適當大小(因為已知型別type-aware,所以才能配置適當的大小)且整齊並排的(放在一起的aligned)記憶體給要儲存在其中的特定型別物件使用:6:34:00

When an allocator object allocates memory, it allocates memory that is appropriately sized and aligned to hold objects of the given type:

6:35:20第100集

allocator<string>alloc;//宣告、定義一個allocator型別物件,它可以配置出一個可以存放string型別的記憶體資源(區塊)

object that can allocate strings

auto const p = alloc.allocate(n); //用alloc這個allocator型別物件來配置n個未經建構(unconstructed應即是未經初始化)的字串空間。——要注意,不是字串物件!

像這樣對allocate成員函式的呼叫就會預先為n個string物件配置好記憶體空間資源(記憶體區塊)以供存放這n個string物件。

6:58:40 程式庫的allocator類別,定義於memory標頭中,能讓我們將配置與建構的工作分開來進行。它可配置出一個可讓一定的型別存放的(type-aware)、且尚未經任何物件建構的原始、原初、原生(raw)記憶體區。表12.7描述了allocator支援的運算。在本章此處,我們只會介紹allocator的運算。到了§13.5(頁525),我們才會看到通常是如何利用這個類別的。

就像vector,allocator也是一種模板(§3.3)。因此,若要定義一個allocator物件,我們就必須指明那個allocator要配置的物件群,其型別是什麼。一個allocator物件所配置出來的記憶體會是有適當大小的【根據在定義此allocator物件時所指定的型別及其型別物件個數,來做規劃,計算出足以容下指定型別及其數量大小的記憶體區塊】,並經過對齊排列的,以便存放指定型別的物件:7:2:44

allocator<string> alloc;//能夠配置string的物件

auto const p = alloc.allocate(n);//配置n個string能放入的記憶體空間,這n個string是尚未建構出來的

對allocate的這個方法(成員函式)呼叫會為n個string配置好它們能存放的記憶體空間。

頁482

7:5:10

Table 12.7. Standard allocator Class and Customized Algorithms

表12.7標準的allocator類別和自訂的演算法

英文應是有掉字!

allocator<T> a

定義一個叫做a的allocator物件來為型別T的物件配置記憶體資源 Defines an allocator object named a that can allocate memory for objects of type T.

a.allocate(n)

疑當作:

auto p= a.allocate(n) 配置一個原初(raw)、尚未開發的(unconstructed,未經物件建構的)記憶體區塊(其實就是動態陣列的概念、模型)來放置(儲存hold)n個T型別的物件。

a.deallocate(p,n) 從p這個指向T型別的指標值(位址)位置開始,釋放n個T型別物件的記憶體資源。p必須是先前經由allocate成員函式運算回傳的指標,而n必須是當p被創建時被指定的那個大小值。在用到deallocate這個運算前,必須先對在這個記憶體區塊中被建構出來的物件呼叫destroy方法(destroy(p),詳本表末列)。



a.construct(p,args) p須是一個指標,指向一個型別為T、且為原生(raw memory)的記憶體區塊。引數args則會傳給T類別的建構器(constructor)用來在p指向的記憶體區塊中建構出一個該類別的物件。7:10:00

a.destroy(p) 在p所指向的物件上執行解構器(destructor,§12.1.1,頁452),p是一個指向T型別的指標(T* pointer p)。其實p應即是指向物件型別T的普通指標

邏輯應該是:

1)配置allocate 2)建構construct

3)解構destroy 4)釋放deallocate解配置

allocators Allocate Unconstructed Memory

allocator的allocate成員函式配置出來的是毫無物件建構的記憶體(荒漠記憶體。上面空無一物)

也就是:不是配置好了就可以用。allocate一定要搭配construct建構來用

7:15:40

allocator型別物件配置的記憶體是尚未經物件建構過的記憶體(白地,未經開墾的白地)

一個allocator型別物件所配置出來的記憶體是未經建構初始化的(unconstructed)。因為allocator類別本就是為了將建構(初始化)與配置分割開來而設的。

我們是藉由在allocator物件配置出來的記憶體區中建構物件來使用這些配置出來的記憶體。所以沒有建構物件,就不能使用這個記憶體區,即使它配置成功了也一樣。(等於是一個懸置集區了)在新的程式庫中construct這個成員函式會帶有一個指標型別的參數、還有一組或有或無(可有可無)的額外參數。這個construct成員函式會在指定的記憶體位置上建構一個元素物件出來(這群動態配置的元素的第一個),額外的參數則是被用來初始化這個元素物件的。(可見這裡建構和初始化是區分開的,但在這裡仍是一塊工作、同時進行,並未切割;但若與「配置」來比的話,則建構與初始化是一國的)就像make_shared成員函式的參數(§12.1.1,頁451)這些額外的參數args必須是有效的初始器,以初始化這個被建構出來的型別物件元素。一定要提供初始器,construct的建構工作才能順利完成。特別是,如果這個物件是一個類別型別(class type)的物件,那麼這些引數就必須和那個類別的建構器(constructor)相配合才行。可見這個allocator的construct成員函式也僅止是假借類別的建構器及其初始器串列(initializer list,建構器初始器串列(constructor initializer list))來完成其物件的建構與初始化的

就像vector,allocator類別也是一個模板類別(template,§3.3,頁96)。要定義一個allocator的類別物件(型別物件)就必須指定想要讓這個allocator去配置(allocate)的物件其型別是什麼。當allocator型別物件在配置記憶體時,它會分配適當大小(因為已知型別type-aware,所以才能配置適當的大小)且整齊並排的(放在一起的aligned)記憶體給要儲存在其中的特定型別物件使用:6:34:00

When an allocator object allocates memory, it allocates memory that is appropriately sized and aligned to hold objects of the given type:

6:35:20第100集

allocator<string>alloc;//宣告、定義一個allocator型別物件,它可以配置出一個可以存放string型別的記憶體資源(區塊)

object that can allocate strings

auto const p = alloc.allocate(n); //用alloc這個allocator型別物件來配置n個未經建構(unconstructed應即是未經初始化)的字串空間。——要注意,不是字串物件!


像這樣對allocate成員函式的呼叫就會預先為n個string物件配置好記憶體空間資源(記憶體區塊)以供存放這n個string物件。

6:58:40 程式庫的allocator類別,定義於memory標頭中,能讓我們將配置與建構的工作分開來進行。它可配置出一個可讓一定的型別存放的(type-aware)、且尚未經任何物件建構的原始、原初、原生(raw)記憶體區。表12.7描述了allocator支援的運算。在本章此處,我們只會介紹allocator的運算。到了§13.5(頁525),我們才會看到通常是如何利用這個類別的。

就像vector,allocator也是一種模板(§3.3)。因此,若要定義一個allocator物件,我們就必須指明那個allocator要配置的物件群,其型別是什麼。一個allocator物件所配置出來的記憶體會是有適當大小的【根據在定義此allocator物件時所指定的型別及其型別物件個數,來做規劃,計算出足以容下指定型別及其數量大小的記憶體區塊】,並經過對齊排列的,以便存放指定型別的物件:7:2:44

allocator<string> alloc;//能夠配置string的物件

auto const p = alloc.allocate(n);//配置n個string能放入的記憶體空間,這n個string是尚未建構出來的

對allocate的這個方法(成員函式)呼叫會為n個string配置好它們能存放的記憶體空間。

頁482

7:5:10

Table 12.7. Standard allocator Class and Customized Algorithms

表12.7標準的allocator類別和自訂的演算法

英文應是有掉字!

allocator<T> a

定義一個叫做a的allocator物件來為型別T的物件配置記憶體資源 Defines an allocator object named a that can allocate memory for objects of type T.

a.allocate(n)

疑當作:

auto p= a.allocate(n) 配置一個原初(raw)、尚未開發的(unconstructed,未經物件建構的)記憶體區塊(其實就是動態陣列的概念、模型)來放置(儲存hold)n個T型別的物件。

a.deallocate(p,n) 從p這個指向T型別的指標值(位址)位置開始,釋放n個T型別物件的記憶體資源。p必須是先前經由allocate成員函式運算回傳的指標,而n必須是當p被創建時被指定的那個大小值。在用到deallocate這個運算前,必須先對在這個記憶體區塊中被建構出來的物件呼叫destroy方法(destroy(p),詳本表末列)。



a.construct(p,args) p須是一個指標,指向一個型別為T、且為原生(raw memory)的記憶體區塊。引數args則會傳給T類別的建構器(constructor)用來在p指向的記憶體區塊中建構出一個該類別的物件。7:10:00

a.destroy(p) 在p所指向的物件上執行解構器(destructor,§12.1.1,頁452),p是一個指向T型別的指標(T* pointer p)。其實p應即是指向物件型別T的普通指標

邏輯應該是:

1)配置allocate 2)建構construct

3)解構destroy 4)釋放deallocate解配置

allocators Allocate Unconstructed Memory

allocator的allocate成員函式配置出來的是毫無物件建構的記憶體(荒漠記憶體。上面空無一物)

也就是:不是配置好了就可以用。allocate一定要搭配construct建構來用

7:15:40

allocator型別物件配置的記憶體是尚未經物件建構過的記憶體(白地,未經開墾的白地)

一個allocator型別物件所配置出來的記憶體是未經建構初始化的(unconstructed)。因為allocator類別本就是為了將建構(初始化)與配置分割開來而設的。

我們是藉由在allocator物件配置出來的記憶體區中建構物件來使用這些配置出來的記憶體。所以沒有建構物件,就不能使用這個記憶體區,即使它配置成功了也一樣。(等於是一個懸置集區了)在新的程式庫中construct這個成員函式會帶有一個指標型別的參數、還有一組或有或無(可有可無)的額外參數。這個construct成員函式會在指定的記憶體位置上建構一個元素物件出來(這群動態配置的元素的第一個),額外的參數則是被用來初始化這個元素物件的。(可見這裡建構和初始化是區分開的,但在這裡仍是一塊工作、同時進行,並未切割;但若與「配置」來比的話,則建構與初始化是一國的)就像make_shared成員函式的參數(§12.1.1,頁451)這些額外的參數args必須是有效的初始器,以初始化這個被建構出來的型別物件元素。一定要提供初始器,construct的建構工作才能順利完成。特別是,如果這個物件是一個類別型別(class type)的物件,那麼這些引數就必須和那個類別的建構器(constructor)相配合才行。可見這個allocator的construct成員函式也僅止是假借類別的建構器及其初始器串列(initializer list,建構器初始器串列(constructor initializer list))來完成其物件的建構與初始化的

在早期版本的程式庫中,construct只接受兩個引數:要建構一個物件的指標,以及該元素型別的一個值。因此,我們只能拷貝一個元素到未經建構的空間中,無法使用該元素型別的其他建構器。】6:32:10

要記住的是:企圖使用一個未曾有物件建構的原生記憶體區是決定錯誤的:【沒有物件要怎麼用?所以叫「物件導向」嘛】因為沒有實體物件存在,如何「使用」?這個意思應該是要說,若在一個已經經過配置、但未有任何構建成功的物件存在的記憶體區做運算,那決定是要出錯的。所謂的「原生raw」,乃指未有任何物件建構成功。若有,則至少在該物件所佔用的記憶體上,這個記憶體就不再是原生的了。原生raw就是沒有實體物件存在(即建構成功的物件存在)而僅只是配置出了能放得下該物件的空間而已

cout << *p << endl; //可以:使用了string的輸出運算子(output operator)

cout << *q << endl; //阿災(災難,disaster,但實際在Visual Studio 2019此項並不會出錯!):q指向的是一個未經建構的記憶體區塊(就是q指向了一個不存在的物件,類似或等同於懸置指標)

警告:想要用到由allocate成員函式傳回的記憶體區域,我們就必須先用construct成員函式去建構好相關的物件。若是以其他方式企圖用到未經物件建構的記憶體區,都是毫無意義的行為。

意思是要使用原生記憶體區,一定要該區已有建構好的物件存在才行

因為只經過配置,而尚未實際建構與初始化任何物件,當然不能使用。使用的對象(object)也不會是記憶體本身,而是記憶體上存放的東西(物件object)。7:36:20就如同用一個未經初始化的東西一樣,都是毫無意義的行徑。

當不再用到那些經由construct建構出來的物件時,就必須將它們摧毀。此時就需要調用destroy成員函式來完成這個工作。destroy含有一個指標參數,會在這個指標指向的物件上呼叫對應的解構器(destructor,§12.1.1,頁452)來刪除元素物件以釋放其佔用的記憶體資源:


頁483

7:38:10

while (q != p)

alloc.destroy(--q); //將那些q指向的string物件一一刪除

可見動態配置的物件,必須一個個清除;動態陣列new出來的多個元素物件其元素物件的刪除原理應亦復如此,但實際上,我們卻只要寫如delete []p這樣的語法就可以了,而相信在實際編譯時,還是得用如上的迴圈來一一解構的。

在上述while迴圈的一開頭,q指向的是最後一個被建構出來的元素後的位置(因為q++遞增推移)。我們將它遞減(這裡前綴、後綴版本很關鍵,課本就誤倒了!),再調用destroy成員函式。所以第一次執行destroy函式,q所指向的就是最後一個建構出來的元素物件。(沒建構出來的,不能在其上呼叫destroy,會出錯!)而在最後一次迭代(iteration)時,就會對第一個建構出來的元素,使用destroy函式。7:41:12在這次迭代q指標遞減之後,q就會等於p,判斷式(q!=p)不成立,迴圈就不再執行而結束了。可見p是指向第一元素的指標,q則是因為「q++」遞增推進,才會是指向p後的一個位置,而此例中,只有p位置上一個元素物件被建構(construct)出來,故只有1個元素(size=1)。

警告:只能在建構好的元素物件上,調用destroy成員函式。

destroy成員函式⑧找對主詞 destroy什麼?當然是destroy物件啊!沒有物件如何destroy?!

在我們這個實例中,一旦元素被摧毀,就可以再重新利用它曾佔用過的記憶體位置來存放其他的string物件,或將之歸還給系統(system)。而釋放記憶體的工作則是交由deallocate成員函式來處理的:

alloc.deallocate(p,n);

因為allocator類別就是負責要把配置與建構,解構與釋放的工作分割開來做的

和destroy成員函式一樣,指標引數都不可以是空值(null、nullptr)且指向的必須是由allocate成員函式配置出來的記憶體區塊(記憶體區段)。就像delete也只能刪除new出來的東西(物件)。甚至另一個傳入的引數n,其值大小也須和當初allocate配置給這個指標引數的記憶體時大小相同。

Algorithms to Copy and Fill Uninitialized Memory

前面一區段談記憶體配置,這裡談物件建構與初始化

複製和填充補滿未經初始化的記憶體的演算法

拷貝與填入未初始化的記憶體之演算法

將尚未經過物件建構與初始化的記憶體區,用copy與file演算法來填滿有效的物件

對尚未初始化(建構物件、空無一物)的記憶體,進行拷貝、填滿物件的運算(演算)

未初始化應即前文的raw原生的

Copy and Fill就是建構並初始化

程式庫還定義了2個演算法(algorithm)來配合(as a companion to)allocator類別的操作。這2個演算法可在未初始化(就是原生raw)的記憶體區上建構出物件來。在表12.8中列出的這兩個函式(演算法)都是定義在memory標頭檔中的。

Table 12.8 allocator Algorithms

These functions construct elements in the destination,rather than assigning to them.

這些函式演算法是用來在原生的記憶體區上建構初始化元素而不是給這些元素指定值 7:51:25

uninitialized_copy(b,e,b2) 從一個由b、e兩個迭代器(iterator)指出的輸入範圍中將其元素值拷貝到尚未經任何構建的原生記憶體區,這個記憶區是由b2這個迭代器(iterator,這裡其實應是指標)來指出的,且以b2所指來作為其拷貝開始的位置。這個記憶區必須大到容得下輸入範圍內的所有元素才行。「b」是「begin」,而「b2」可理解為「begin too」或「begin 2」。

uninitialized_copy_n(b,n,b2) 從迭代器b所指示的位置開始,將n個元素拷貝到b2迭代器應是指標所指向的原生記憶體區的位置上。b2指向的是拷貝目的地的開始位置。

copy就是拷貝copy來源的元素到目的地相對應的位置上。fill則是copy拷貝t來將目的地的元素填滿成這個t值。

uninitialized_fill(b,e,t) 在原生記憶體區,由迭代器指標b與e指出的範圍上,將複製(就是正文中提到的拷貝)t來作為建構物件的初始器。(就是拷貝初始化(copy initialize)用拷貝、複製值、指定值的方式來作物件的初始化)fill就是把目的位置上的元素值都用t這個值來填滿。

uninitialized_fill_n(b,n,t) 在迭代器b指向的原生記憶體區,以b為開始位置,將n個物件建構起來。這個記憶體區必須夠大才行。Constructs an unsigned number n objects starting at b. b must denote unconstructed, raw memory large enough to hold the given number of objects.這裡沒提到t這個參數,t應是用來建構n個物件的初始器。

t大概是承前省略了。⑧找對主詞 fill什麼?用什麼fill(填滿)什麼?(用t來填滿目的位置上的元素之值)

上表之b即begin,而b2就是第2個--也就是另一個begin(起始位置)。e即end,結束位置。

7:56:38

在這裡我們會先試著配置出兩倍大容量的記憶體區來納入一個vector中的所有元素,再將這個vector中的所有元素(其型別為int),拷貝到這個配置出來的動態記憶體(dynamic memory)上。我們須先在這個兩倍大的記憶體區的前半部,用拷貝的方式建構出所有vector內的元素,然後在後半部將這些建構出來的元素以它們相對應的值來進行初始化(填滿它們該有的值fill them with a given value):








我們會從原本的vector拷貝元素來建構新配置記憶體的前半部,而後半部的元素則是以一個給定值填入(fill)來建構

這樣做只是為了同時演示copy和fill這兩種演算法是如何運作的,並沒有其他特殊意義!並不是說要用到copy和fill運算法時,一定要配置出一個兩倍大的空間才行!所以這一部分的英文版真的很可惡,很容易誤導初學者,竟然還是最暢銷的入門書?!等而下之,就不敢想像了!

頁484

//配置一個兩倍大的記憶體區來存放vi容器中的元素,為什麼要2倍大?只是這裡是為了演示「copy、fill」這兩種演算法運作的實際情形,並將此2演算法的異同來作比較,並不是將vector拷貝到動態記憶體中都得配置2倍大的區塊才行!copy,是直接拷貝來源;而fill則只是在已配置的記憶體區中建構指定範圍或數量的物件(元素),然後再將其元素值指定為特定的值;即用一個指定數量的指定值來建構、拷貝出新元素。

auto p = alloc.allocate(vi.size() * 2);

//在p指標指向的原生記憶體區的位置上,以該位置為起點,拷貝vi的元素來建構所有的元素物件//(allocate回傳的一個普通指標(plain pointer、ordinary pointer),普通指標是相對於智慧指標(smart pointer)來說的)

auto q = uninitialized_copy(vi.begin(), vi.end(),p);

//將其餘的元素物件初始化為42這個值,也就是把42這個值用拷貝的方式來將未經初始化的元素初始化(這裡元素即指多個物件中的那些物件),因為uninitialized_copy回傳一個指標,指向它拷貝終了的結束、末端位置。

uninitialized_fill_n(q,vi.size(),42);

就像演算法copy(algorithm)(§10.2.2,頁382),uninitialized_copy也是回傳一個在拷貝目的地上已經經過遞增推進1個位置的迭代器。(即尾端後迭代器(off-the-end iterator)。回傳的是一個指標,這裡指標與迭代器又不分了。)所以,uninitialized_copy的運算就會回傳一個指向拷貝目的地最後一個元素後面那個位置的指標。(在此例「後面那個位置」,即是一個已配置給元素使用,而尚未經建構或初始化元素的位置,是後半部的第1個元素。)

此處指標pointer和迭代器(iterator)又不分了,參見前表12.8,迭代器與指標也不分)

我們在此將那個指標用q記下來(存到q裡頭),再將q當作引數傳給uninitialized_fill_n,作為標識填滿值的起始begin(b)位置。這個uninitialized_fill_n函式一如fill_n函式(§10.2.2,頁381。這裡又是「演算法」與「函式」不分了),都帶了3個引數(可見演算法中的fill就意指用一個指定值來作拷貝、複製、填滿的工作):

第1個引數是指向拷貝目的地的指標,指向要填滿元素值的起始位置(b,在此為q)

第2個指出要拷貝的數量(n,在此為vi.size())

第3個引數則是要元素填滿fill的值是什麼(t,在此為42)

uninitialized_fill_n會在要填滿的目的地的記憶體上由q(即Table 12.8 allocator Algorithms中的「b」)所指出的位置開始,以第3個引數值(t)來建構(初始化)指定數量(n)的元素物件。

8:20:13

練習12.26

用allocator類別來重寫481頁上的那個將建構與配置結合的程式。

size_t n; vector<string>vs;

allocator<string>alloc;

//string* const p = new string[n]; //建構n個空的string字串(預設初始化)



string s;

//while (cin >> s && q != p + n) //「cin>>s」:讀取標準輸入到s

while (cin >> s )

vs.push_back(s);

n = vs.size();

//p要在最後delete或destroy作為引數傳入,才能清除動態陣列,故p不能更動,才為const p

string* const p = alloc.allocate(n); //配置n個未初始化的string原生記憶體空間block(區塊、記憶體區塊);const是讓p不會被變動,才不會影響到要插入位置的正確性

// *q++ = s; //將一個新值(=s)指定給q指向的元素

//string* q = p; //q指向了p動態陣列中的第1個字串元素

string*q=uninitialized_copy(vs.cbegin(), vs.cend(), p); //可見並不需要2倍大的容量才能進行copy、fill,課文有誤導之嫌。在表12.8(頁483)也說只要是能容下來源數量的大小便可以了!

//const size_t size = q - p; //記下已經讀取了多少個動態陣列p中的字串元素

const size_t size = q - p;

if (size==n)

{

cout << "size==n" << endl;

}

//……

//對這個動態陣列的使用

q = p;

while (q != p + n)

cout<<*(q++)<<",";//++優先權比*高,加不加()是一樣的

cout << endl;

//delete[] p; //因為普通常值(恆定)指標(內建型別為對string的指標)p指向的是一個動態陣列,所以對它delete必須加上[]空的方括號

alloc.destroy(p);

alloc.deallocate(p,n);//摧毀解構後別忘了釋放(解配置)

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise12_26/prog1/prog1.cpp

留言

熱門文章