C++ 容器的綜合應用的一個簡單實例——文本查詢程序


[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 哈哈~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM