C++自修入門實境秀、C++ Primer 5版研讀秀 101/ ~12.3.2. Defining the Query Program Cla...
12.3. Using the Library: A Text-Query Program
第101集開始 系統喇叭音量大小設定
6:30
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多行。(中文版,頁484)
頁485
12.3.1. Design of the Query Program
第101集26:29
12.3.1此查詢程式之設計
這個檢索程式的設計
要開始設計一個程式前,最好是先能條列出這個程式會用到的所有操作/運算。能知道我們會需要怎麼樣的運算,就能夠明白我們會需要什麼樣的資料結構(這裡指定義類別物件所需要的資料結構)。若能以這樣的需求取向來考量,就知道我們這個檢索程式必須要能完成如下的工作(達成如下的任務)才行:
在讀取資料的同時,程式必須記錄下檢索之字出現的那行文字。因此就必須借重一次讀取一行的功能,而且還要在讀取後,有效地將各個字詞從整行中分析出來。【包不包括標點?!一般是不會包括的!】
在輸出檢索結果的時候:
程式必須能擷取到檢索字詞所在行的行號
這些行號又必須按照遞增的方式來排序,且又不能重複
程式要能將行號與其內容都呈現(print)出來
若能善用程式庫提供的各種機能,這些需求就能得到很好的滿足
可以用vector<string>來儲存整個輸入檔案的內容。先把整個檔案讀入,就不會影響原檔案;然後將讀入的檔案內容每行編號,可把每行當作vector的每個元素來儲存,那麼元素的索引值就會是「行號減1」(行號-1。因為沒有0行,而索引值是由0開始的)。要印出檢索的結果行時,就可以用下標的方式提取其行內容(即元素值)
利用istringstream(§8.3,頁321)來將每行內的字詞切割開來。33:20
用關聯式容器(associative container)set來儲存來源檔案內容中每個字詞所在的行號。因為用set可以確保我們儲存的值不會重複,而且行號還會依大小遞增的次序存入set。
用關聯式容器map來將每個字詞與set中所儲存的該字詞所在的行號作關聯(這裡的associate就如map(映射))。如關聯式資料庫(資料表)的概念。這樣一來,就可以利用map來提取set的內容:不管檢索的字詞是什麼都可以得到它們對應到的行號。
中文版這裡亂翻!錯得離譜!!
我們會用一個map來將每個字詞關聯到該字詞出現的行號集合(set of line numbers)。使用map能讓我們擷取任何給定字詞的set。
其實上述利用set、map作儲存容器的設計應該就類似資料庫中做索引的概念。
我們的解決方案還會用到shared_ptr,原因稍後我們會說明。
Data Structures 資料結構(實體資料的框架、代數、模型)
38:00
有了需求取向的設計概念後,就是對資料結構的設計實作了
雖然我們也可以直接用vector、set、map來寫我們的程式就好,但是如果我們的解決方案(solution)能夠定義一個更為抽象化的資料結構,那將會更加實用。意思就是說,不定義類別也是可以直接在main或函式內用上vector等來實作。可是如果能設計一個抽象類別,日後的彈性更大、應用更廣。因此,我們可以從定義一個類別來著手(類別即抽象資料結構或抽象資料型別(abstract data type),詳前),用這個類別來存放來源資料檔(input file)以便在檢索字串時使用。這個類別,可命名為TextQuery,它會儲存一個vector和一個map。vector會存放來源檔的內容,map則會負責將來源檔中的每個字詞和set中所存放的那個字詞(that word)所在的行號關聯起來。41:50這個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的內容來用,因為就只為了印出(可能通常僅只是)來源檔中一小部分的檢索結果內容,卻得動用到整個來源檔的內容,這實在是太過誇張、荒謬了。
56:30
因為函式回傳的方式預設為「拷貝」回傳值,故有此疑慮也,所以才有下面的解決方案:
我們可以藉由回傳指向TextQuery物件中的迭代器(iterator)或指標,來避免拷貝整個容器或物件的內容, 然而這樣的方式恐怕還有風險(opens up a pitfall,問題浮上檯面),這風險就是:萬一QueryResult要取得的(corresponding)TextQuery物件在取得其資料前就被摧毀了,那該怎麼辦?
想要讓QueryResult能夠存取到TextQuery的資料,那這個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來檢查要用的資料物件是否還存在。
不見題目只見關鍵字:看到「共用」,想到「動態記憶體(dynamic memory)」。
想到「動態記憶體」就要想到「智慧指標(smart pointer)」
看到動態,就要想到new
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:11:44
在設計一個類別(design a class)時,如果能在實際實作其類別中的成員之前,先寫一些用到這個類別的程式(program)來測試或表示、或試用、試算的話,對設計好一個類別,會是很有幫助的。(這裡程式與函式是等義的)
設計一個類別時,在實際實作其成員前,先寫程式使用該類別,可能會有幫助。
所以不要急著實作類別內容,而是用程式來測試它,做中學,也比較具體,方便掌握所需的功能,具體是什麼;若不合用,再不斷修改;如我們在這裡對練習12.27就擬修改多少次了。
這樣的話,我們就可以了解到要設計的類別,是否有我們所需的功能(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;表示恆為「真」true!),因此除非讀取失敗、或使用者輸入了「q」,否則迴圈就不會終止。離開迴圈的方式是藉由break這個述句(§5.5.1,頁190)來完成執行的,它在if條件式為真ture後,就會被執行,while迴圈也就結束了。
頁487
1:24:50
只要使用者輸入的不是「q」,就會讓tq去檢索使用者輸入的字詞,並以print函式在這裡應是介面函式,而不是成員函式,因為print前沒有成員存取運算子「.」與型別物件。來列印出檢索的結果。
1:32:00
練習12.27
TextQuery和QueryResult類別會用到的功能並不會超出我們已經討論過的範圍。在繼續閱讀本書前,針對這兩個類別,先試著依照我們談過的功能寫出你自己版本的TextQuery和QueryResult。
第98集 1:52:00
成功了。沒錄到的部分,請看臉書直播503集2:09:00前後 https://www.facebook.com/oscarsun72/videos/2569013886543063
2:8:00這次12、11章通盤重讀重譯複習一遍真的有價值了!
2:59:47 來試驗當TextQuery死掉時QueryResult還能正常運作,才能彰顯利用智慧指標資源共用的真諦
這裡只貼我最後用shared_ptr來管理容器的,其餘詳:
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise12_27print_QueryResult_without_TextQuery/prog1
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise12_27/prog1
.cpp
#include<iostream>
#include<memory>
#include <fstream>
#include"TextQuery.h"
#include"QueryResult.h"
using namespace std;
pair<shared_ptr<vector<string>>, shared_ptr<map<string, set<size_t>>>> queryData(ifstream& infile)
{
string lStr;
size_t line_Num{ 0 };
vector<string>vs;//主要就是這兩個(vector、map)容器要作為TexQuery與QueryResult資源共享者
map<string, set<size_t>>word_lineNum;
/*用了make_shared函式,就已經動用到了動態記憶體區了:
這個函式會在動態記憶體區中配置並初始化(即建置)一個物件,然後回傳一個shared_ptr指向該物件。和智慧指標一樣,make_shared也是定義在memory標頭檔中。(頁451)
而shared_ptr類別是會保證只要還有任何的shared_ptr依附在那個記憶體上,那個記憶體就不會被釋放。(頁454)
https://play.google.com/books/reader?id=J1HMLyxqJfgC&pg=GBS.PT842.w.7.0.42
*/
shared_ptr<vector<string>>spVs(make_shared<vector<string>>(vs));//利用智慧指標shared_ptr來達到
shared_ptr<map<string, set<size_t>>>spWord_lineNum(
make_shared<map<string, set<size_t>>>(word_lineNum));//資源共用的目的
while (infile && !infile.eof())//第98集6:46:00
{
getline(infile, lStr);
spVs->push_back(lStr);//one line of text in an element
++line_Num;
istringstream isstr(lStr);
string word;
while (isstr >> word)
{
map<string, set<size_t>>::iterator mIter = spWord_lineNum->find(word);
if (mIter == spWord_lineNum->end()) {//如果文字行號的map還沒有此文字的話
set<size_t> line_num_st;
line_num_st.insert(line_Num);
spWord_lineNum->insert(make_pair(word, line_num_st));
}
else//如果文字行號的map已經有此文字的話
mIter->second.insert(line_Num);//若原已有此行號,用insert就不會插入(何況set本來鍵值(就是「值」)就不能重複
}
}
return make_pair(spVs, spWord_lineNum);
}
int main() {
string fName, strSearch;
cout << "請指定要檢索的檔案全名(fullname,含路徑與副檔名)" << endl;
if (cin >> fName);
//必須檢查檔案存不存在
else//若沒有指定檔案的話
{
fName = "V:\\Programming\\C++\\input.txt";
}
cin.clear();//cin前面已經移動它的迭代器(iterator)了到讀取失敗的位置,故要歸零清除,
//否則如果這裡讀取失敗,後面的cin >> strSearch判斷就會永遠都是false(讀取失敗)了
//第89集1:4:00//可參考前面談資料流(stream)的部分
ifstream ifs(fName);
TextQuery tq(queryData(ifs));
while (true) {
cout << "請輸入檢索字串,或輸入「q」離開" << endl;
if (!(cin >> strSearch) || strSearch == "q") break;
QueryResult qr = tq.query(strSearch);
qr.print();
}
}
TextQuery.h
#ifndef TextQuery_H
#define TextQuery_H
#include<vector>
#include<memory>
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>//要用getline函式,要引入這一行
#include<map>
#include<set>
#include "QueryResult.h"
using namespace std;
class TextQuery
{
friend class QueryResult;
typedef pair<map<string, size_t>::const_iterator, map<string, size_t>::const_iterator>
pair_iterator_map;
typedef pair<shared_ptr<vector<string>>, shared_ptr<pair<string, set<size_t>>>> pair_sp_vec_str_sp_pair_str_set;
using iterator_map = map<string, set<size_t>>::iterator;
public:
//TextQuery() ;
TextQuery(ifstream& infile);
TextQuery(pair<shared_ptr<vector<string>>, shared_ptr<map<string, set<size_t>>>>spPair) :
spVs(spPair.first), word_lineNum(*spPair.second) {};
~TextQuery();
QueryResult query(const string&);
private:
shared_ptr<vector<string>>spVs;//第89集 2:12:00
//一個map關聯式容器(associative container)因為一個字詞key(string)會有好幾行與之對應,故用
//map,而其「值」為set容器
map<string, set<size_t>>word_lineNum;
};
TextQuery::TextQuery(ifstream& infile)
{
string lStr;
size_t line_Num{ 0 };
vector<string>vs;
spVs = make_shared<vector<string>>(vs);
while (infile && !infile.eof())//第89集2:4:00
{
getline(infile, lStr);
spVs->push_back(lStr);//one line of text in an element
++line_Num;
istringstream isstr(lStr);
string word;
while (isstr >> word)
{
map<string, set<size_t>>::iterator mIter = word_lineNum.find(word);
if (mIter == word_lineNum.end()) {//如果文字行號的map還沒有此文字的話
set<size_t> line_num_st;
line_num_st.insert(line_Num);
word_lineNum.insert(make_pair(word, line_num_st));
}
else//如果文字行號的map已經有此文字的話
mIter->second.insert(line_Num);//若原已有此行號,用insert就不會插入(何況set本來鍵值(就是「值」)就不能重複
}
}
}
TextQuery::~TextQuery()
{
}
QueryResult TextQuery::query(const string& wordForQuery)
{
/*第88集4:18:23//4:31:30回傳的應該是檢索結果,
*(此行註文但作參考)或者試用allocator物件記錄在動態記憶體(dynamic memory),再與QueryResult物件共用此資料*/
//臉書直播第443集、444集。第89集1:18:00
iterator_map wlIter = word_lineNum.find(wordForQuery);
if (wlIter == word_lineNum.end())
{
cout << "沒有找到您要找的字串!" << endl;
set<size_t>st;
return QueryResult(make_shared<pair<string, set<size_t>>>
(make_pair(wordForQuery, st)));//()呼叫運算子(call operator)這裡表示呼叫預設建構器(default constructor)
}
//shared_ptr<pair<string, set<size_t>>> sp = make_shared<pair<string,set<size_t>>>(*wlIter);
//QueryResult qrfound(spVs, sp);
return QueryResult(spVs, make_shared<pair<string, set<size_t>>>(*wlIter));//「()」:呼叫建構器
}
#endif // !TextQuery_H
QueryResult.h
#ifndef QueryResult_H
#define QueryResult_H
#include<vector>
#include<memory>
#include<iostream>
#include<iterator>
#include"TextQuery.h"
using namespace std;
class QueryResult
{
public:
QueryResult(shared_ptr<pair<string, set<size_t>>>sp_key) :pair_str_set(sp_key) { found = false; }
QueryResult(shared_ptr<vector<string>>, shared_ptr<pair<string, set<size_t>>>);
~QueryResult();
void print();
private:
shared_ptr<vector<string>>vs;
shared_ptr<pair<string, set<size_t>>>pair_str_set;
bool found;
};
QueryResult::QueryResult(shared_ptr<vector<string>> sp_vec_str, shared_ptr<pair<string, set<size_t>>> sp_pair_str_set)
{
vs = sp_vec_str;//這樣才是由智慧指標(smart pointer)來管理,而不是
//vs=*sp_vec_str ←原來寫這樣,就是解參考智慧指標後再把解參考的結果容器,指定(即複製一份)
//給「vs」這個資料成員;這樣就不合資源共享(共用資源)的原則,反而成複製一份了。第98集2:40:00
pair_str_set = sp_pair_str_set;
found = true;
}
QueryResult::~QueryResult()
{
}
inline void QueryResult::print()
{
ostream_iterator<string>o(cout);
ostream_iterator<size_t>o_size_t(cout);
*o++ = pair_str_set->first;
*o++ = " occurs ";
if (!found)
*o++ = "0 time";
else//如果有找到
{
*o_size_t++ = pair_str_set->second.size();
*o++ = (pair_str_set->second.size() > 1) ? " times" : " time";
cout << endl;
for (const size_t i : pair_str_set->second)
{
*o++ = "\t(line ";
*o_size_t++ = i;
*o++ = ")";
*o++ = (*vs)[i - 1];
cout << endl;
}
}
cout << endl;
}
#endif // !QueryResult_H
觀念題! escape character 破格字元(破例字元、翻義字元、轉義字元、反義字元……):「\」:意謂將C++語言定義符號,還原(翻轉)為原來的定義(即人類或作業系統用的意義)「\」反斜線後面加一個字元,二者組合成的就叫反義序列sequence
感恩鄭宇翔老師啟發 https://youtu.be/WCpHAWsnS-4
練習12.28
此後重譯撰者詳此檔 http://bit.ly/2Rw53sH ,併合中文版原譯,不再分二檔了。
練習12.29
12.3.2. Defining the Query Program Classes
頁487
練習12.28
複習關聯式容器map、set及查找其鍵值的部分
第101集 1:45:40 1:59:10
寫一個沒有定義類別來處理資料的文字查詢程式。這個程式應該要帶有一個檔案的參數,並能與使用者互動來對該檔案的文字進行查詢。利用vector、map和set這些容器來存放該檔案的內容且產出查詢的結果。
寫一個程式來實作文字查詢,但不定義類別來管理資料。你的程式應該接受一個檔案,作為引數,並能與使用者互動,以查詢那個檔案中的字詞。使用vector、map與set容器來存放該檔案的資料,以及產生查詢的結果。
1:51:45 2:59:00
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>
#include<vector>
#include<map>
#include<set>
#include<memory>
using namespace std;
vector<string>vs;
map<string, set<size_t>>mpWord_lineNum;
map<string, set<size_t>>::iterator mpIter;
void qureyData(ifstream& ifs) {//配置好檢索資料
string wordLine;
size_t lineNum(0);
while (!ifs.eof() && getline(ifs, wordLine))
{
vs.push_back(wordLine);
lineNum++;
istringstream is(wordLine);
string word;
while (is >> word)
{
mpIter = mpWord_lineNum.find(word);
if (mpIter == mpWord_lineNum.end()) {
set<size_t>st_lineNum;
st_lineNum.insert(lineNum);
mpWord_lineNum.insert(make_pair(word, st_lineNum));
}
else//若map中已有此字
mpIter->second.insert(lineNum);
}
}
}
void query(string& searchWord) {
mpIter = mpWord_lineNum.find(searchWord);
if (mpIter == mpWord_lineNum.end())
{
cout << "沒有找到\"" << searchWord << "\"字!" << endl;
}
else
{
size_t s = mpIter->second.size();
cout << endl;
cout << searchWord << " occurs " << s << ((s > 1) ? " times" : " time") << endl;
for (size_t s : mpIter->second)
cout << "\t(line " << s << ") " << vs[s-1] <<endl;
cout << endl;
}
}
int main() {
string strSearch;
cout << "請指定要檢索的檔案全名(fullname,含路徑與副檔名)" << endl;
if (cin >> strSearch);
//必須檢查檔案存不存在
else//若沒有指定檔案的話
{
strSearch = R"(V:\Programming\C++\input.txt)";//"V:\\Programming\\C++\\input.txt";raw string literal
}
ifstream ifs(strSearch);
qureyData(ifs);
cin.clear();
while (true)
{
cout << "請輸入檢索字串,或輸入「q」離開" << endl;
if (!(cin >> strSearch) || strSearch == "q") break;
query(strSearch);
}
}
3:15:46
練習12.29
我們也可能用do while迴圈來作為與使用者互動的機制。就請用do while來重寫那個迴圈。說說看您比較喜歡哪種版本,為什麼?
我們其實可以把用來管理使用者互動的迴圏寫成一個do while (§5.4.4,頁189)。改寫那個迴圈,使用do while。請解釋你偏好哪個版本以及原因。
do
{
cout << "請輸入檢索字串,或輸入「q」離開" << endl;
if (!(cin >> strSearch) || strSearch == "q") break;
query(strSearch);
}while (true);
while就可以不再贅一個do了。
12.3.2. Defining the Query Program Classes
定義這個文字查詢程式的類別
首先定義這個TextQuery類別。使用到這個類別的,會透過讀取輸入用的檔案,來傳入一個istream引數以創建一個TextQuery型別的物件。TextQuery類別還定義了一個query成員函式,它帶了一個作為檢索用的string引數,並回傳一個QueryResult型別的物件,這個物件內會含有那個檢索的string所在行的資訊。
TextQuery類別的資料成員則必須要和QuerResult物件分享它的資訊。
改中文版作文:
這個類別的資料成員必須考慮要與QueryResult物件共享資料。
QueryResult類別會用到存放了要被檢索的檔案資料(即讀入的檔案內容)的vector、及儲存在這個資料中的詞彙與其所在行資訊的set。因此TextQuery類別就要有二個資料成員:一個是指向動態配置出來的這個vector的shared_ptr,還有一個是由string映射到shared_ptr<set>的map容器。這個map存放了檢索用檔案資料內的所有詞彙及其所在行號的資訊,這些行號資訊是由一個動態配置的set來儲存的。
定義一個型別成員(即型別別名(type alias),§7.3.1,p.271)來作為引用行號資料時的型別參考,會讓我們寫出來的程式碼讀來更淺顯易懂。這些行號是用來作為vector的索引值,來對vector下標用的。
第101集3:59:00
class QueryResult; //對於query成員函式的回傳值而言,這個宣告是必須,因為QueryResult就是query函式的回傳型別
class TextQuery
{
public:
using line_no = vector<string>::size_type;
TextQuery(ifstream &);
QueryResult query(const string &) const;
private:
shared_ptr<vector<string>> file; //指向要被檢索的檔案資料
//
map<string, shared_ptr<set<line_no>>> wm;
};
4:9:00 4:32:00
頁488
要放到標頭檔的程式碼一般都不應該用到using宣告(詳頁83),因此在這裡,我們才要將每個程式庫名稱(library name)都冠上std::這樣的前綴。雖然難讀了些,但是是有其必要的(原因詳見頁83)。
TextQuery的建構器
4:21:23
TextQuery的建構器帶了一個ifstream的引數,這個ifstream能一次讀取要檢索的來源檔案一行內容的文字。
//讀取要檢索的檔案內容以建構含有其每行內容及箭號的map
TextQuery::TextQuery(ifstream &is) : file(new vector<string>)
{
string text;
while (getline(is, text))
{ //一行行處理要檢索的檔案
file->push_back(text); //將這行的內容存放到vector裡頭
int n = file->size() - 1; //當前這行的行號
istringstream line(text); //讀取此行內的各個詞彙
string word;
while (line >> word)
{ //處理這行內的個個字彙
//如果這個字彙並不在wm這個map裡面,就對wm下標來新增這個字彙進去
auto &lines = wm[word]; //lines這個變數的型別是shared_ptr
if (!lines) //如果這個shared_ptr是個空值的話,那麼這就是第一次加入這個新字彙到wm中
lines.reset(new set<line_no>); //若此字彙是第一次加入到wm中就配置一個新的set給wm作「值」用
lines->insert(n); //將此行的行號,在set中記下來
}
}
}
4:55:02
這個建構器的初始器配置了一個由new動態建置的vector,來作為file資料成員的初始器,以存放要檢索的檔案內容。我們利用getline函式來就此檔案內容逐行處理,將其每行的內容都存到這個vector裡去。因為file是個指向這個vector的shared_ptr,所以須用箭號運算子(arrow operator:-> operator)來存取這個vector的push_back成員函式以在vector中以每行一個元素的方式來存入每行來源檔案的內容。
接著我們用istringstream(§8.3,頁321)來逐字處理正在讀取的這行文字。內圈的while是用這個istringstream來讀取這行的每個字,再把它們存到word這個變數中。在while迴圈中我們用了map的下標運算以取得對應到的指向set的智慧指標shared_ptr<set>,並將其存放到變數lines中。因為lines是個參考(對shared_ptr<set>的參考),所以對lines的改變都會影響到map中這個shared_ptr<set>的元素值。
5:18:00
如果正在處理的字彙/字詞尚未在map中,那麼對這個map(wm)的下標就會新增這個字彙到map中(§11.3.4,頁435)。而這個字彙對應到的元素值則會被值初始化(value initialize)。因此當這個新的字彙被加入到map時,lines就會是個空指標(而其型別為shared_ptr<set>)。如果lines是空指標,就表示map中還沒有這個字彙word存在,我們就可以用new來動態建置一個set,並利用lines的reset成員函式將lines重新指向到這個剛建置出來的set。
頁489
不論是否有用new來動態配置一個set出來,我們都會調用insert來加入一個新的行號進去。因為lines是個參考型別,是以傳址(pass by reference)的方式來操作變數值的,所以在這裡調用insert就會變成實際在wm這個map裡的set中加入一個新的元素。如果某個字(a given word)在同一行中出現超過一次,那麼對insert的呼叫就不會有任何作用。
5:30:00 7:27:40
關於QueryResult類別(如何定義)
QueryResult類別有3個資料成員(data member):
1.string型別,要查找的字詞;
2.shared_ptr,指向了存放著被檢索檔案內容的vector。
3. shared_ptr,指向存放了這個要檢索的字詞所在行號的set。
QueryResult唯一的成員函式就是它的建構器(constructor),這個建構器只是負責將這3個資料成員初始化:
6:3:55 7:30:20
class QueryResult
{
friend std::ostream &print(std::ostream &, const QueryResult &);
public:
QueryResult(std::string s, std::shared_ptr<std::set<line_no>> p, std::shard_ptr<std::vector<std::string>> f) : sought(s), lines(p), file(f) {}
private:
std::string sought //這次要找的字
std::shared_ptr<std::set<line_no>>lines;//要找的字所在的行號
std::shared_ptr<std::vector<std::string>> file; //要被檢索的檔案內容
};
5:52:00 7:33:30
std::string sought; // word this query represents
中文版照英文翻成
這個查詢代表的字詞
請問誰看得懂?!我翻成:
這次查詢要找的字
請問誰看不懂?!!6:5:20
這個建構器唯一的工作就是將它的引數作為其所對應到的資料成員的初始值。(參考建構器初始器串列(constructor initializer list,§7.1.4,頁265)
query成員函式
query成員函式有一個string的引數,它是用來作為檢索map的鍵值,以找到它所對應到的行號;這個行號是存放在set容器裡。如果這個string有在map中找到,那麼query函式就會用這個要找的string來創建出一個QueryResult的物件出來;它會帶入TextQuery的file這個資料成員以及由wm這個map取得的儲存行號訊息的set,來作為建構這個QueryResult物件的參數。
剩下來的問題就只有:要找的string若沒有找到的話,那這個query函式要回傳什麼東東呢?此時就沒有適合的set來回傳了。因此我們將藉由定義一個區域的靜態變數(local static object)shared_ptr來指向一個空的set。當string沒有被找到時,query就會回傳這個靜態shared_ptr的副本:6:18:56 7:36:50
QueryResult TextQuery::query(const string &sought) const
{
//如果要找的字沒找到的話,就回傳這個指向空set的shared_ptr
static shared_ptr<set<line_no>> nodata(new set<line_no>);
//用find來找而不是用下標運算,以避免動到wm中的元素
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file);
//沒有找到的話,就回傳這個QueryResult
else
return QueryResult(sought, loc->second, file);
}
印出查詢/檢索結果
7:38:55 7:40:11
print函式則負責在指定給它的stream上印出傳入給它的QueryResult物件。 6:28:30 6:38:01
ostream &print(ostream &os, const QueryResult &qr)
{
//如果要找的字有找到,就印出它出現的次數及所有找到的內容
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "time", "s") << endl
//印出所有要找的字詞所在行的內容
for (auto num : *qr.lines) //處理set中的每個元素
//避免出現「第0行」這樣的訊息來使人困擾
os
<< "\t(line " << num + 1 << ")"
<< *(qr.file->begin() + num) << endl;
return os;
}
這裡是用了qr.lines所指向的set的size成員函式回傳值來表示有多少行含有要找的字。(在這裡表示要檢索的字詞出現了幾「次」,同一行內只算一次)因為set是shared_ptr lines所指的物件,所以我們必須將lines解參考,才能取用它所指向的set,以調用set的size成員函式。我們在這裡又調用了在§6.3.2中製作的make_plural函式(頁224)來判斷是要印出「time」還是「times」;這是利用set容器的元素數量(即size成員函式回傳的值)是否是1來決定的。
在for迴圈中,我們將linses所指向的set內的元素逐一巡覽。在此,我們以人們比較能接受的方式印出了行號(使它不會從「0行」開始。The body of the for prints the line number, adjusted to use human-friendly counting. 翻譯學:翻譯,難道就不需要「human-friendly」嗎?!自己看我們重譯撰的和中文版原文哪個對中文母語者較為「human-friendly」呢!)。而set內所存的數值則是作為對vector元素存取的索引值,它是由0開始的數字。只是一般人都覺得行號應從1開始,而不是從0,因此我們就將行號通通加1來換算成人們比較能夠接受的行號表示方式。
我們用此行號再去取得file所指向的vector它所儲存關於該行的內容。還記得當我們對vector的迭代器進行加法時,我們就會將這個迭代器往前推進了我們加了多少個的位置嗎?這裡就是用這個迭代器算術的結果來取得該位置上的元素值。(§3.4.2,頁111)因此「file->begin()+num」這個表述式就是表示對於file指向的vector,將其解參考後取得其begin成員函式回傳的那個指向vector中第一個元素的迭代器,我們將它推了num個位置。
當要找的字沒有找到時,print函式又要怎麼處理呢?此時set會是個空的容器,這裡第一行「os <<」開頭的述句會印出:要找的字,只出現0次。因為解參考qr.lines(*qr.lines,英文版誤作「*res.lines」,中文版也照翻!)後得到的是個空的set,並沒有元素存在,所以for迴圈並不會執行。
7:5:00 8:0:50
練習12.30
定義您自己版本的TextQuery和QueryResult類別並執行§12.3.1(頁486)的runQueries函式看看。
前面練習12.27已經定義過了,這默寫(複講)本段課文的。
8:33:40 9:30:50
忘了錄的部分見臉書直播526集 https://www.facebook.com/oscarsun72/videos/2612859058825212/
練習時才發現課本程式碼有錯(或不足的部分)今標識如下)!
.cpp
#include<iostream>
#include<fstream>
#include<string>
#include"TextQuery.h"
using namespace std;
void runQueries(ifstream& infile){//頁486,引數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;
}
}
int main() {
ifstream ifs(R"(V:\Programming\C++\input.txt)");
runQueries(ifs);
}
9:37:50
TextQuery.h
#ifndef TextQuery_H
#define TextQuery_H
#include<vector>
#include<memory>
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>//要用getline函式,要引入這一行
#include<map>
#include<set>
//using namespace std;標頭檔避免用這個
class QueryResult;
class TextQuery
{
public:
using line_no = std::vector<std::string>::size_type; //這line_no只是TextQuery類別的型別成員,QueryResult要用它,當然要前置範疇運算子「::」!
TextQuery(std::ifstream&);
QueryResult query(const std::string&)const;
private:
std::shared_ptr<std::vector<std::string>>file;
std::map<std::string, std::shared_ptr<std::set<line_no>>>wm;
};
TextQuery::TextQuery(std::ifstream& is) :file(new std::vector<std::string>)
{
std::string text;
while (getline(is, text))//課本頁488沒有「&&!is.eof()」恐怕會出問題
{
file->push_back(text);
int n = file->size() - 1;//頁488。這裡應是用size_t或unsigned
std::istringstream line(text);
std::string word;
while (line >> word)
{
auto& lines = wm[word];
if (!lines)
{
lines.reset(new std::set<line_no>);
}
lines->insert(n);
}
}
}
class QueryResult
{
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
QueryResult(std::string s, std::shared_ptr<std::set<TextQuery::line_no>>p,
std::shared_ptr<std::vector<std::string>>f)
:sought(s), lines(p), file(f) {};//英文版這裡和下面對lines的宣告「TextQuery::line_no」又錯了(中文版照錯,頁489) https://play.google.com/books/reader?id=J1HMLyxqJfgC&pg=GBS.PT906.w.2.0.0
private:
std::string sought;
std::shared_ptr<std::set<TextQuery::line_no>>lines;
std::shared_ptr<std::vector<std::string>>file;
};
QueryResult TextQuery::query(const std::string& sought)const
{
static std::shared_ptr<std::set<TextQuery::line_no>>
nodata(new std::set<line_no>);
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file);
else
return QueryResult(sought, loc->second, file);
}
std::ostream& print(std::ostream& os, const QueryResult& qr) {
os << qr.sought << " occurs " << qr.lines->size() <<
((qr.lines->size() > 1) ? " times" : " time") << std::endl;
for (auto num : *qr.lines)
os << "\t(line " << num + 1 << ") " << *(qr.file->begin() + num) << std::endl;
return os;
}
#endif // !TextQuery_H
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise12_30/prog1
9:47:17
練習12.31
如果我們用vector來取代程式碼中的set來存放行號資訊,結果會變成怎樣?用哪一種容器較好,為什麼?
set是的元素值是唯一、不會重複的,且是經過排序的。vector就不然。然在此例中,因為是由第一行,逐行讀取的,所以行號順序也還好,就怕是一個字詞在同一行中出現超過1次,那麼行號就會重複,但也因此方便計算所有字詞出現的次數。在課本所舉例中,並不會排除標點符號與文字粘結的情形,所以,諸如「word」與「word.」就分別當作不同的字詞了。
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise12_31/prog1
忘了錄的部分見臉書直播526集 https://www.facebook.com/oscarsun72/videos/2612859058825212/
10:3:59
練習12.32
用StrBlob取代vector<string>來存放要檢索的檔案內容。
.cpp檔同前
TextQuery.h
#ifndef TextQuery_H
#define TextQuery_H
#include"StrBlob.h"
#include<memory>
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>//要用getline函式,要引入這一行
#include<map>
#include<set>
//using namespace std;標頭檔避免用這個
class QueryResult;
class TextQuery
{
public:
using line_no = StrBlob::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string&)const;
private:
std::shared_ptr<StrBlob>file;
std::map<std::string, std::shared_ptr<std::set<line_no>>>wm;
};
TextQuery::TextQuery(std::ifstream& is) :file(new StrBlob)
{
std::string text;
while (getline(is, text))//課本頁488沒有「&&!is.eof()」恐怕會出問題
{
file->push_back(text);
int n = file->size() - 1;//頁488
std::istringstream line(text);
std::string word;
while (line >> word)
{
auto& lines = wm[word];
if (!lines)
{
lines.reset(new std::set<line_no>);
}
lines->insert(n);
}
}
}
class QueryResult
{
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
QueryResult(std::string s, std::shared_ptr<std::set<TextQuery::line_no>>p,
std::shared_ptr<StrBlob>f)
:sought(s), lines(p), file(f) {};//英文版這裡和下面對lines的宣告「TextQuery::line_no」又錯了(中文版照錯,頁489) https://play.google.com/books/reader?id=J1HMLyxqJfgC&pg=GBS.PT906.w.2.0.0
private:
std::string sought;
std::shared_ptr<std::set<TextQuery::line_no>>lines;
std::shared_ptr<StrBlob>file;
};
QueryResult TextQuery::query(const std::string& sought)const
{
static std::shared_ptr<std::set<TextQuery::line_no >>
nodata(new std::set<line_no>);
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file);
else
return QueryResult(sought, loc->second, file);
}
std::ostream& print(std::ostream& os, const QueryResult& qr) {
os << qr.sought << " occurs " << qr.lines->size() <<
((qr.lines->size() > 1) ? " times" : " time") << std::endl;
for (auto num : *qr.lines)
//以下2式效果相同
//os << "\t(line " << num + 1 << ") " << *(qr.file->begin() + num) << std::endl;
os << "\t(line " << num + 1 << ") " << (*qr.file)[num] << std::endl;
return os;
}
#endif // !TextQuery_H
StrBlob.h
#ifndef STRBLOB_H
#define STRBLOB_H
#include<vector>
#include<string>
#include<memory>
class StrBlob
{
public:
typedef std::vector<std::string>::size_type size_type;
typedef std::vector<std::string>::iterator iterator;//自定義迭代器
StrBlob() :data(new std::vector<std::string>){};
StrBlob(std::initializer_list<std::string> il);
inline size_type size() const { return data->size(); }
inline bool empty() const { return data->empty(); }
// add and remove elements
inline void push_back(const std::string& t) {data -> push_back(t);}
inline void pop_back() { data->pop_back(); }
// element access
inline std::string& front() { return data->front(); }
inline std::string& back() { return data->back(); }
inline std::string& operator[](size_type i) { return data->at(i); }//自定義StrBlob的下標運算子
//
inline iterator begin() { return data->begin(); }
private:
std::shared_ptr<std::vector<std::string>> data;
void check(size_type i, const std::string& msg) const;
};
#endif // !STRBLOB_H
10:40:00
https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/tree/exercise12_32/prog1
10:44:40
練習12.33
在15章我們會再擴展我們的查詢系統,在QueryResult類別中加入一些額外的成員。試著先加入begin和end這兩個成員函式以取得查詢結果中儲存行號的set的首尾二個迭代器。還有一個叫做get_file的成員函式,它會回傳一個shared_ptr,指向QueryResult物件內的那個檔案(file)的。
在第15章中,我們會擴充我們的查詢系統,並且會需要在QueryResult類別中添加一些額外的成員。新增名為begin和end成員,它們會回傳迭代器指向一個給定的查詢所回傳的行號set中,以及一個名為get_file的成員,回傳一個shared_ptr指向QueryResult物件中的檔案。(中文版)
留言