1. 程式人生 > >福大軟工1816 · 第二次作業

福大軟工1816 · 第二次作業

開發 sign nes sca proc ESS 估計 lines 字符

Github 項目地址

github

PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 10 4
· Estimate · 估計這個任務需要多少時間 10 4
Development 開發 335 635
· Analysis · 需求分析 (包括學習新技術) 50 70
· Design Spec · 生成設計文檔 80 30
· Design Review · 設計復審 20 10
· Coding Standard · 代碼規範 (為目前的開發制定合適的規範) 15 5
· Design · 具體設計 40 30
· Coding · 具體編碼 60 300
· Code Review · 代碼復審 10 10
· Test · 測試(自我測試,修改代碼,提交修改) 60 180
Reporting 報告 40 110
· Test Repor · 測試報告 20 40
· Size Measurement · 計算工作量 5 10
· Postmortem & Process Improvement Plan · 事後總結, 並提出過程改進計劃 15 60
合計 385 749

解題思路

拿到題目後,我首先聯想到的是曾經寫過的統計一行裏有幾個單詞的一段程序。那段程序通過逐個獲取字符,結合InWord標誌位來判定單詞個數。

之後結合了單詞的判斷方法造了如下的流程圖
技術分享圖片

在寫processChar函數的過程中,感覺情況有點過於復雜,而且出現了遺漏。因此將判斷合法性的過程與分離單詞的過程相分離,成為如下流程圖。

技術分享圖片

聯想到python中的高階函數,我將“讀取文件得到字符”的過程和“處理字符”的過程分成兩個類,以便修改“處理字符”的方式。如果需要進行對字符的其他操作,便可以提升類ScanProcesser為父類,而將SomeScanProcesser作為其子類。

整體構思完成之後查詢了文件的輸入輸出操作,在比對了fstreamFILE*寫法的不同之後,我選擇FILE*方式。因為fstream方式所提供的更詳細的錯誤信息在這個解決方案裏並不是很有用,fstream

下的getc方法也比較復雜。

實現過程

根據上述流程圖 得到下面的幾個類

1. ArgParser

用於解析用戶的輸入參數並驗證其合法性

該類在Main函數中被調用

主要方法:

// 構造時對輸入參數處理
ArgParser(int argc, char* argv[]);

// 獲取處理後的文件名
string getFileName();

// 用戶錯誤輸入時的測試文檔
int helpDoc();

2. Scanner

打開文件並逐個獲取字符,並向ScanProcesser類傳遞該字符
特別的,在讀取到EOF時,該類會額外傳遞一個EOF

主要方法:

// 構造時從外界獲取文件名並打開,得到對應的FILE*指針
Scanner::Scanner(std::string inFileName, ScanProcesser* inProcesser)

// 逐個讀取字符並傳遞
int Scanner::scan();

3. ScanProcesser

Scanner類傳來的每一個字符進行相應的處理,以得到字符數等多個結果數據

主要方法:

// 構造時從外界獲取std::map指針
ScanProcesser(map<string,int> * inStrMap);

// 對一個字符進行處理
// 統計各數據並將 字符串及其出現次數存入std::Map中
int processChar(char c);

// 從std::Map中獲取數據並排序,得到前十個字符串
int processTop10Words();

改進過程

這次工程中,對程序的改進一部分依舊伴隨著代碼的實際書寫,另一部分則伴隨著新多出來的單元測試和性能分析。

在最開始的設計中,ScanProcesser類傳遞字符數等結果給Scanner類,再由Scanner類傳遞給主函數,進行輸出。在代碼復審時發現這樣操作和Scanner類原本的用意不符合,而且產生了一個不必要的冗余。因此重構各個結構數據到Scanner類裏。

在寫完ScanProcesser類並進行單元測試時,便發現代碼中存在的bug和冗余,由此進行了一定量的修復和重構。

其後通過對整個程序的單元測試,發現了EOF未處理、中文無法處理等一些問題,並很快的處理了。

而在性能分析之後,發現占比最大的分別是std::string和std::map,發現了自己測試時使用的一句string str;沒有刪除。但是對於std::map暫時沒有什麽更好的思路。

技術分享圖片

目前最大的問題還是std::map的占用過高 如下

技術分享圖片

技術分享圖片

代碼說明

class ScanProcesser{

private:
    int charNum;
    int wordNum;
    int wordNumTotal;
    int lineNum;
    int inWord;// IN = 1, OUT = 0 標誌掃描指針在一個疑似單詞串中
    int newLine;// OLD = 1, NEW = 0
                // 標誌此行有無非空白符,即是否為新行
    stringstream* ss; //存儲當前找到的可疑單詞序列
    const int SPACE = ‘ ‘;
    const int LINESYM = ‘\n‘;
    const int TAB = ‘\t‘;
    const int LINKWORDSYM = ‘-‘;
};

// 處理單個字符並更新數據
int ScanProcesser::processChar(char c){
    
    // 存儲當前找到的單詞
    string nowWord;

    if (c == EOF) {
        if (newLine == OLD)

            // 當文件最後一行有數據時,行數增加1
            lineNum++;
    }
    else {

        // 中文字符不對字符數影響
        if (isascii(c))
            charNum++;
        else {
            c = TAB;
        }
    }

    //遇到換行符時刷新空行標誌
    if (c == LINESYM) {
        if (newLine == OLD) {
            newLine = NEW;
            lineNum++;
        }
    }
    else {
        if (newLine == NEW) {
            if (!(c == SPACE || c == TAB))
                newLine = OLD;
        }
    }
    
    // 遇到分隔符
    if (!(isalnum(c))){
        
        // 在可疑單詞序列內,則判斷單詞合法性,將合法單詞存入map
        if (inWord == IN) {
            *ss >> nowWord;
            if (checkWordValid(nowWord)) {
                map<string, int>::iterator iter;

                iter = strMap->find(nowWord);

                if (iter != strMap->end()) {
                    wordNumTotal++;
                    int count = (iter->second) + 1;
                    strMap->erase(iter);
                    strMap->insert(pair<string, int>(nowWord, count));
                }
                else {
                    wordNum++;
                    wordNumTotal++;
                    strMap->insert(pair<string, int>(nowWord, 1));
                }
            }
            inWord = OUT;
            delete ss;
            ss = new stringstream();//刷新ss
        }
        else;
    }
    else if (isalnum(c)){
        if (isalpha(c))
            c = tolower(c);
        if (inWord == IN);
        else{
            // 若不在可疑單詞序列內,置標誌位為IN
            // 視為進入可疑單詞序列
            inWord = IN;
        }
        *ss << c;
    }
    else;
    return 0;
}

心路歷程

在看完題目要求的時候,雖然感覺到算法並不是很復雜,但是我感覺需要很多的時間。在投靠了python(誤)之後,也已經很長時間沒有寫這麽長的c++代碼了,以至於上來甚至出現了語法錯誤。

和以前寫項目,最大的變化是加入了單元測試和流程圖。在整理完流程圖的第二天,打開工程的時候我就已經有點忘記接下來要寫哪些部分的代碼了,靠著流程圖才想起來。

在寫類的同時寫單元測試,讓我能夠方便地對單獨的類進行分析測試。原先寫這樣的工程時,由於主要的類之間具有重要的聯系,往往需要改變Main函數甚至將一些private部分改為public。完成功能之後,對整體項目的單元測試讓我能夠很快的對新的改動進行測試,從而確認改動的正確性。

然而單元測試真是想的頭破血流。為了大數據下能夠正確運行,找出了sicp裏仲夏夜之夢的txt來和璟哥進行對拍(PS: 讀者不妨也和我對拍一下)。比較困惑如何去構建好的測試用例。

福大軟工1816 · 第二次作業