從今天起唬涧,我們要開始學習用工程化的思想去解決問題狮荔。之前雅采,我們總是把所有的代碼寫在一個源文件中巡雨,這樣看起來比較方便正蛙。不過這些代碼中狂塘,有些在邏輯上關系并不緊密泪漂。對于這種情況,我們往往用獨立的文件去管理瞬捕。就像我們之前總把程序劃分為幾個相對獨立的子功能香嗓,如果把這些不同的子功能分別用獨立的文件管理起來,就能幫助我們更容易理解代碼的邏輯結構。
很多同學覺得這個系列越往后代碼越多俏蛮,越難理解第煮。其實,一個大的問題在被拆成若干個小程序之后,都是非常容易的线欲。如果拿出任何一個子功能出來,大部分人都能順利完成,那為什么放在一起就覺得難了呢,原因就在于缺乏一種工程化的編程思想床佳。
今天,我們就模擬一下團隊合作影兽,看看如何通過多人協(xié)作的方法來解決上一篇中的習題旦万。
1. 題目
編程統(tǒng)計出input.txt
文件保存的文章中丹莲,每個單詞出現(xiàn)的次數(shù)洲赵。文章內容如下:
In this chapter we will be looking at files and directories and how to manipulate them. We will learn how to create files, open them, read, write and close them. We'll also learn how programs can manipulate directories, to create, scan and delete them, for example. After the last chapter's diversion into shells, we now start programming in C.
Before proceeding to the way UNIX handles file I/O, we'll review the concepts associated with files, directories and devices. To manipulate files and directories, we need to make system calls (the UNIX parallel of the Windows API), but there also exists a whole range of library functions, the standard I/O library (stdio), to make file handling more efficient.
這段文字來自網絡鸳惯。為了統(tǒng)計更有意義,加入兩個條件:
- 統(tǒng)計過程中不考慮空格和標點符號
- 不區(qū)分大小寫(可以把所有字母轉成小寫后參與統(tǒng)計)
2. 分析
首先叠萍,我們思考一下芝发,程序流程大概如下:
- 依次掃描每個單詞
- 把單詞做簡單的處理
- 查找單詞計數(shù)
- 結果排序整理
- 打印輸出
用這五個功能就能夠組成最終的程序。假如我們有一個5個人團隊來完成這個工作苛谷,那就可以讓這5個人分別負責一個部分辅鲸。對于每個人而言,他的工作量僅僅是實現(xiàn)一個非常簡單的小程序腹殿。下面我們來看看這幾個小程序独悴。
3. 小程序一 : 文件讀取
題目:
請編程實現(xiàn)把input.txt
文件中的每個單詞打印在屏幕上。
看到這個題目赫蛇,我們很容易想到天花板編程手把手計劃-第1期-第6天中從文件中讀取表達式的方法绵患。我們依然希望像讀鍵盤輸入那樣讀取文件內容。這個問題我們交給小A同學做悟耘,他的代碼如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAX_SIZE 50
int main()
{
char str[MAX_SIZE];
freopen("input.txt", "r", stdin);
while (1)
{
str[0] = 0;
scanf("%s", str);
if (str[0] == 0)
{
break;
}
printf("%s\n", str);
}
}
有了前面的基礎落蝙,這段代碼應該很好理解。通過循環(huán)調用scanf
函數(shù)來讀取每一個字符串。需要注意的是在scanf
函數(shù)之前筏勒,我們用了str[0] = 0;
這句話對str數(shù)組進行了初始化移迫。這樣在文件結尾處,str不會發(fā)生變化管行。這時我們就知道循環(huán)可以結束了厨埋。
這段代碼的執(zhí)行結果如下:
現(xiàn)在,問題來了捐顷。如果小A同學最終給你提交的是這份代碼荡陷,你能方便地使用嗎?換句話說迅涮,你希望怎樣使用他寫好的代碼呢废赞?如果是我,我希望他能夠封裝成API函數(shù)給我叮姑。了解了我的需求唉地,小A同學給我提供了兩個文件:
- 文件:FileOper.h
// File : FileOper.h
// Author : 小A
#ifndef __FILE_OPER_H__
#define __FILE_OPER_H__
#include <stdio.h>
void FileInit();
int FileGetString(char* pBuf);
void FileDispose();
#endif
- 文件FileOper.c
// File : FileOper.c
// Author : 小A
#define _CRT_SECURE_NO_WARNINGS
#include "FileOper.h"
void FileInit()
{
freopen("input.txt", "r", stdin);
}
int FileGetString(char* pBuf)
{
pBuf[0] = 0;
scanf("%s", pBuf);
if (pBuf[0] == 0)
{
return 0;
}
else
{
return 1;
}
}
void FileDispose()
{
fclose(stdin);
}
于是,我在文件main.c
中只需要調用者三個函數(shù)即可传透。
#include <stdio.h>
#include "FileOper.h"
#define MAX_SIZE 50
int main()
{
char str[MAX_SIZE];
FileInit();
while (FileGetString(str) == 1)
{
printf("%s\n", str);
}
FileDispose();
}
現(xiàn)在小A的任務已經完成了耘沼。也許有人會說,這么一來代碼行數(shù)會變多朱盐,那是因為我們的程序實在太簡單了群嗤,如果業(yè)務邏輯稍微復雜一些,你就會發(fā)現(xiàn)這么拆開變得豁然開朗了托享。
4. 小程序二 : 字符處理
我們從文件中得到了一組單詞骚烧,但由于有標點符號和大小寫字母,影響了我們的統(tǒng)計闰围。于是小B同學領導了這個小程序。
題目:
實現(xiàn)一個字符串過濾函數(shù)既峡,濾掉字符串中的無用字符羡榴,并把所有的大寫字母轉換成小寫字母。
仔細分析一下运敢,這個小程序包括兩個功能校仑,一個是大小寫字母的轉換,另外一個是刪除符號传惠。小B給出了下面兩個文件:
- Filter.h
// File : Filter.h
// Author : 小B
#ifndef __FILTER_H__
#define __FILTER_H__
void Filter(char* pBuf);
#endif
- Filter.c
// File : Filter.c
// Author : 小B
#include "Filter.h"
void ToLower(char* pBuf)
{
int i;
for (i = 0; pBuf[i] != 0; i++)
{
if (pBuf[i] >= 'A' && pBuf[i] <= 'Z')
{
pBuf[i] += 32;
}
}
}
void Remove(char* pBuf, int index)
{
int i;
for (i = index; pBuf[i] != 0; i++)
{
pBuf[i] = pBuf[i + 1];
}
}
void FiltSymbols(char* pBuf)
{
int i;
for (i = 0; pBuf[i] != 0; i++)
{
switch (pBuf[i])
{
case '(':
// go to next
case ')':
// go to next
case ',':
// go to next
case '.':
Remove(pBuf, i);
i--;
break;
default:
// Do nothing
break;
}
}
}
void Filter(char* pBuf)
{
ToLower(pBuf);
FiltSymbols(pBuf);
}
ToLower
函數(shù)負責把傳入字符串的大寫字母轉成小寫字母迄沫。由于在ASCII碼表中,大寫字母和它對應的小寫字母的值差32,所以直接計算即可卦方。Remove
函數(shù)的功能是刪除一個字符羊瘩,實現(xiàn)過程是把它后面的每一個字符向前移動。FiltSymbols
的功能是刪掉符號,這里用了switch
的一種特殊用法尘吗,需要注意的是在不使用break
的時候要用注釋說明你是故意不寫的逝她。
有人會說,頭文件只有一個函數(shù)聲明睬捶,是否有必要專門寫一組文件呢黔宛?其實這才是重點,通過這種方法擒贸,小B同學有效地幫我們過濾掉了冗余信息臀晃,只提供給我們必要的Filter
函數(shù)。極不容易引起混淆介劫,也最大限度的降低了耦合性积仗。在main
函數(shù)中,我們只需要做簡單修改就好蜕猫。
#include <stdio.h>
#include "FileOper.h"
#include "Filter.h"
#define MAX_SIZE 50
int main()
{
char str[MAX_SIZE];
FileInit();
while (FileGetString(str) == 1)
{
Filter(str);
printf("%s\n", str);
}
FileDispose();
}
看看效果寂曹,是不是工整多了。
5. 小程序三 :單詞統(tǒng)計
下面到了小C出場了回右,他遇到的題目是隆圆。編程統(tǒng)計出一組字符串中每個字符串出現(xiàn)的次數(shù)。
首先翔烁,為了實現(xiàn)統(tǒng)計功能渺氧,首先要設計一個合適的數(shù)據(jù)結構。
- 文件Word.h
// File : Word.h
// Author : 小C
#ifndef __WORD_H__
#define __WORD_H__
#define CHAR_SIZE 50
typedef struct _tagWord
{
char m_arr[CHAR_SIZE];
int m_cnt;
}Word;
void WordSet(Word* pWord, char* pStr, int cnt);
void WordCpy(Word* pDest, Word* pSrc);
#endif
這個頭文件定義了一個結構體Word
蹬屹,它保存了一個字符串和它的次數(shù)侣背。為了方便使用,還提供了兩個函數(shù)用來給結構體賦值和拷貝慨默。這兩個函數(shù)的實現(xiàn)如下:
- 文件Word.c
// File : Word.c
// Author : 小C
#define _CRT_SECURE_NO_WARNINGS
#include "Word.h"
#include <string.h>
void WordSet(Word* pWord, char* pStr, int cnt)
{
strcpy(pWord->m_arr, pStr);
pWord->m_cnt = cnt;
}
void WordCpy(Word* pDest, Word* pSrc)
{
strcpy(pDest->m_arr, pSrc->m_arr);
pDest->m_cnt = pSrc->m_cnt;
}
接下來贩耐,小C又設計了一組統(tǒng)計方法,聲明如下:
- 文件Dict.h
// File : Dict.h
// Author : 小C
#ifndef __DICT_H__
#define __DICT_H__
#include "Word.h"
void DictInit(); // 創(chuàng)建字典
void DictInsert(char* pStr); // 插入字符串
Word* DictSearch(char* pStr); // 查找字符串
void DictSort(); // 字典排序
void DictPrint(); // 字典打印
#endif
這個文件聲明了五個函數(shù)厦取,它們都以Dict
開頭潮太,表明它們的存在是為了解決同一個問題,維護了一個字典的使用虾攻。
- 文件Fille.c
// File : Dict.c
// Author : 小C/小D/小E
#include "Dict.h"
#include <string.h>
#define SIZE 200
Word g_arrWords[200];
int g_index;
void DictInit()
{
g_index = 0;
}
void DictInsert(char* pStr)
{
WordSet(&g_arrWords[g_index], pStr, 1);
g_index++;
}
Word* DictSearch(char* pStr)
{
int i;
for (i = 0; i < g_index; i++)
{
if (strcmp(g_arrWords[i].m_arr, pStr) == 0)
{
return &g_arrWords[i];
}
}
return NULL;
}
小C只實現(xiàn)了三個函數(shù):
- DictInit()
字典初始化函數(shù)铡买,它負責初始化數(shù)組g_arrWords
。
- DictInsert()
它負責把一個字符串保存進字典霎箍。
- DictSearch()
這個函數(shù)負責在字典中尋找一個字符串的位置奇钞。找到了返回這個Word
的指針,找不到返回NULL
漂坏。這里用了最簡單的方法景埃,遍歷所有的元素媒至,比較每一個字符串看看是否匹配。比較的動作使用了字符串的庫函數(shù)strcmp
纠亚。
6. 小程序四 :字符串排序
小D負責實現(xiàn)DictSort
函數(shù)塘慕,他在文件Dict.c
中添加了下面的內容:
void DictSort()
{
int i, j;
Word wordT;
for (i = 0; i < g_index - 1; i++)
{
for (j = i + 1; j < g_index; j++)
{
if (strcmp(g_arrWords[i].m_arr, g_arrWords[j].m_arr) > 0)
{
WordCpy(&wordT, &g_arrWords[i]);
WordCpy(&g_arrWords[i], &g_arrWords[j]);
WordCpy(&g_arrWords[j], &wordT);
}
}
}
}
這里用了最常用的冒泡排序算法,通過字符串的比較實現(xiàn)排序蒂胞。里面用到了WordCpy
函數(shù)用來復制Word图呢。
7. 小程序五 :打印輸出
最后出場的是小E,他的任務很簡單骗随,把字典中的內容打印在屏幕上蛤织。
void DictPrint()
{
int i;
for (i = 0; i < g_index; i++)
{
printf("%12s - %d\n", g_arrWords[i].m_arr, g_arrWords[i].m_cnt);
}
}
8. 功能整合
五個小程序都已經完成了,現(xiàn)在作為項目的最終功能實現(xiàn)者鸿染,你擁有了四組API函數(shù)指蚜,看看如何利用這些代碼來完成這個程序。打開文件main.c涨椒,代碼如下:
#include <stdio.h>
#include "FileOper.h"
#include "Filter.h"
#include "Dict.h"
#define MAX_SIZE 50
int main()
{
char str[MAX_SIZE];
Word* pWord;
FileInit();
DictInit();
while (FileGetString(str) == 1)
{
Filter(str);
if ((pWord = DictSearch(str)) == NULL)
{
DictInsert(str);
}
else
{
pWord->m_cnt++;
}
//printf("%s\n", str);
}
DictSort();
DictPrint();
FileDispose();
}
執(zhí)行結果:
今天這個題目是一個比較簡單的練習摊鸡,我在某本C語言教科書的課后習題中看到的。但我們用了一個相對復雜的工程方式來設計代碼蚕冬,希望大家在看懂代碼的同時重點思考這個項目的邏輯結構和設計思想免猾。如果有什么問題,可以在群里討論囤热。
這部分源碼如果不清楚猎提,請在GitHub中下載。
9. 課后練習
今天的練習題我們做一道簡單的開發(fā)性程序旁蔼。請編程實現(xiàn)一個功能锨苏,輸入任意一個日期,計算出那一天是星期幾棺聊。這道題沒有任何限制伞租,你可以設計任何自己喜歡的交互形式,可以使用任何自己能想到的算法躺屁。
我是天花板肯夏,讓我們一起在軟件開發(fā)中自我迭代。
如有任何問題犀暑,歡迎與我聯(lián)系。