C++自修入門實境秀、C++ Primer 5版研讀秀 88/ ~12.3.1. Design of the Query Program~練習1...
1:00 擬作一個刪除時間軸記號的程式
12.3.1. Design of the Query Program
第88集開始 2:22 24:55
可以用vector<string>來儲存整個輸入檔案的內容。先把整個檔案讀入,就不會影響原檔案;然後將讀入的檔案內容每行編號,可把每行當作vector的每個元素來儲存,那麼元素的索引值就會是「行號-1」(因為沒有0行,而索引值是由0開始的)。要印出檢索的結果行時,就可以用下標的方式提取其行內容(即元素值)
利用istringstream(§8.3,頁321)來將每行內的字詞切割開來。
用關聯式容器(associative container)set來儲存來源檔案內容中每個字詞所在的行號。因為用set可以確保我們儲存的值不會重複,而且行號還會依遞增的次序存入set。
用關聯式容器map來將每個字詞與set中所儲存的該字詞所在的行號作關聯。如關聯式資料庫(資料表)的概念。這樣一來,就可以利用map來提取set的內容:不管檢索的字詞是什麼都可以得到對應的行號。
中文版這裡亂翻!錯得離譜!!
我們會用一個map來將每個字詞關聯到該字詞出現的行號集合(set of line numbers)。使用map能讓我們擷取任何給定字詞的set。20:00 32:10
其實上述利用set、map作儲存容器的設計應該就類似資料庫中做索引的概念。
我們的解決方案還會用到shared_ptr,原因稍後我們會說明。
Data Structures 資料結構
有了需求取向的設計概念後,就是對資料結構的設計實作了
2:29:11雖然我們也可以直接用vector、set、map來寫我們的程式就好,但是如果我們的解決方案(solution)能夠定義一個更為抽象化的資料結構,那將會更加實用。38:55 意思就是說,不定義類別也是可以直接在main或函式內用上vector等來實作。可是如果能設計一個抽象類別,日後的彈性更大、應用更廣。因此,我們可以從定義一個類別來著手(類別即抽象資料結構或抽象資料型別(abstract data type),詳前),以方便使用來源檔(input file)的方式來存放這個檔案。這個類別,可命名為TextQuery,它會儲存一個vector和一個map。vector會存放來源檔的內容,map則會負責將來源檔中的每個字詞和set中所存放的那個字詞(that word)所在的行號關聯起來。這個TextQuery類別會有一個建構器(constructor)用來讀取來源檔,還有一個執行檢索的運算(函式)。
這個檢索運算/操作是很簡易的,原理就是負責檢查要檢索的字詞是否在map中。設計這個檢索函式(運算,function函式,這裡的運算、操作operation和函式是等義的。)最困難的部分就在於它是要回傳什麼東西。因為一旦在map中找到檢索的字詞了,還必須知道這個字詞在來源檔中出現了幾次,還有它所在的行號、以及這些行號所在行的內容。
要將上述所需的資訊(data)一次性地回傳,最簡便的方式就是再定義一個類別;我們會將之命名為QueryResult。它則負責存放檢索的結果。它會有一個叫做print的成員函式,負責印出一個QueryResult型別物件所儲存的檢索結果是什麼。2:37:52
頁486
Sharing Data between Classes 在類別間共用資料
1:5:59
QueryResult類別是用來表示一次檢索的結果。這些檢索結果包含了set中回傳的檢索字詞的所在行號、以及其行的文字內容。而這些要用到的資料卻是儲存在一個TextQuery型別的物件中。
因為QueryResult所需的資料是儲存在TextQuery型別物件中,就必須留意要怎麼由QueryResult來存取這些存在TextQuery中的資料。當然我們可以直接把set中的行號資料拷貝(copy)出來用,但這樣執行的話,恐怕是沒有什麼甜頭可嚐的(an expensive operation)。甚至,我們絕對不會想要去拷貝(copy)vector的內容來用,因為就只為了印出(可能通常僅只是)來源檔中一部分的小資料,卻動用了整個來源檔。
因為函式回傳的方式預設為「拷貝」回傳值,故有此疑慮也,所以才有下面的解決方案:
我們可以藉由回傳迭代器(iterator)或指標的方式,來避免拷貝整個容器或物件的內容,以存到TextQuery的物件之中。然而這樣的方式恐怕還有風險(opens up a pitfall,問題浮上檯面),2:46:00這風險就是:萬一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的生命週期,最後的這個觀察暗示了這種設計問題的一個解法。
既然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類別要如何運用
1:47:20 3:0:33
在設計一個類別(design a class)時,如果能在實際實作其類別中的成員之前,先寫一些用到這個類別的程式(program)來測試或表示的話,將會是很有幫助的。(這裡程式與函式是等義的)
設計一個類別時,在實際實作其成員前,先寫程式使用該類別,可能會有幫助。
所以不要急著實作類別內容,而是用程式來測試它,做中學,也比較具體,方便掌握所需的功能,具體是什麼。
這樣的話,我們就可以了解到要設計的類別,是否有我們所需的功能(operations)。比如像下面這個程式就用到了我們先前提出的TextQuery和QueryResult這兩個類別。(此時程式programs與函式function又等義了)這個函式(function)帶了一個ifstrem的參數,來接受準備處理的檔案內容。這個函式能和輸入者(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;
}
}
2:9:50 3:10:11
這個程式由建置一個TextQuery的型別物件tq開始,它是由一個檔案資料流ifstream infile來傳入給TextQuery建構器來建構的。這個建構器會將該檔案讀入到它的資料成員(data member)vector中,並且建置一個map以儲存來源檔中的字詞及其所在的行號。
這個while迴圈會一直要求使用者輸入一個檢索字詞,以供檢索,並列印出檢索結果。因為while的條件式是一個字面值(literal,§2.1.3,頁41)「ture」,因此除非讀取失敗、或使用者按下「q」,否則迴圈永遠不會終止。離開迴圈的方式是藉由break這個述句(§5.5.1,頁190),它在if條件式成立後,就會被執行,迴圈就會結束。
頁487
只要使用者輸入的不是「q」,就會讓tq去檢索使用者輸入的字詞,並以print成員函式來列印出檢索的結果。
練習12.27
3:17:25
TextQuery和QueryResult類別將會用到的功能並不會超出我們經涵蓋談過的範圍。先別往前看在繼續閱讀前,針對這些類別,先試著依照我們談過的功能寫出你自己的版本的這些類別。
練習12.26
用allocator類別來重寫481頁上的那個將建構與配置結合的程式。10:41:00
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++)<<",";//++優先權比*高,加不加()是一樣的
cout << endl;
//delete[] p; //因為普通常值(恆定)指標(內建型別為對string的指標)p指向的是一個動態陣列,所以對它delete必須加上[]空的方括號
alloc.destroy(p);
alloc.deallocate(p,n);//摧毀解構後別忘了釋放(解配置) 第88集 3:26:40
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise12_26/prog1/prog1.cpp
留言