• 文章
  • 您想构建一个程序,但从哪里开始
发布
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++代码,并且在许多人看来甚至可能是错误的。

欢迎随时与我联系。