C++自修入門實境秀、C++ Primer 5版研讀秀 91 ~12.3.1. Design of the Query Program~練習12...



第91集開始

auto ret = wptr.lock();// 用ret區域變數來記下Weak_ptr這個型別StrBlobPtr資料成員wptr,它叫用它的成員函式lock回傳的結果,這個結果是一個shared_ptr型別的指標;用這個表達式(運算式express)來測度wptr所指向的vector物件是否還存在。wptr是由StrBlob資料成員data來初始化的,也就是與shared_ptr型別的data指向的是同一個物件(非強勢性地共享資源),在此這個物件是vector

if (!ret) //如果傳回來的指標為null值,便等於「0」,條件式就會成立(在C++中true=1(只要是非0的都是true),false=0)

throw std::runtime_error("unbound StrBlobPtr");//要用「runtime_error」要記得「#include <stdexcept>」。「"unbound StrBlobPtr"」表示調用這個check的StrBlobPtr型別物件(它是一個指標型別)並沒有指向任何存在的物件(在這裡是vector裡的元素,而StrBlobPtr本身則是這樣的vector的迭代器)——沒有和任何物件繫結(bind)在一塊;而StrBlob的資料成員、型別為shared_ptr的data,指向的則是那個vector 第91集6:00 對StrBlob而言,其實質是data,所以StrBolb實質是一個動態配置的ector,而對StrBlobPtr來說,其實質則是「i」(也就是strBlobPtr的資料成員curr。為了對wptr指向的vector作下標運算用的索引值),因此StrBlobPtr的實質即是vector的迭代器(iterator)。

if (i >= ret->size())

throw std::out_of_range(msg);

if(i<0)

throw std::out_of_range(msg);

return ret; // 只要通過了以上兩個if條件式的檢驗,就可以放心回傳那個由wptr調用lock成員函式回傳的shared_ptr,而這個shared_ptr和wptr是指向同一個的vector//要用「out_of_range」也要記得「#include <stdexcept>」。「ret」應即「return」的縮寫

}



std::string& StrBlobPtr::deref() const

{

auto p = check(curr, "dereference past end");

return (*p)[curr]; // (*p) is the vector to which this object points

/*真正「推進」元素是在這行,不是在incr(),incr()只是「推進」索引值而已。再在此deref()先檢查

索引值curr有效否,有效才「真的」「推進」(其實是對vector的「下標運算」!)

故反而是在解參考(即deref())時才「推進」元素(實即對vector下標爾),而不是在incr()就推進了

真正有「推進」的,只有索引值,而元素並未被「推進」,只是vector被下標(subscript)而已

第85集 4:5:00 */

}



// prefix: return a reference to the incremented object

StrBlobPtr& StrBlobPtr::incr()

{

// if curr already points past the end of the container, can't increment it

/*若如課本,只寫「=」後面的表達式,那麼編譯器會出現這樣的警告訊息:

Severity Code Description Project File Line Suppression State

Warning C26444 Avoid unnamed objects with custom construction and destruction (es.84). prog1 V:\PROGRAMMING\C++\OSCARSUN72\PROG1\PROG1\STRBLOB.CPP 85

*/

//auto sp = check(curr, "increment past end of StrBlobPtr");

shared_ptr<vector<string>> sp = check(curr, "increment past end of StrBlobPtr");

++curr; // advance the current state// 第91集 33:30這會誤導!應該說為了推進這個vector的迭代器,所以需要遞增對它下標用的索引值。這裡的迭代器就是這個StrBlobPtr型別物件。

return *this;

}



StrBlobPtr& StrBlobPtr::decr()

{

auto p = check(curr-1, "遞減過頭了!");

--curr; //遞減索引值

return *this;

}



bool StrBlobPtr::isEnd()

{

auto p = wptr.lock();

if (p)

{

if (this->curr == p->size())

return true;

}

return false;

}





執行檔、應用程式檔

#include<iostream>

#include"StrBlob.h"



using namespace std;



int main() {

StrBlob stb;

StrBlob stbv{"a","b"};

StrBlob stbv4{"a","b","c","d"};

StrBlobPtr srbp(stbv);

cout << srbp.incr().deref() << endl;

cout << stbv4.begin().deref() << endl;

cout << stbv4.end().decr().deref() << endl;

for (StrBlobPtr i = stbv4.begin(); !i.isEnd(); i.incr())

{

cout << i.deref() ;

}

cout << endl;

}

練習12.20

第91集37:40

改中文版作文:

寫一個程式來讀取一個輸入用的檔案,一次讀取一行到一個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); //最後一個元素會重複,未詳,俟考!(詳下行註解)

//原來要加以「!f.eof()」這個判斷,讀到檔尾時,才不會又多讀一次。第91集40:30

}//可見StrBlobPtr只是模仿了vector迭代器的行為模式,並不是一個「真的」迭代器;或許「真的」迭代器根本就只是這樣,底層用下標(subscript)運算來達成看似遞增(推移)、遞減(倒退)的效果!

StrBlobPtr stbP(stb);

while (!stbP.isEnd())

{

cout << stbP.deref() << endl;

stbP.incr();

}

}



int main() {

string fname = "V:\\Programming\\C++\\input.txt";

readFromFile(fname);

}

練習12.21

我們也可以把StrBlobPtr的deref成員函式寫成這樣:

std::string &deref() const

{ return (*check(curr, "dereference past end"))[curr];}

您哪個比較好,為什麼?

原來(課文正文)的版本比較清楚,且「使得主要程式的長度變短而有助於日後的維護。」

但若顧慮行內函式(inline function),則可考慮以上寫法。

練習12.22

如果想要讓StrBlobPtr這個類別的物件也可以指向常值的(const)StrBlob,那麼它的定義當如何修訂?就請試著定義一個可以用在常值StrBlob上的類別,將之命名作ConstStrBlobPtr。

StrBlobPtr(StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}

改成

StrBlobPtr(const StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}

即可

只要將上式作為第3個建構器即可。

標頭檔更動處如下:

class StrBlobPtr

{

friend class StrBlob;

public:

StrBlobPtr() : curr(0) {}//第1個建構器(也是預設建構器(default constructor)——沒有引數)

StrBlobPtr(StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}//第2個建構器

StrBlobPtr(const StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {}//第3個建構器,針對常值的StrBlob,詳練習12.22

……

};

ConstStrBlobPtr

就請試著定義一個可以用在常值StrBlob上的類別,將之命名作ConstStrBlobPtr。

一個專門給常值的StrBlob用的ConstStrBlobPtr類別

//一個專門給常值的StrBlob用的ConstStrBlobPtr類別

class ConstStrBlobPtr

{

public:

ConstStrBlobPtr()=default;

ConstStrBlobPtr(const StrBlob& cstrb, size_t i = 0) :wptrC(cstrb.data),curr(i){}

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;

};

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

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

第91集53:24 動態陣列其實就是動態配置的多個物件(動態配置物件(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)時解釋為什麼)用程式庫所提供的容器來處理這類的操作,其優勢就更加顯著。而且符合新標準規範的程式庫,其表現也較舊版本傑出得多。



1:0:14

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

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

改中文版作文:

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

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

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)),用以表示這個別名是一個陣列型別。在用這種型別別名來定義一個動態陣列,就可省掉方括號。因為「大小」是陣列型別的一部分,所以在定義型別別名(type alias)的時候,就該包括大小在內,如下:

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

int *p = new arrT; // 配置一個具有42個int元素的陣列,使得 p 指向這個陣列的第一個元素。總之「new」出來的就是動態配置的!1:23:00第90集

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

也就是一個「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配置陣列時,其實是產生一個指向其元素型別的指標

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

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

因為配置出來的物件型別並不是陣列型別,我們就不能對其用上begin或end (§ 3.5.3, p. 118)這樣的函式(不能用begin或end來存取「動態陣列」)。像begin、end這些函式是利用了陣列的大小(元素數,dimension),也就是作為陣列的型別的一部分的那個值來分別回傳指向該陣列第一和末端位置的指標。同樣的,我們也無法對一個(號為)動態陣列的東西,作range for的運算,來對它的各個元素進行操作。(類似下標運算)因為這些操作都必須在該陣列具備「dimension」這種常值的屬性下,才有可能。



正因為「動態陣列」的元素量(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

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

初始化動態陣列

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

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

頁478

1:39: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),猶仍是未經初始化的;而程式庫型別則其預設初始化,就會用它預設的值來做值初始化,故其預設初始化會是有定義的、預設的值。(應該說是某類別只要有預設建構器,並在預設建構器下定義初始器串列(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)

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作解參考後下標,但卻可以遞增、遞減

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

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

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。

2:8:15當我們用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

如何清除動態陣列,釋放動態陣列所佔用的記憶體資源

2:12:55

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

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

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

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

當我們對一個指向動態陣列的指標進行delete的動作時,那對空的方括號是必須的。2:16: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類別物件下定義時,在其指向的元素型別名稱後接上一對空的方括號,來表示要管控的是一個動態陣列。(可見「動態陣列」一定與「空的方括號」有不可分割的關係!)2:29:30


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

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支援的運算和我們在前面§12.1.5(頁470)見到的稍有不同。2:35:22這些不同的運算會在下頁表12.6中列出。當unique_ptr指向的是一個動態陣列時,就不能使用成員存取運算子(不論是「.」點運算子,或「->」箭號運算子 arrow operator:-> operator)來存取其成員了。因為這樣的unique_ptr指向的是一個陣列(同型別元素、或同類資料的集合),並不是單一物件(型別物件,就有類別成員;而陣列並沒有成員member,只有element元素),對它作成員存取是毫無意義的事。然而,當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特有的運算第91集2:49:10

將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出來的多個物件)

在此例中我們將一個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時忘了提供[]就是根本的失誤!(了不異人意,未讀到即知道課本要說什麼)(§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

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

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

沒錄到的看臉書直播第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

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

寫一個從標準輸入讀取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


Algorithms to Copy and Fill Uninitialized Memory

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

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

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

未初始化應即前文的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:9:51

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

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

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

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

頁484

//配置一個兩倍大的記憶體區來存放vi容器中的元素,為什麼要2倍大?大概只是為了演示「copy、fill」這兩種演算法運作的實際,以作比較吧,並不是將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個位置的迭代器。(回傳的是一個指標,這裡指標與迭代器又不分了)所以,uninitialize_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個要填滿元素的值(t,在此為42)

uninitialized_fill_n會在目的的記憶體上由q(b)所指出的位置開始,以那個值(t)的引數值來建構(初始化)指定數量(n)的元素物件。


7:44:17

練習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(區塊、記憶體區塊)

// *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++)<<",";//++優先權比*高,加不加()是一樣的 7:48:50

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

12.3. Using the Library: A Text-Query Program

12.3利用程式庫:做一個字串查詢的程式

作為這一篇對C++程式庫論述的總結,我們會實作一個檢易的字串查詢程式。這個程式可以讓使用者檢索文字檔(就是類似Word Find尋找功能)。檢索的結果會是一個數字及清單。數字表示該字詞在整個檔案中出現的次數;而清單則會陳列出要找的字詞所在該行的內容。如果要找的字在一行內出現多於1次,那行文字也只會列出一次,不會重複。且這個清單會以各行的次序來作條列。

舉例來說,如果我們是從一個包含了本章內容的檔案(英文版這裡又錯,以下內容是前一章(第11章)不是這一章的!),試圖在其中找「element」這個字的話,那麼列出來的結果其前面幾行應該會是像下面這樣:

element occurs 112 times

(line 36) A set element contains only a key;

(line 158) operator creates a new element

(line 160) Regardless of whether the element

(line 168) When we fetch an element from a map, we

(line 214) If the element is not found, find returns

在此後則接著100多行都是element這個字出現的所在行。

後面會接著出現element這個字的剩餘的100多行。

頁485

12.3.1. Design of the Query Program

第91集8:12:29

12.3.1此查詢程式之設計

這個檢索程式的設計

要開始設計一個程式前,最好是先能條列出這個程式會用到的所有操作/運算。知道我們需要什麼樣的運算就能夠明白我們需要怎樣的資料結構(這裡指定義類別物件所需要的資料結構)。以需求取向來考量,就知道我們這個檢索程式必須要能完成如下的工作(達成如下的任務):

 在讀取資料的同時,程式必須記錄下檢索之字出現的那行文字。因此就必須借重一次讀取一行的功能,而且還要在讀取後,有效地將各個字詞從整行中分析出來。

 在輸出檢索結果的時候:

 程式必須能擷取到檢索字詞所在行的行號

 這些行號又必須按照遞增的方式來排序,且又不能重複

 程式要能將行號與其內容都呈現(print)出來

能善用程式庫提供的各種機能,這些需求就能得到很好的滿足

 可以用vector<string>來儲存整個輸入檔案的內容。先把整個檔案讀入,就不會影響原檔案;然後將讀入的檔案內容每行編號,可把每行當作vector的每個元素來儲存,那麼元素的索引值就會是「行號減1」(行號-1。因為沒有0行,而索引值是由0開始的)。要印出檢索的結果行時,就可以用下標的方式提取其行內容(即元素值)

 利用istringstream(§8.3,頁321)來將每行內的字詞切割開來。

 用關聯式容器(associative container)set來儲存來源檔案內容中每個字詞所在的行號。因為用set可以確保我們儲存的值不會重複,而且行號還會依遞增的次序存入set。

 用關聯式容器map來將每個字詞與set中所儲存的該字詞所在的行號作關聯。如關聯式資料庫(資料表)的概念。這樣一來,就可以利用map來提取set的內容:不管檢索的字詞是什麼都可以得到對應的行號。

中文版這裡亂翻!錯得離譜!!

我們會用一個map來將每個字詞關聯到該字詞出現的行號集合(set of line numbers)。使用map能讓我們擷取任何給定字詞的set。

其實上述利用set、map作儲存容器的設計應該就類似資料庫中做索引的概念。

我們的解決方案還會用到shared_ptr,原因稍後我們會說明。

Data Structures 資料結構

有了需求取向的設計概念後,就是對資料結構的設計實作了

雖然我們也可以直接用vector、set、map來寫我們的程式就好,但是如果我們的解決方案(solution)能夠定義一個更為抽象化的資料結構,那將會更加實用。意思就是說,不定義類別也是可以直接在main或函式內用上vector等來實作。可是如果能設計一個抽象類別,日後的彈性更大、應用更廣。因此,我們可以從定義一個類別來著手(類別即抽象資料結構或抽象資料型別(abstract data type),詳前),用這個類別來存放來源資料檔(input file)以便在檢索字串時使用。這個類別,可命名為TextQuery,它會儲存一個vector和一個map。vector會存放來源檔的內容,map則會負責將來源檔中的每個字詞和set中所存放的那個字詞(that word)所在的行號關聯起來。8:30:40這個TextQuery類別會有一個建構器(constructor)讀取來源檔,以構建它的這些資料成員,還有一個執行檢索的運算(函式)。

這個檢索運算/操作的內容是很簡單的,原理就是負責檢查要檢索的字詞是否在map中。設計這個檢索函式(運算,function函式,這裡的運算、操作operation和函式是等義的。)最困難的部分就在於它是要回傳什麼東西。因為一旦在map中找到檢索的字詞了,還必須知道這個字詞在來源檔中出現了幾次,還有它所在的行號、以及這些行號所在行的內容。

要將上述所需的資訊(data)一次性地回傳,最簡便的方式就是再定義一個類別用來儲存這些打算回傳來的資訊;我們會將之命名為QueryResult。它則負責存放檢索的結果。它還會有一個叫做print的成員函式,負責印出一個這個QueryResult型別物件所儲存的檢索結果是什麼。

頁486

Sharing Data between Classes 在類別間如何共用資料

QueryResult類別是用來儲存以表示一次檢索的結果(is intended to represent the results of a query)。這些檢索結果資訊包括了set中儲存或放置的檢索字詞所在行號、以及其行的文字內容。而這些會用到的資料卻是儲存在一個TextQuery型別的物件中。

因為QueryResult所需的資料是儲存在TextQuery型別物件中,就必須留意要怎麼由QueryResult來存取這些放在TextQuery中的資料。當然我們可以直接把TextQuery中set的行號資料拷貝(copy)過來用,但這樣一來,恐怕得付出可觀的運算代價/成本(an expensive operation)。尤有甚者,我們絕對不會想要去拷貝(copy)vector的內容來用,因為就只為了印出(可能通常僅只是)來源檔中一小部分的資料,卻得動用到整個來源檔。

因為函式回傳的方式預設為「拷貝」回傳值,故有此疑慮也,所以才有下面的解決方案:


我們可以藉由回傳迭代器(iterator)或指標的方式,來避免拷貝整個容器或物件的內容,以存到TextQuery的物件之中。然而這樣的方式恐怕還有風險(opens up a pitfall,問題浮上檯面),8:47:55這風險就是:萬一QueryResult相關的(corresponding)TextQuery物件在它結束前就被摧毀了,那麼當這個QueryResult再試圖去存取它所需要的資料,又該怎麼辦?

想要讓QueryResult想要存取的TextQuery物件生命夠長,這一需求就透露出了解決我們問題的一線曙光。

This last observation about synchronizing the lifetime of a QueryResult with the TextQuery object whose results it represents suggests a solution to our design problem.

我們需要同步TextQuery物件以及代表其結果的QueryResult的生命週期,最後的這個觀察暗示了這種設計問題的一個解法。

既然想要(conceptually)讓TextQuery和QueryResult這兩個類別「共用」資料,那麼我們就可以在我們的資料結構中加入shared_ptr(§12.1.1頁,第450頁)來實現這樣的資料共用。因為只要shared_ptr的參考計數(reference count)不是0,它所管理物件就不會被摧毀,其生命長度,就得到延續,不會受它者影響。○在此可能也得加入weak_ptr來檢查要用的資料物件還存在否。



Given that these two classes conceptually Given that these two classes conceptually “share” data, we’ll use shared_ptrs (§ 12.1.1, p. 450) to reflect that sharing in our data structures.

Using the TextQuery Class

TextQuery類別要如何運用

在設計一個類別(design a class)時,如果能在實際實作其類別中的成員之前,先寫一些用到這個類別的程式(program)來測試或表示的話,將會是很有幫助的。(這裡程式與函式是等義的)

設計一個類別時,在實際實作其成員前,先寫程式使用該類別,可能會有幫助。

所以不要急著實作類別內容,而是用程式來測試它,做中學,也比較具體,方便掌握所需的功能,具體是什麼。

這樣的話,我們就可以了解到要設計的類別,是否有我們所需的功能(operations)。比如像下面這個程式就用到了我們先前提出的TextQuery和QueryResult這兩個類別。(此時程式programs與函式function又等義了)這個函式(function)帶了一個ifstream的參數,來接受準備處理的檔案內容。這個函式能和輸入者(user)互動,由使用者輸入的字詞來決定印出的結果會是什麼:

void runQueries(ifstream & infile)

{//引數infile是一個檔案資料流(stream ifstream)代表一個準備作為檢索對象的檔案

TextQuery tq(infile); //讀入檔案並建置(build)檢索用的map

// iterate with the user: 提示使用者輸入檢索字詞來進行檢索並印出其檢索結果

while (true)

{

cout << "enter word to look for, or q to quit:";

string s;

//如果讀取使用者輸入的字詞失敗(hit end-of-file on the input),或者是使用者輸入了「q」,就中止

if (!(cin >> s) || s == "q")

break;

執行檢索並印出結果

print(cout, tq.query(s)) << endl; //可見query需要傳回的,應該就是一個QueryResult了

}

}

這個程式由建置一個TextQuery的型別物件tq開始,它是由一個檔案資料流ifstream infile來傳入給TextQuery建構器來建構的。這個建構器會將該檔案讀入到它的資料成員(data member)vector中,並且建置一個map以儲存來源檔中的字詞及其所在的行號。

而while迴圈會一直要求使用者輸入檢索字詞以供檢索,並列印出檢索的結果。因為while的條件式中放的是「ture」這個字面值(literal,§2.1.3,頁41),因此除非讀取失敗、或使用者輸入了「q」,否則迴圈就不會終止。離開迴圈的方式是藉由break這個述句(§5.5.1,頁190)來執行的,它在if條件式為真ture後,就會被執行,迴圈就會結束。

頁487

只要使用者輸入的不是「q」,就會讓tq去檢索使用者輸入的字詞,並以print成員函式來列印出檢索的結果。

練習12.27

TextQuery和QueryResult類別會用到的功能並不會超出我們已經討論過的範圍。在繼續閱讀本書前,針對這兩個類別,先試著依照我們談過的功能寫出你自己版本的TextQuery和QueryResult。

第91集 9:13:50

觀念題! escape character 破格字元(破例字元、翻義字元、轉義字元、反義字元……):「\」:意謂將C++語言定義符號,還原(翻轉)為原來的定義(即人類或作業系統用的意義)「\」反斜線後面加一個字元,二者組合成的就叫反義序列sequence

感恩鄭宇翔老師啟發 https://youtu.be/WCpHAWsnS-4

練習12.28



練習12.29



12.3.2. Defining the Query Program Classes


頁487

練習12.28

第91集 9:42:00

寫一個沒有定義類別來處理資料的文字查詢程式。這個程式應該要帶有一個檔案的參數,並能與使用者互動來對該檔案的文字進行查詢。利用vector、map和set這些容器來存放該檔案的內容且產出查詢的結果。

寫一個程式來實作文字查詢,但不定義類別來管理資料。你的程式應該接受一個檔案,作為引數,並能與使用者互動,以查詢那個檔案中的字詞。使用vector、map與set容器來存放該檔案的資料,以及產生查詢的結果。

練習12.29

10:8:00

我們也可能用do while迴圈來作為與使用者互動的機制。就請用do while來重寫那個迴圈。說說看您比較喜歡哪種版本,為什麼?

我們其實可以把用來管理使用者互動的迴圏寫成一個do while (§5.4.4,頁189)。改寫那個迴圈,使用do while。請解釋你偏好哪個版本以及原因。



留言

熱門文章