C++自修入門實境秀、C++ Primer 5版研讀秀 86/~ 12.2 Dynamic Arrays 動態陣列(其實是指向元素型別的指標)-...
頁473
指標有效性是經過檢查的指標類別(具備檢查check機制的指標類別)
經過檢查的指標類別
第86集開始
什麼叫做「指標類別」也不過就是它的成員函式支援/提供了如同指標一般的運算,如StrBlobPtr中的incr、deref二個成員函式就是模擬/模仿內建型別(builtin type)指標的「++、*」運算子的功能,或者說,利用了「++、*」運算子的功能來達到所謂的指標類別該提供的諸如遞增(推進)和解參考的操作/運算。只要有樣的成員函式,大概就可以視之為指標類別了。4:42:30
所以類別叫/是「抽象資料結構」只要其資料(此資料是指類別內的內容、內容物、所含物)結構符合某些特性或具備哪些功能的,就屬哪一種類別了。
頁476
練習12.20
改中文版作文:
寫一個程式來讀取輸入的檔案,一次讀一行到一個StrBlob中,並利用StrBlobPtr來印出那個StrBlob中的每個元素。
參考練習8.4
標頭檔與源碼檔與12.19同這裡只錄執行檔:
#include<iostream>
#include"StrBlob.h"
#include<fstream>
//#include<iostream>
using namespace std;
void readFromFile(const string& fFullName) {
ifstream f(fFullName);
string str;
StrBlob stb;
while (f&&!f.eof())//f用來判斷選取成功否;.eof()來判斷到檔案尾否(即使選取成功)
{
getline(f, str);
stb.push_back(str); //最後一個元素會重複,未詳,俟考!11:29:20
//原來要加以「!f.eof()」這個判斷,讀到檔尾時,才不會又多讀一次。C++自修入門實境秀、C++ Primer 5版研讀秀第86集先講這個!第86集5:40 18:00
}
StrBlobPtr stbP(stb);
while (!stbP.isEnd())
{
cout << stbP.deref() << endl;
stbP.incr();
}
}
int main() {//第85集 10:55:30 沒錄到的部分見臉書直播第432集
string fname = "V:\\Programming\\C++\\input.txt";
readFromFile(fname);
}
頁476
練習12.21
我們也可以把StrBlobPtr的deref成員函式寫成這樣:
std::string &deref() const
{ return (*check(curr, "dereference past end"))[curr];}
您哪個比較好,為什麼?
原來(課文正文)的版本比較清楚,且「使得主要程式的長度變短而有助於日後的維護。」第86集26:20
但若顧慮行內函式(inline function),則可考慮如此寫法:
練習12.22
ConstStrBlobPtr
就請試著定義一個可以用在常值StrBlob上的類別,將之命名作ConstStrBlobPtr。
第86集3:20
一個專門給常值的StrBlob用的ConstStrBlobPtr類別
1:55:10 2:0:00
//一個專門給常值的StrBlob用的ConstStrBlobPtr類別
class ConstStrBlobPtr
{
public:
ConstStrBlobPtr()=default;
ConstStrBlobPtr(const StrBlob& cstrb, size_t i = 0) :wptrC(cstrb.data),curr(i){}
//~ConstStrBlobPtr();//要自定義解構器(destructor)才需要這個,否則應是會和編譯器預設的解構器相衝突,造成編譯錯誤
ConstStrBlobPtr incr();
ConstStrBlobPtr decr();
string& deref()const;
bool isEnd()const;
bool isBegin()const;
private:
shared_ptr<vector<string>> check(size_t , const string&)const;
weak_ptr<vector<string>>wptrC;
size_t curr;
};
//ConstStrBlobPtr::~Con stStrBlobPtr()
//{
//}
cpp檔的增訂
inline shared_ptr<vector<string>> ConstStrBlobPtr::check(size_t i, const string& msg)const
{
shared_ptr<vector<string>>p = wptrC.lock();
if (!p)
throw runtime_error("unbound ConstStrBlobPtr");
if (i >= p->size())
throw out_of_range(msg);
return p;
}
inline ConstStrBlobPtr ConstStrBlobPtr::incr()
{
auto p = check(curr, "increment past end of StrBlob");
++curr;
return *this;
}
ConstStrBlobPtr ConstStrBlobPtr::decr()
{
auto p = check(curr, "");//這個主要是用來檢查指標是否有效
--curr;
return *this;
}
string& ConstStrBlobPtr::deref()const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
bool ConstStrBlobPtr::isEnd()const
{
auto p = wptrC.lock();
if (p) {
if (curr == p->size())
return true;
return false;
}
}
bool ConstStrBlobPtr::isBegin()const
{
//int i;
//i = curr;//因為curr型別為size_t,是unsigned,不會是負的
if (curr == -1)//已超出unsigned範圍,但此條件式卻可成立!可能對unsigned來說,超出範圍的值就是-1。
return true;
return false;
}
12.2. Dynamic Arrays
動態陣列(其實是指標,指向元素型別)
6:27:10故亦可名之為「由new配置出來的N個元素所組成的記憶體區塊」(參見後文)。
艥 2:33:00 字形結構兼音義 字形結構換部首且字形結構換聲符之例 ③字形結構兼音義
2:36:10 增加程式碼標色的程式功臣:字型指定為verdana字型。
2:8:20
重譯中文版:
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的話,常會有更好的效能表現,對記憶體的管控也能更加靈活。
2:47:55 3:5:40
許多,甚至可說是在大多數的情況下,並沒有直接需要用到動態陣列之處。當必須用上(application)不定量的物件時(即不確定要用到多少物件的時候),像我們先前利用vector這類的程式庫容器來配置Strblob這樣的做法,就可以說是最容易、也最快且最妥善(safer)的應用(application)方式。尤其在新標準下(我們會在§13.6(頁531)時解釋為什麼)用程式庫所提供的容器來處理這類的操作,其優勢就更加顯著。而且符合新標準規範的程式庫,其表現也較舊版本傑出得多。3:8:33
3:16:20 5:49:00
就如我們所見,那些利用程式庫容器的類別,都能直接將這些容器定義(default; use the default versions of the operations for)的運算方式,拿來利用,諸如對其物件的拷貝、指定與解構(§7.1.5,頁267)等,無所不可。而若是用到動態陣列的類別,就必須對這類的操作先定義好,才能在拷貝、指定與解構其類別物件時有效地管控所配置的記憶體。3:26:20
警告:在學會13章的技能前,千萬不要在你的類別中,用到動態陣列。
改中文版作文:
12.2動態陣列(其實是指向元素型別的指標)
new和delete運算子只能一次配置一個物件,有些時候會需要一次為許多物件同時在記憶體內配置儲存區。舉例來說,vector和string會將它們的元素儲存在連續的記憶體位址中,並且必須在每次容器需要重新配置(§9.4)時,一次性地配置好多個元素。
為了要滿足這種需求,所以C++語言和其程式庫就提供了兩種方式來一次性地配置陣列物件(因為陣列一定是由一至多個元素組成的集合/群集。多個同質(型別)元素的組合或集合、成為一個整體,就叫「陣列」)。第一種方式,是C++定義了第二種new的表達式來配置和初始化一個陣列物件。第二種方式則是程式庫納入了一個模板類別,名為allocator,它讓我們可以將配置與初始化物件的工作分開來執行。因為在§ 12.2.2(頁481)中會提到的因素,我們若是能利用一個 allocator來配置和初始化物件的話,通常會得到更佳的效能表現且更具彈性的記憶體管理。
在實務上的應用,甚至可以說是絕大多數的實務應用,動態陣列(dynamic arrays)是很罕用到的。當需要用到數目不定的物件時,若能以我們處理過StrBlob那樣的方式來進行物件的配置與管控,可以說是更為容易、且更快速、更安全的方式;也就是盡量利用一個程式庫容器,諸如vector,來管控我們所需用到的物件。因為§3.6中解釋的原因,在新標準下若能利用程式庫容器,其好處決定是更加明顯。尤其是符合新標準規範的程式庫,在執行效能的表現上通常會比之前的版本亮眼許多。
養成好習慣:應該盡量使用程式庫容器,而非動態配置的陣列,來管理眾多物件(元素)。使用容器不但相較於動態陣列容易得多,且也比較不會產生記憶體管理上的錯誤,甚至非常有(and)可能得到更好的效能。 5:37:10
如我們見過的,使用容器類別,可以使用它既定的拷貝、指定及解構(§7.1.5,頁267)運算。然而,配置動態陣列的類別就必須定義它們自己版本的這些運算,以在物件被拷貝、指定或摧毀時,管理關聯的記憶體。
警告:在閲讀第13章前,你的程式碼不要在類別中配置動態陣列。
5:10:00
Word VBA將中文間多餘的半形空格清除
頁477
3:40:20
12.2.1. new and Arrays
new運算子和陣列
怎麼用new來配置動態陣列
在用new運算子來配置物件陣列時,我們可以在一個型別名稱後面的一對方括號(方括弧、中括號、中括弧)中指定要配置物件的數量,用這樣的表達式來配置物件陣列。這樣做後,new就會依所指定的數量來配置,只要配置成功,就會回傳一個指向該陣列第一個元素的指標。
// 調用get_size函式來決定有多少ints要作配置
int *pia = new int[get_size()]; // pia 是一個指向第一個int的指標
所以動態陣列的差別就在:
方括號中的值只要是整數型的型別即可,但不必是常值。(一般陣列就須是常值,且其值為其型別的一部分)
也可以用型別別名(type alias)來定義一個陣列(allocate an array by using a type alias (§ 2.5.1, p. 67)),用以表示這個別名是一個陣列型別。在用這種型別別名來定義一個動態陣列,就可省掉方括號
typedef int arrT[42]; // arrT 就表示是一個含有42個int元素的陣列型別
int *p = new arrT; // 配置一個具有42個int元素的陣列,使得 p 指向這個陣列的第一個元素
也就是一個「arrT」即代表了「int[42]」
像這樣,new就會配置一個元素為int的陣列,且會回傳一個指向該陣列第一元素的指標。即使在利用型別別名(type alias)來定義動態陣列時可以省略掉方括號,但編譯器在編譯這個表達式時仍是用「new[]」來完成的。也就是說,編譯器其實是用下列的方式來編譯這段程式碼(expression)的:
int* p=new int[42];
Allocating an Array Yields a Pointer to the Element Type 當用new配置陣列時,其實是產生一個指向其元素型別的指標
4:11:10雖然一般都將用new T[](T表元素型別)這樣的表達式配置出來的物件叫做動態陣列,但這種叫法不盡正確,甚至誤導了我們。當我們用一個new去配置一個陣列時,我們並不會得到一個陣列型別的物件,而是得到一個指標型別的物件,這個指標是指向該陣列元素型別的指標。即使我們企圖用型別別名(type alias)去定義出一個陣列型別,也是枉然。因為new並不會真的去配置一個型別為陣列的東西(object)出來,在這種情況下,我們配置出來的其實是一個不存在的陣列,也就是說,並沒有一個具備陣列雛形的[num]這樣的實體存在,new只會回傳一個指向元素型別的指標,而不是陣列。
因為配置出來的物件型別並不是陣列型別,我們就不能對其用上begin或end (§ 3.5.3, p. 118)這樣的函式(不能用begin或end來存取「動態陣列」)。像begin、end這些函式是利用了陣列的大小(元素數,dimension),也就是作為陣列的型別的一部分的那個值來分別回傳指向該陣列第一和末端位置的指標。同樣的,我們也無法對一個(號為)動態陣列的東西,作rnage for的運算,來對它的各個元素進行操作。因為這些操作都必須在該陣列具備「dimension」這種常值的屬性下,才有可能。4:29:10
正因為「動態陣列」的元素量(dimension)是不定的(非常值),所以就不能對其進行begin、end、for諸如此類一般陣列適用的運算。
It is important to remember that what we call a dynamic array does not have an array type.
4:33:59 4:42:00
we=「我們……所」;或「所……的」=what
Initializing an Array of Dynamically Allocated Objects
初始化由動態配置的物件所組成的一個陣列
初始化動態陣列
前面講配置,這裡要講初始化了
通常,由new動態配置出來的物件,不管是單一物件或一個陣列,都會用預設初始化的方式來初始化該物件。我們也可以用值初始化(value initialize)的方式來初始化陣列的元素值,就是在該陣列的大小標記位置(即方括號)後面再接著指定一對空的小括號(圓括號)。
頁478
5:57:50
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),猶仍是未經初始化的;而程式庫型別則其預設初始化,就會用它預設的值來做值初始化,故其預設初始化會是有定義的、預設的值。
string *psa2 = new string[10](); // block of ten empty strings也是由10個空字串組成的記憶體區塊;就因為程式庫型別(尤其這裡的string型別),其預設初始化是利用了預設值來做值的初始化,故其預設初始化的結果與其值初始化的結果是一樣的
6:10:45
在新標準發布後,我們也可以用一個由大括號圍起來的元素初始器串列來對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)
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)
/* process the array */ ;處理這個陣列
在這種情況下,即使get_size回傳的是「0」,也會照常執行。因為雖然我們不可能創建出一個大小為0的陣列,但用「0」這個值來作為new運算的引數卻是可以被接受的。
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。
7:15:06 7:36:07
當我們用new來配置一個不含任何元素的陣列時,new就會回傳一個無效且非0的指標(即非null指標,不是nullptr。這種指標其實就是懸置指標(dangling pointer),懸置指標就是不指向任何存在物件的指標。指標的無效是說它所指的物件是無效=不存在的)。這個指標決定不會與任何new成功配置後所回傳的指標混淆。對一個沒有元素的陣列而言,這個無效的懸置指標作用就如同它的尾端後指標指針(off-the-end pointer,§3.5.3,頁119)一樣。我們是怎麼使用尾端後指標的,就可以那麼用這個指標。因此,這個指標就可以放在迴圈的開頭作為一個比較判斷式的一部分。可以對它加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不相等時,才會執行)
7:43:10
頁479
Freeing Dynamic Arrays
如何清除動態陣列,釋放動態陣列所佔用的記憶體資源
8:28:10
有特殊形式的new,就有別致的delete。要清除動態配置的陣列,就必須用這種形式的delete表述式,這種別致型的delete表述式(expression)就是包含了一對空方括號的delete述句。
delete p;//p必須是空指標,或是一個指向動態配置物件的指標
delete []pa;//a=array。pa(=pointer array)必須是一個指向動態陣列的指標或是一個空指標。這個表述式的意思應該就是:「用pa來delete,以清除[]」。一般的陣列(內建型別的陣列)須指定陣列大小,作為其陣列型別的一部分來宣告,而動態陣列,就是保留那指定大小的部分(即方括號內),把它空下來、保持空白,表示任何可接受的值(包括0,參見前)都是可以代入的。
空的方括號其實就是動態陣列(不定數目物件)的代表記號。上述第二個表述式(述句,statement)會摧毀所有在pa指向的陣列中的元素,然後釋放掉它們佔用的記憶體資源。而這些陣列中的元素在被刪除時是倒著(倒序,反向的順序)來抹除的;意思就是陣列中最後一個元素會最先被刪除,然後是倒數第二個……,如此類推。
當我們對一個指向動態陣列的指標進行delete的動作時,那對空的方括號是必須的。8:38:26因為它會告訴編譯器這些元素中第一個的指標值(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決定不是一個指向單一arrT型別物件的指標。因此,我們仍然必須寫出[]空的方括號,才不會讓編譯器誤以為我們要delete的是一個指向單一物件的指標。
警告:編譯器通常在我們對動態陣列進行delete時忘了附上空的方括號——或者在我們對一個指向單一物件的指標進行delete時,卻錯加上了[]一對空的方括號——並不會提出警告。編譯器未必會發現這樣的錯誤。因此在編譯完成後、實際執行程式碼時,這樣的程式,往往就會在沒有警訊下,頻頻出錯(is apt to misbehave)。
Smart Pointers and Dynamic Arrays 智慧指標與動態陣列的關係
C++程式庫還定義了一種unique_ptr類別,它可以控管new出來的動態陣列。要用這樣的unique_ptr來管控動態陣列,我們就必須在對這種unique_ptr類別物件下定義時,在其指向的元素型別名稱後接上一對空的方括號,來表示要管控的是一個動態陣列。(可見「動態陣列」一定與「空的方括號」有不可分割的關係!)10:1:00
//up:u=unique、p=pointer;up指向一個由new配置出來的10個未經初始化的int所組成的陣列
unique_ptr<int[]>up(new int[10]);//前面學的非陣列(單一物件)是用()圓括號,不是用方括號[]。因此,用方括號即表是多個物件(即陣列),而圓括弧來與new搭配就是指僅只配置出單一一個動態物件。
up.reset();//reste會在up這樣的unique_ptr上調用delete[]運算以刪除它底層指向10個未初始化int構成的陣列的指標。
在型別指定式(type specifier:<int[]>這個意思就是有好多個、不只一個int)中所寫的空的方括號就指出up是一個指向由多個int所組成的陣列,而不是單一一個int的物件。因為up指向的是一個動態陣列,所以當up摧毀它底層的指標時,就會自然調用delete[]這樣的運算——這是unique_ptr類別才有的定義,shared_ptr類別是沒有的(詳下)。
指向動態陣列的unique_ptr(英文版拼錯成unqiue_ptr)支援的運算和我們在前面§12.1.5(頁470)見到的稍有不同。10:10:55這些不同的運算會列在下頁表12.6中。當unique_ptr指向的是一個動態陣列時,就不能使用成員存取運算子(不論是「.」點運算子,或「->」箭號運算子 arrow operator:-> operator)來存取其成員了。因為這樣的unique_ptr(英文版又拼錯成unqiue_ptr)指向的是一個陣列,並不是單一物件,對它作成員存取是毫無意義的事。然而,正因為它是一個指向陣列的指標,而此指標又適正指向一個元素型別的指標(即動態陣列),我們卻可以對它指向動態陣列的unique_ptr作下標運算來存取它所指陣列內的元素。
for(size_t i=0;i!=10;++i)
up[i]=i;//因為動態陣列不是真的陣列,只是一個指向元素型別的指標,由此可以直接用up這樣的智慧指標來作下標的動作。將up所指陣列的每個元素,指定為i此變數之值。
頁480
Table 12.6. unique_ptrs to Arrays 表12.6 指向動態陣列的unique_ptr特有的運算
除了成員存取運算子不能用外,其他的unique_ptr運算依然有效
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<int> sp(new int[10],[](int *p){delete[] p});//「[](int *p){delete[] p}」是可呼叫物件(callable object)lambda;最前端的「[]」是lambda的捕捉串列(capture list)(頁388),而「delete[]」的「[]」才是指動態陣列。
sp.reset();//用我們在建構sp時所指定的lambda來調用delete[]以清除sp所指向的動態陣列
在此例中我們將一個lambda(§10.3.2,頁388)當作刪除器(deleter)引數,傳給shared_ptr的建構器來建構一個指向動態陣列的shared_ptr。而這個lambda是用delete[]來清除這個shared_ptr所指向的動態陣列(其實就是多個物件)。
如果在定義shared_ptr時忘了像這裡這樣提供lambda這類的刪除器(deleter),寫出來的程式碼就會是毫無意義的了。因為在沒有提供刪除器(deleter)時,shared_ptr就會直接調用delete運算子(而不是delete[])來刪除它所指向的單一物件。可是現在shard_ptr指向的並不是一個單一物件,而是多個物件(就是多個「元素」)組成的陣列(記憶體區塊block),要刪除這樣的陣列物件,若在調用delete時忘了提供[]就是根本的失誤!(了不異人意,未讀到即知道課本要說什麼)(§12.1.1,頁479)10;31:40 9:42:30
shared_ptr不能直接對動態陣列操作的根本原因就在於它沒有提供對這樣動態陣列適用的刪除器(deleter),它的這種特性也改變了存取其所指陣列元素的方式:
//shared_ptr類別並沒有提供下標運算子([]運算子,subscript operator),且也不支援指標算術(pointer arithmetic)
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的值指定給這個物件元素。
9:50:00 10:36:40
而shared_ptr類別並未定義下標運算子(unique_ptr就有,見前頁479末行),且智慧指標也不支援一般指標的指標算術(pointer arithmetic,§3.5.3,頁119)。所以若要進行指標的推移,以便存取陣列中的元素,就必須將智慧指標轉回普通指標才行。因此,get成員函式在此就扮演了不可或缺的角色。
9:58:10:38:30
練習12.23
寫一個程式來串接兩個字串字面值,將結果放到一個動態配置的char陣列中。再寫一個程式來串接兩個程式庫string,它們的值跟第一個程式中使用的字面值一樣。
寫一個程式來將二個字串字面值(string literal)串接起來,將結果放到一個經由動態配置的char陣列中。再寫一個串接兩個程式庫型別string字串的程式,將兩個與前一程式有相同值的string字串串接在一塊。
10:39:20沒錄到的看臉書直播第437集
#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
10:48:20
寫一個程式從標準輸入讀取一個字串到一個動態配置的字元陣列中。描述你的程式如何處理長短不定的輸入。給一個超過你程式所配置的陣列大小的長字串,來測試它。
寫一個從標準輸入讀取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
11:22:30
如下列的new宣告/定義(expression),您會如何對pa進行delete運算?
int* pa = new int[10];
delete[] pa;
留言