[0. 需求]
最近在粗略學習《C++ Primer 4th》的容器內容,關聯容器的章節末尾有個很不錯的實例。
通過實現一個簡單的文本查詢程序,希望能夠對C++的容器學習有更深的理解。
由於是淺略探討研究,高手可無視,各位讀者發現有什么不妥的地方,請指教。
程序將讀取用戶指定的任意文本文件,然后允許用戶從該文件中查找單詞。
查詢的結果是該單詞出現的次數,並列出每次出現所在的行。
如果某單詞在同一行中多次出現,程序將只顯示該行一次。
行號按升序顯示,即第 1行應該在第 2 行之前輸出,依此類推。
本人用到的文本文件“ inputfile.txt ”,內容如下(顯示的行號並非文件原內容):
1 Our program will read a file specified by the user and then allow the user to 2 search the file for words that might occur in it. The result of a query will be 3 the number of times the word occurs and a list of lines on which 4 it appears. If a word occurs more than once on the same line, 5 our program should be smart enough to display that line only once. 6 Lines should be displayed in ascending orderthat is, 7 line 7 should be displayed before line 9, and so on.
[1. 程序的設計]
設計程序的一個良好習慣是首先將程序所涉及的操作列出來。
這樣有助於建立需要的數據結構和實現這些行為。
本程序的需求如下:
它必須允許用戶指明要處理的文件名字。
程序將存儲該文件的內容,以便輸出每個單詞所在的原始行。
它必須將每一行分解為各個單詞,並記錄每個單詞所在的所有行。
在輸出行號時,應保證以升序輸出,並且不重復。
對特定單詞的查詢將返回出現該單詞的所有行的行號。
輸出某單詞所在的行文本時,程序必須能根據給定的行號從輸入文件中獲取相應的行。
1.1 數據結構
我們將用一個簡單的類 TextQuery ,再配合幾種容器的使用,實現這個程序的要求。
使用一個 vector<string> 類型的對象存儲整個輸入文件的副本。
輸入文件的每一行是該 vector 對象的一個元素。
因而,在希望輸出某一行時,只需以行號為下標獲取該行所在的元素即可。
將每個單詞所在的行號存儲在一個 set 容器對象中。
使用 set 就可確保每行只有一個條目,而且行號將自動按升序排列。
使用一個 map 容器將每個單詞與一個 set 容器對象關聯起來,該 set 容器對象記錄此單詞所在的行號。
綜上所述,我們定義的 TextQuery 類將有兩個數據成員:
1. 儲存輸入文件的 vector 對象;
2. map 容器對象(其關聯每個輸入的單詞和記錄該單詞所在行號的 set 容器對象)。
1.2 操作
對於類還要求有良好的接口。
查詢函數需返回存儲一組行號的 set 對象。這個返回類型應該如何設計呢?
事實上,查詢的過程相當簡單:使用下標訪問 map 對象獲取關聯的 set 對象即可。
唯一的問題是如何返回所找到的 set 對象。安全的設計方案是返回該 set 對象的副本。
但如此一來,就意味着要復制 set 中的每個元素。
如果處理的是一個相當龐大的文件,則復制 set 對象的代價會非常昂貴。
其他可行的方法包括:
返回一個 pair 對象,存儲一對指向 set 中元素的迭代器;或者返回 set 對象的 const 引用。
為簡單起見,我們在這里采用返回副本的方法,
但注意:如果在實際應用中復制代價太大,需要新考慮其實現方法。
類的接口需提供下列三個 public 函數:
1. read_file 成員函數,其形參為一個 ifstream& 類型對象。
該函數每次從文件中讀入一行,並將它保存在 vector 容器中。
輸入完畢后,read_file 將創建關聯每個單詞及其所在行號的 map 容器。
2. run_query 成員函數,其形參為一個 string 類型對象,返回一個 set 對象,
該 set 對象包含出現該 string 對象的所有行的行號。
3. text_line 成員函數,其形參為一個行號,返回輸入文本中該行號對應的文本行。
無論 run_query 還是 text_line 都不會修改調用此函數的對象,
因此,可將這兩個操作定義為 const 成員函數。
為實現 read_file 功能,還需定義兩個 private 函數來讀取輸入文本和創建 map 容器:
4. store_file 函數讀入文件,並將文件內容存儲在 vector 容器對象中。
5. build_map 函數將每一行分解為各個單詞,創建 map 容器對象,同時記錄每個單詞出現的行號。
[2. TextQuery 類]
定義 TextQuery 類的頭文件 “ TextQuery.h ” 內容如下:
#ifndef __TEXTQUERY_H__ #define __TEXTQUERY_H__ #include <iostream> #include <istream> #include <fstream> #include <vector> #include <map> #include <set> #include <utility> #include <string> typedef std::vector<std::string>::size_type line_no; class TextQuery { public: // interface: void read_file(std::ifstream &is) { store_file(is); build_map(); } std::set<line_no> run_query(const std::string &) const; std::string text_line(line_no) const; private: // utility functions used by read_file void store_file(std::ifstream&); void build_map(); // associate each word with a set of line numbers // remember the whole input file std::vector<std::string> lines_of_text; // map word to set of the lines on which it occurs std::map< std::string, std::set<line_no> > word_map; }; #endif
注意:這個類的定義中,在引用標准庫內容時都必須完整地使用 std:: 限定符。
read_file 函數在類的內部定義。該函數首先調用 store_file 讀取並保存輸入文件,
然后調用 build_map 創建關聯單詞與行號的 map 容器。
[3. TextQuery 類的使用]
下面的主程序 main 使用 TextQuery 對象實現簡單的用戶查詢會話。
這段程序的主要工作是實現與用戶的互動:
提示輸入下一個要查詢的單詞,然后調用 print_results 函數輸出結果。
3.1 引子
程序首先檢查 argv[1] 是否合法,然后調用 open_file 函數打開以 main 函數實參形式給出的文件。
檢查流以判斷輸入文件是否正確。
如果不正確,就給出適當的提示信息結束程序的運行,返回 EXIT_FAILURE 說明發生了錯誤。
一旦文件成功打開,建立支持查詢的 map 容器就相當簡單。
open_file 函數的實現如下:
// opens in binding it to the given file ifstream& open_file(ifstream &in, const string &file) { in.close(); // close in case it was already open in.clear(); // clear any existing errors // if the open fails, the stream will be in an invalid state in.open(file.c_str()); // open the file we were given return in; // condition state is good if open succeeded }
3.2 實現查詢
為了使用戶在每次會話時都能查詢多個單詞,我們將提示語句也置於 while 循環中:
#include "TextQuery.h" define EXIT_FAILURE -1 using namespace std; int main(int argc, char *argv[]) { // open the file from which user will quer words ifstream infile; if(argc < 2 || !open_file(infile, argv[1])){ cerr << "No input file!" << endl; return EXIT_FAILURE; } TextQuery tq; tq.read_file(infile); // prompt for a word to file and print result while(true){ cout << "Enter word to look for(or Q/q to Quit): \n" ; string str; cin >> str; // stop if hit eof on input or a 'Q' is entered if(!cin || str == "Q" || str == "q") break; // get tje set of line numbers on which this word appears set<line_no> locs = tq.run_query(str); // print count an all occurrences, if any print_results(locs, str, tq); } return 0; }
while 循環條件為布爾字面值 true,這就意味着循環條件總是成立。
在檢查 cin 和讀入 s 值后,由緊跟的 break 語句跳出循環。
具體說來,當 cin 遇到錯誤或文件結束,或者用戶輸入 q 時,循環結束。
每次要查找一個單詞時,訪問 tq 獲取記錄該單詞出現的行號的 set 對象。
將 set 對象、要查找的單詞和 TextQuery 對象作為參數傳遞給 print_results 函數,該函數輸出查詢結果。
3.3 輸出結果
輸出時,首先給出查詢到的匹配個數,即 set 對象的大小。
然后調用一個 make_plural 函數,根據 size 是否為 1 輸出“time”或“times”。
// return plural version of word if ctr isn't 1 string make_plural(size_t ctr, const string &word, const string &ending) { return (ctr == 1) ? word : word + ending; }
void print_results(const set<line_no>& locs, const string& sought, const TextQuery& file) { // if the word was found, then print count and all occurrences typedef set<line_no> line_nums; line_nums::size_type size = locs.size(); cout << "\n" << sought << " occurs " << size << " " << make_plural(size, "time", "s") << endl; // print each line in which the word appeared line_nums::const_iterator it = locs.begin(); for(; it != locs.end(); ++it){ // don't confound user with text lines starting at 0 cout << "\t(line" << (*it) +1 << ")" << file.text_line(*it) << endl; } }
以上幾個函數定義和實現都放在 main.cpp 文件之中。
注意:為了與 C++ 的容器和數組下標編號匹配,在儲存文本時,我們以行號 0 存儲第一行。
但考慮到很多用戶會默認第一行的行號為 1,所以輸出行號時,
相應地所存儲的行號上加 1 使之轉換為更通用的形式。
[4. 成員方法的實現]
TextQuery 類的成員方法實現 “ TextQuery.cpp” 文件內容如下:
#include "TextQuery.h" #include <stdexcept> #include <sstream> using namespace std; set<line_no> TextQuery::run_query(const string &query_word) const { // must use find and not subscript he map directly // to avoid adding words to word_map map< string, set<line_no> >::const_iterator loc = word_map.find(query_word); if(loc == word_map.end()){ // not found, return empty set return set<line_no>(); } else{ // fectch and return set of line numbers for this word return loc->second; } } string TextQuery::text_line(line_no line) const { if(line < lines_of_text.size()) return lines_of_text[line]; throw std::out_of_range("line number out of range"); } // utility functions used by read_file void TextQuery::store_file(ifstream &is) { string textline; while (getline(is, textline)) lines_of_text.push_back(textline); } // associate each word with a set of line numbers void TextQuery::build_map() { // process each line from the input vector for (line_no line_num = 0; line_num != lines_of_text.size(); ++line_num) { // we'll use line to read the text a word at a time istringstream line(lines_of_text[line_num]); string word; while (line >> word){ // add this line number to the set; // subscript will add word to the map if it's not already there word_map[word].insert(line_num); } } }
[5. 編譯運行]
之前一直是用 CFree5.0 做的《C++ Primer 4th》的練習,
但是由於沒找到怎么手動添加參數運行程序,
>_<|||...有知道怎么搞的,還望不吝賜教。
沒辦法最后選了在 cygwin 下編譯,命令如下:
編譯成功后,文件如下:
運行,命令如下:
結果顯示如下:
根據運行結果觀察,應該是實現了預期的程序功能。O(∩_∩)O 哈哈~