福大軟工1816 · 第二次作業
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
作為其子類。
整體構思完成之後查詢了文件的輸入輸出操作,在比對了fstream
和FILE*
寫法的不同之後,我選擇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 · 第二次作業