• 文章
  • 您想構建一個程式,但從哪裡開始
釋出
2008年1月14日(最後更新:2011年8月4日)

您想構建一個程式,但從哪裡開始?

評分:3.6/5(39票)
*****
您想構建一個程式,但從哪裡開始?
好的,我來告訴您。這應該按以下步驟進行
1. 規格
2. 設計
3. 實現
4. 測試與除錯
5. 文件

我們將透過一個示例程式來介紹這篇文章。想象一下,我們接到任務為一所學校製作一個程式。他們需要一個程式來儲存學生的姓名和平均分數。然後,他們應該能夠透過知道學生的姓名來查詢學生的平均分數,反之亦來。該程式還應該能夠將所有平均分數按字母順序排序並在螢幕上顯示。

好的。現在我們知道了他們的需求,所以我們可以進入第一步:規格

> 程式應以選單開始,包含以下選項:1)顯示列表 2)輸入新姓名 3)更改分數 4)刪除條目 5)按姓名搜尋 6)按分數搜尋 7)退出
> 程式應能夠執行選單中的所有任務
> 程式應將記錄(姓名和分數)儲存在硬碟上,以便在斷電時安全儲存,並在程式啟動時檢索它們。
現在進行第二步,設計
這是開發中最重要的部分,因為良好的設計將使實現變得容易而高效,而糟糕的設計將使您痛苦(程式的潛在使用者可能會因此而侮辱您!)。我們應該如何開始?有經典的程式設計方法。對於像這樣的瑣碎程式,我們使用自頂向下功能分解技術。我們編寫虛擬碼來演示設計。正如我們在規格中看到的,需要6個函式。此外,應該有一個函式來讀取使用者選擇並呼叫相應的函式。我們還需要將記錄儲存在某個地方,並在程式啟動時再次讀取它們。所以我們應該有一個類似這樣的main()函式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() 
{
     LoadDataFromFile();
     while(userChoice != 7){
     userChoice = GetUserChoice();
     switch (userChoice) {
          case 1: ShowList(); break;
          case 2: AddEntry(); break;
          case 3: ChangeMark(); break;
          case 4: Delete(); break;
          case 5: ShowMark(); break;
          case 6: ShowName(); break;
      }
   }
   SaveDataToFile();
   return 0;
}


main() 獲取使用者選擇,呼叫相應函式,並迴圈直到使用者選擇7(退出)。在這種情況下,main() 返回,程式終止。main() 還在退出時(就在返回之前)載入資料並儲存資料。

這是從檔案中讀取資料的函式。當程式第一次執行時,沒有檔案可以開啟。所以這個函式會建立一個空檔案來儲存資料。

1
2
3
4
5
6
7
8
9
10
LoadDataFromFile()
{
        if(fileExists){ 
           OpenFile();
           ReadFileToMemory();
        }
        else 
        CreateNewFile();
		
}


我們需要對列表進行排序,然後逐項列印列表中的每個項,直到到達列表末尾。每次新增一個項時,我們都會對列表進行排序,因此記憶體中將始終有一個排序後的列表。

1
2
3
4
ShowList()
{
        for(int i = 0; i < listCount; ++i  ) cout << i << "\t" << listItem<i>;
}


要新增條目,程式會詢問學生的姓名和分數。有一個名為listCount的整數,它儲存列表中記錄的數量。

1
2
3
4
5
6
7
8
AddEntry() 
{
        name = GetStudentName();
        mark = GetStudentMark();
        listCount++;
        AddToList(name, mark);		
        SortList();
}


列表中的每個條目(姓名和分數對)都有一個索引號。假設使用者想更改或刪除一個條目。她/他應該先選擇“顯示列表”選項或使用搜索選項找到所需的條目並檢視其索引。然後她/他可以使用“刪除”或“更改分數”選項,這些選項會詢問所需條目的索引。每次新增一個項時,列表都會被排序。因此,索引可能會更改。

1
2
3
4
5
6
ChangeMark()
{
	idx = GetStudentIdx();
	mark = GetStudentMark();
	SetNewMark(idx, mark);
}


我們稍後將討論SetNewMark()。

下面的函式似乎很直接

1
2
3
4
5
6
Delete()
{
	idx = GetStudentIdx();
	DeleteFromTheList(idx);
	listCount--;
} 



1
2
3
4
5
ShowName()
{
	mark = GetStudentMark();
	for(int i = 0; i < listCount; ++i  ) if(listItem<i>.mark == mark) cout  << i << "\t" << listItem.name;
}


1
2
3
4
5
ShowMark()
{
	name = GetStudentName();
	for(int i = 0; i < listCount; ++i  ) if(listItem<i>.name == name) cout << i << "\t" << listItem.mark;
}


Sort函式使用氣泡排序演算法對列表進行排序。在實現階段研究其程式碼。

您可以看到在虛擬碼中出現了一些新函式。其中一些函式很簡單,如GetStudentMark(),而有些函式則需要深入研究,如AddToList()。為了更詳細地瞭解這些函式,我們現在應該考慮如何將記錄儲存在記憶體中。

這是IT的一個分支,它討論了用於儲存特定型別資料的方法。但由於我不想進入那個領域,我選擇了一個簡單的方法。我們將有一個類來定義姓名和分數的對。

1
2
3
4
5
6
class StudentEntry
{
public:
	string name;
	int mark;
}


定義了一個條目。為了有一個列表,我們使用一個指向該類的指標陣列,該陣列儲存指向每個條目的指標。

 
StudentEntry *entryList[max_student];


AddToList() 就像這樣
1
2
3
4
5
AddToList(name, mark)
{
	entryList[entryCount] = new StudentEntry(name, mark);
	
}


如果您不熟悉“new”關鍵字,可以在此網站上閱讀有關它的資訊。

DeleteFromTheList() 就像這樣
1
2
3
4
5
6
if(entryCount != idx) 
        for(int i = idx; i < entryCount; i++) {
              entryList<i>->name = entryList[i+1]->name;
              entryList<i>->mark = entryList[i+1]->mark;
        } 
delete entryList[entryCount];


要刪除一個條目,我們只需用其後繼項替換它,並重復此操作直到列表末尾。我們還必須刪除列表向上移動時重複的最後一個條目。否則,我們將面臨記憶體洩漏。

您可以在設計虛擬碼中看到一些內容,例如(listItem.name == name)。在決定條目如何精確地儲存到記憶體中之前,我寫了它們。現在我們知道我們正在使用一個類指標陣列,我們可以將其重寫為(entryList->name == name)。

我們已經完成了設計步驟。現在我們確切地知道程式是如何工作的。我們實際上已經編寫了其中的一些部分。

第三步,實現:

現在是用C++語言編寫程式碼的時候了。正如您注意到的,我們的虛擬碼幾乎是用C++語法編寫的,但它需要進行打磨才能成為一個功能性的C++程式。我很高興能稍微解釋一下將cpp原始碼轉換為可執行檔案的過程。首先,我們用cpp語法編寫程式碼並將其儲存在硬碟上。當然,這個cpp檔案是人類可讀的。接下來,我們將這個檔案交給一個特殊的執行程式,名為“編譯器”。編譯器是一個將人類可讀資料翻譯成機器可讀資料並將其儲存為目標檔案(這些是副檔名為.obj的檔案)的程式。這些檔案還沒有準備好被系統執行。原因是它們呼叫了在其他檔案中編寫的許多例程。例如,cout << 運算子定義在.lib或.dll檔案中。還有另一個名為“連結器”的程式,它從.lib或其他.obj檔案中複製程式碼,並將它們放入目標檔案(即具有main()入口點的.obj檔案)中。完成此操作後,檔案就可以執行了,並且將具有.exe副檔名。

因此,要建立一個程式,您首先需要將原始檔儲存在某個地方,例如在Windows記事本中,然後將其交給編譯器。編譯器的輸出是我們.cpp檔案的.obj檔案,然後將其與所需的.lib檔案一起提供給連結器,以生成最終的可執行檔案。

這可能看起來很複雜,如果您真的嘗試這樣做,那將是一件非常痛苦的事情。因為這個原因,有叫做IDE(整合開發環境)的程式,它們使工作變得簡單。它們通常有一個易於使用的介面,它們會突出顯示cpp關鍵字,透過空格和縮排來格式化文件,使程式碼易於閱讀,更重要的是,它們可以在不打擾您的情況下完成編譯和連結工作。它們的工作如此順暢,以至於您不會知道有一個獨立的編譯器和連結器程式存在且獨立於IDE程式執行。IDE通常還包含一些有用的除錯工具,可以幫助我們找到程式中的錯誤。儘管如此,您還是必須知道如何使用您的IDE,我們假設您知道。如果您不知道,請閱讀其幫助和文件。現在我建立一個新的空cpp檔案來編寫我的程式碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#include <iostream> #include <string> #include <fstream> using namespace std; /////Declarations//////////////////////////////////////////////// const int MAX_STUDENT = 500; const char FILE_PATH[] = "C:\\entry_file.txt"; typedef int INDEX; typedef double MARK; typedef string NAME; int entryCount = -1; //-1 means the list is empty fstream entryFile; class StudentEntry { public: StudentEntry(NAME name, MARK mark): name(name), mark(mark){} StrudentEntry& operator = (StudentEntry &entry) { name = entry.name; mark = entry.mark; return *this; } NAME name; MARK mark; }*entryList[MAX_STUDENT]; /////Function prototypes//////////////////////////////////////// INDEX GetStudentIdx(); //Gets number of index from user MARK GetStudentMark(); //Gets number of maerk from user NAME GetStudentName(); //Gets string of name from user void DeleteFromTheList(INDEX idx); //Deletes an item with index of idx void Delete(); //Called by DeleteFromTheList(INDEX) void ChangeMark(); //Changes the mark field of a record void SetNewMark(INDEX idx, MARK mark); //Called by ChangeMark() void AddEntry(); //Adds new item to list void AddToList(NAME name, MARK mark); //Called by AddEntry() void SortList(); //Does a buble sort on list void ShowMark(); //Shows all marks with the same name void ShowName(); //Shows all names with the same mark void ShowList(); //Shows all the items in the list int GetUserChoice(); //Gets numbet of option from user void LoadDataFromFile(); //Loads data from a file void SaveDataToFile(); //Saves data to a file ///////////////////////////////////////////////////////////// INDEX GetStudentIdx() { cout << "Enter index: "; INDEX idx; cin >> idx; return idx; } MARK GetStudentMark() { cout << "Enter mark: "; MARK mark; cin >> mark; return mark; } NAME GetStudentName() { cout << "Enter name: "; NAME name; cin >> name; return name; } void DeleteFromTheList(INDEX idx) { if(entryCount != idx) for(int i = idx; i < entryCount; i++) { entryList<i>->name = entryList[i+1]->name; entryList<i>->mark = entryList[i+1]->mark; } delete entryList[entryCount]; } void Delete() { if(entryCount != -1){ DeleteFromTheList(GetStudentIdx()); entryCount--; } } void SetNewMark(INDEX idx, MARK mark) { entryList[idx]->mark = mark; } void ChangeMark() { SetNewMark(GetStudentIdx(), GetStudentMark()); } void AddToList(NAME name, MARK mark) { entryList[entryCount] = new StudentEntry(name, mark); } void SortList() { for(int i = 0 ; i < entryCount;i++){ for(int j = 0 ; j < entryCount;j++) { if(entryList[j]->name.compare(entryList[j+1]->name ) == 1) { StudentEntry temp = *entryList[j+1]; *entryList[j+1] = entryList[j]; *entryList[j] = temp; } } } } void AddEntry() { entryCount++; NAME name = GetStudentName(); AddToList(name , GetStudentMark()); SortList(); } void ShowMark() { NAME name = GetStudentName(); for(int i = 0; i <= entryCount; ++i ) if(entryList<i>->name == name) cout << i << "\t" << entryList<i>->mark << endl; } void ShowName() { MARK mark = GetStudentMark(); for(int i = 0; i <= entryCount; ++i ) if(entryList<i>->mark == mark) cout << i << "\t" << entryList<i>->name <<endl; } void ShowList() { for(int i = 0; i <= entryCount; ++i ) cout << i << "\t" << entryList<i>->name << "\t" << entryList<i>->mark << endl; } int GetUserChoice() { int choice; cout << "Enter the option's number and press enter: "; cin >> choice; return choice; } void LoadDataFromFile() { entryFile.open(FILE_PATH,ios_base::in); if(entryFile.is_open()){ cout << "File opened." << endl; char temp[100]; for(entryCount = 0; entryFile >> temp; entryCount++) { entryList[entryCount] = new StudentEntry(temp, 0); entryFile >> entryList[entryCount]->mark; } entryCount--; entryFile.close(); entryFile.clear(); } else{ entryFile.clear(); cout << "File not found in " << FILE_PATH << endl; } } void SaveDataToFile() { entryFile.open(FILE_PATH,ios_base::out); if(entryFile.is_open()){ if(!entryFile.good()) { cout << "Error writing file." << endl; } else for(int i =0 ;i<=entryCount;i++) { entryFile << entryList<i>->name<<endl; entryFile << entryList<i>->mark<< endl; } entryFile.close(); } } int main() { LoadDataFromFile(); int userChoice; do{ cout <<"1: Show List\n2: Add Entry\n3: Change Mark\n4: Delete\n5: Search Name\n6: Search Mark\n7: Save and Exit\n"; cout << "Current number of records: " << entryCount + 1<< endl; userChoice = GetUserChoice(); switch (userChoice) { case 1: ShowList(); break; case 2: AddEntry(); break; case 3: ChangeMark(); break; case 4: Delete(); break; case 5: ShowMark(); break; case 6: ShowName(); break; } }while(userChoice != 7); SaveDataToFile(); return 0; } 


此步驟現已完成。您應該仔細閱讀此程式碼,並將其與虛擬碼進行比較。它不是一個完美的實現,因為它有很多缺點:
- 您不能輸入姓名和姓氏,只能輸入其中一個
- 如果在要求輸入分數或索引時輸入了除數字以外的任何內容,程式將崩潰
- 程式中沒有錯誤處理
- 當要求輸入選項時,使用者可以輸入“asd”或“23423”之類的內容。
- 您可能會發現許多其他類似的情況

所有這些都是因為我試圖使程式碼保持簡單。
第4步,測試和除錯:
上面的程式碼實際上已經通過了這一步,因為我無法將有bug的程式碼放在網站上。在測試時,我運行了許多次並嘗試了不同的輸入資料。我遇到了一些在讀取檔案時的問題,這僅僅是因為我忘記為從檔案中讀取的項分配記憶體(使用new)。

我把程式交給了我妹妹來測試。她說我的程式不接受像12.5這樣的浮點數。由於實現得很好,我只需要將
 
typedef int MARK;


改為

 
typedef double MARK; 


我在程式碼中看到的另一個問題是分數沒有與姓名關聯。這是我的SortList()函式中的一個bug,因為它只對姓名進行了排序,而沒有對相應的分數進行排序。

最後,當檔案在LoadDataFromFile()中未開啟時,它無法儲存資料。這是因為我沒有使用clear()函式,該函式在檔案未能開啟後重置流標誌。這是一個初學者錯誤。

該程式碼已在MSVC++ 2005 SP1上進行了測試和除錯。

第5步,文件:

文件滿足了兩個人的需求;開發者和使用者。使用者需要了解如何使用該程式、已知問題以及如何進行故障排除。開發者需要了解程式的工作原理、設計如何、外掛介面是什麼(對於支援外掛的程式)等,以便將來開發該程式或進行維護。使用註釋來解釋程式中模糊或關鍵的部分是一個非常好的實踐。但額外的設計和實現描述應該寫在別處以供將來使用。

注意:本教程中在實現階段的程式碼不是一個寫得好的C++程式碼,並且在許多人看來甚至可能是錯誤的。

歡迎隨時與我聯絡。