Skip to content

Commit

Permalink
02.12 -> 02.x
Browse files Browse the repository at this point in the history
  • Loading branch information
ficapy committed Jan 1, 2021
1 parent ce5b8d2 commit 82311d6
Show file tree
Hide file tree
Showing 2 changed files with 422 additions and 0 deletions.
252 changes: 252 additions & 0 deletions 02C++基础:函数和文件/2.12 如何设计你的第一个程序.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# 如何设计你的第一个程序

现在你已经了解了一些关于程序的基础知识,让我们更仔细的看看如何设计一个程序。

当你坐下来写程序的时候,一般都会有一些想法,你想为它写一个程序。新的程序员经常会遇到困难,不知道如何将这个想法转变成实际的代码。但是事实证明,你已经从日常生活中获得了许多你需要的解决问题的技能。

要记住的最重要的事情是(也是最难做的事情),在你开始写代码之前设计你的程序。在很多方面,编程就像建筑。如果你试图在不遵循建筑规划的情况下建造一栋房子,会发生什么?很可能,除非你非常有天赋,否则你最终会得到一个有很多问题的房子:墙壁是歪的,屋顶漏水,等等.... 同样的,如果你尝试在做好规划之前进行编程,你可能会发现你的代码有非常多问题,并且你将不得不花费大量的时间来修复问题,而这些问题本来可以通过预先考虑而避免。

从长远来看,一个小小的前期规划将为你节省时间和挫折。

在本节中,我们将阐述一种将想法转化为简单程序的通用方法。

# 步骤1: 确定你的目标

为了写出一个成功的程序,你首先需要定义你的目标是什么。理想情况下,你应该能够用一两句话来说明这一点。将其表达为一个面向用户的结果往往是有用的。比如说,你的目标是什么?

- 允许用户组织一个名字和相关电话号码的列表
- 生成随机的地牢,这将产生有趣的洞穴
- 生成高分红的股票推荐列表
- 建立模型,说明从塔上掉下来的球落到地面需要多长时间

虽然这一步似乎显而易见,但它非常重要。最糟糕的情况是,你所编写的程序实际上并没有做你(或者你的老板)想要的事情!!!

# 步骤2: 确定需求

虽然定义你的问题有助于你确定你想要的结果,但是它仍然是模糊的。下一步是思考需求。

需求是一个花哨的词,它既包括你的解决方案需要遵守的约束条件(比如预算、时间线、空间、内存等等),也包括程序为满足用户需求而必须展现的功能。请注意,你的需求同样应该关注"是什么",而不是"如何做"。

比如:

- 电话号码应该保存起来,以便以后可以调用
- 随机的地牢应该始终包含从入口到出口的路径
- 股票推荐应该利用历史价格数据
- 用户应该可以输入塔的高度
- 我们需要在七天内提供一个可测试的版本
- 程序应该在用户提交请求后十秒钟内产生结果
- 该程序应该在不到0.1%的用户会话中崩溃

一个问题可能会产生很多需求,在满足所有需求之前,解决方案还没有完成。

# 步骤3: 确定你的工具、目标和备份计划

当你是一个有经验的程序员的时候,这时候通常会有许多其他的步骤,包括:

- 定义你的程序将在什么目标架构和/或操作系统上运行
- 确定你将使用哪一套工具
- 确定你是一个人写还是和团队一起
- 确定你的测试/反馈/发布 策略
- 确定你讲如何备份你的代码

然而,作为一个新手程序员,这些问题的答案通常很简单。你是在为自己写程序,独自一人,在自己的系统上,使用一个你购买或者下载的IDE, 你的代码可能除了你之外,没有其他人使用。这样一来,事情就简单了。

话说回来,如果你要做任何很复杂的工作,你应该有一个计划来备份你的代码。仅仅压缩或者复制目录到机器上的另外一个位置是不够的(虽然这比什么都没有干要好)。如果你的系统崩溃了,你将失去一些。一个好的备份策略需要从你的系统中完全复制一份代码。有很多简单的方法可以做到这一点:将其压缩并通过电子邮件发给自己,复制到Dropbox或者其他云服务,通过FTP将其复制到另外一台计算机,复制到你的本地网络中的另一台计算机,或者使用版本管理系统(比如Github)。版本管理系统还有一个额外的优势,那就是不仅仅可以恢复文件,还可以将它们回滚到以前的版本。

# 步骤4: 将困难的问题分解成简单的问题

在现实生活中,我们经常需要执行一些非常复杂的任务。试图找出如何完成这些任务是非常有挑战性的。在这种情况下,我们经常会利用自上而下的方法来解决这个问题。那就是说,我们不是解决单一的复杂任务,而是将这个任务分解成多个子任务,每个子任务都是单独比较容易解决的。如果这些子任务还是太难解决,可以进一步分解。通过不断地将复杂的任务拆分成简单的任务,你最终可以达到一个点,即每个单独的任务即使不是微不足道的,也是可以管理的。

我们来看一个例子,假设我们想打扫我们的房子,我们的任务层次结构目前是这样的。

- 打扫房间

要一次性完成打扫整个房子是一个相当大的任务,所以我们把它分成几个子任务:

- 打扫房间
- 清洁地毯
- 清洁浴室
- 清洁厨房

这样就比较容易管理了,因为我们现在有了可以单独关注的子任务。另外,我们可以将其中的一些任务进一步细分。

- 打扫房间
- 清洁地毯
- 清洁浴室
- 擦洗马桶(讨厌!)
- 清洗水槽
- 清洁厨房
- 清洁台面
- 擦洗水槽
- 清理垃圾桶

现在我们有了一个任务的层次,都不是特别的难。通过完成每一个相对容易管理的子项目,我们就可以完成比较困难的整体项目,即打扫房间。

另一种创建层次结果的方法是自下而上。在这种方法中,我们将从一个简单的任务列表开始,通过分组来构建层次结构。

举个例子,很多人平日里要上班或者上学,那么我们就要解决“上班”的问题。如果问你早上从睡觉到上班都做了哪些任务,你可能会得到下列清单。

- 挑选衣服
- 穿好衣服
- 吃早餐
- 开车上班
- 刷牙
- 起床了
- 准备早餐
- 上车
- 洗个澡

利用自下而上的方法,我们可以通过寻找具有相似点的项目分组在一起的方法将这些项目组织成一个项目组织结构:

- 从床上到公司
- 卧室
- 起床
- 挑选衣服
- 穿好衣服
- 浴室
- 洗个澡
- 刷牙
- 早餐
- 准备早餐
- 吃早餐
- 交通
- 上车
- 开车上班

事实证明,这些任务层次结构在编程中是非常有用的,因为一旦你有了任务层次结构,你就基本上定义了整个程序的结构。顶层任务(在本例中,"打扫房子"或者"上班") 称为main(), 子项成为程序中的函数。

如果发现其中一个项目(函数)太难实现,就简单地把这个项目拆分成多个子项目/子函数。最终,你应该达到一个点,即你的程序中的每个函数的实现都比较容易。

# 步骤5: 搞清楚事件的顺序

现在你的程序已经有了一个结构,是时候确定如何将所有的任务联系在一起了。第一步是确定要事件的顺序。比如,当你早上起床的时候,你做上述任务的顺序是什么?它可能是这样的。

- 卧室
- 浴室
- 早餐
- 交通

如果我们要写一个计算器,我们可能会按照这个顺序来做

- 从用户获取第一个号码
- 从用户那里得到运算符
- 从用户获取第二个号码
- 计算结果
- 打印结果

这时候,我们就可以着手实现了。

# 实现步骤1: 概述你的main函数

现在我们开始着手实施了。上面的序列可以用来勾勒你的主程序。暂时不要担心输入和输出的问题。

```c++
#int main()
{
// doBedroomThings();
// doBathroomThings();
// doBreakfaseThings();
// doTransportationThings();

return 0;
}
```

或者搞一个计算器:

```c++
int main()
{
// 获取第一个输入
// getUserInput();

// 获取操作符
// getMathematicalOperation();

// 获取第二个输入
// getUserInput();

// 计算结果
// calculateResult();

// 输出结果
// printResult();

return 0;
}
```

请注意,如果你要用这种“大纲”方法来构建你的程序,你的函数将不会被编译,因为定义还不存在。在你准备好定义函数之前,注释掉函数调用是解决这个问题的一种方法(也是我们将在这里展示的方法)。另外,你也可以将你的函数先占位(创建一个空的占位函数),这样你的程序就可以编译了。

# 实现步骤2: 实现各个函数

在这一步中,对于每个函数,你要做三件事情。

1. 定义功能原型(输入和输出)
2. 编写函数
3. 测试功能

如果你的函数足够细化,每个函数应该是相当简单直接的。如果某个函数的实现仍然显得过于复杂,也许需要将其分解成更容易实现的子函数(也有可能是你做的事情顺序不对,需要重新审视你的事件顺序)。

让我们来做计算器例子中的第一个函数

```c++
#include <iostream>

//
int getUserInput()
{
std::cout << "Enter an integer: ";
int input{};
std::cin >> input;
return input;
}

int main()
{
// 获取第一个输入
int value{ getUserInput() }; //
std::cout << value;

// 获取操作符
// getMathematicalOperation();

// 获取第二个输入
// getUserInput();

// 计算结果
// calculateResult();

// 打印结果
// printResult();

return 0;
}
```

首先,我们已经确定getUserInput函数不接受参数,并将返回一个int值给调用者,这就体现在函数原型的返回值是int,没有参数。接下来,我们已经写好了main函数,也就简单的四条语句。最后,我们在main函数中实现了一些临时代码,以测试函数getUserInput(包括其返回值)是否正常工作。

我们可以用不同的输入值来运行这个程序多次,并确保此时程序的表现符合我们的预期。如果我们发现有什么地方不正常,我们就知道问题出在我们刚刚写的代码之中。一旦我们确定程序是按照预期工作的,我们就可以删除临时测试代码,并继续实现下一个函数(getMathematicalOperation)。

记住,不要一次性实现整个程序。分步骤运行,在继续之前测试每一步。

# 实施步骤3: 最终测试

一旦你的程序完成了,最后一步就是测试整个程序,确保它能如期运行。如果不能正常工作,就修复它。

# 编写程序的建议

`让你的程序简单起步`。通常新手程序员对自己写的程序都有一个宏大的愿景。"我想写一个角色扮演游戏,有图像和声音,有随机的怪物和地牢,有一个小镇,你可以去卖你在地牢里面找到的物品。"如果你开始的时候试图写一个很复杂的东西,你会变得不知所措,对你缺乏进展感到气馁。相反,让你的第一个目标尽可能的简单,是你绝对可以达到的东西。比如: "我希望在屏幕上显示一个二维物体"。

`随着时间的推移增加新功能`。一旦完成了简单的程序,并且运行良好,那么你就可以给它添加新功能。比如,一旦你可以显示二维物体,添加一个可以四处走动的角色。一旦你可以四处走动,就添加可以阻碍你前进的墙壁。一旦你有了墙,就用它们建造一个简单的城镇。一旦你有了城镇,就添加商人。通过逐步添加每个功能,你的程序将逐渐变得复杂,而不会在这个过程中搞垮你。

`每次专注于一个领域`,不要试图一次完成所有的代码,也不要将你的注意力分散在多个任务上。一次只专注于一个任务。有一个工作任务和五个还没有开始的任务比同时处理六个任务要好处理的多。如果你分散注意力,你更容易犯错和忘记重要的细节。

`边写边测试每段代码`。新手程序员往往会一次写完整个程序。然后当它们一次编译的时候,编译器会报告数百个错误。这不仅仅会让人望而生畏,如果你的代码不工作,可能很难找出原因。相反,写一段代码,然后立即编译和测试它。如果它不工作,你就会知道问题出在哪里,而且很容易修复。一旦你确定代码能够使用,就转到下一段,然后重复。可能需要较长的时间来完成代码的编写,但是当你完成后,整个代码应该可以工作,你就不需要花费两倍的时间来找出为什么不能工作了。

`不要投资于完善早期的代码`。一个功能(或者程序)的初稿很少是好的。此外,程序往往会随着时间的推移而发展,因为你增加了功能,找到了更好的组织方式。如果你过早地投资于打磨你的代码(添加大量的文档,完全符合最佳实践,进行优化),当需要修改代码的时候,你有可能失去所有投资。相反,让你的功能最小化工作,然后继续前进。当你对你的解决方案获得信心时,就要不断的打磨。不要追求完美 -- 重要的程序永远不会完美,总是有更多的东西可以改进它们。达到足够好的程度,然后继续前进。

大多数新的程序员会对这些步骤和建议进行捷径处理(因为这似乎是一个很大的工作,和/或者 它没有写代码那么有趣)。然而,对于任何重要的项目来说,遵循这些步骤肯定会为你节省很多时间。前期的一点规划可以节省很多最后的调试工作。

好消息是,一旦你适应了所有这些概念,它们就会开始对你更加自然。最终,你会达到这样的程序,即你可以在完全不需要任何预先规划的情况下编写整个函数。

Loading

0 comments on commit 82311d6

Please sign in to comment.