2-13 如何设计你的第一个程序
既然你已经掌握了程序编写的初步知识,现在让我们更深入地探讨如何设计程序。
当你坐下来编写程序时,通常心中已有某个想法,希望通过程序实现它。新手程序员常为如何将想法转化为实际代码而困扰。但事实上,你早已具备解决问题的诸多技能——这些能力源于日常生活。
最关键(也是最难做到)的是:在编写代码前先设计好程序。编程在许多方面类似于建筑设计。试想若不遵循建筑图纸就动工盖房会怎样?除非天赋异禀,否则很可能建成问题丛生的房屋:墙面歪斜、屋顶漏水等等…… 同理,若未制定完善计划就贸然编程,你很可能发现代码漏洞百出,不得不耗费大量时间修复那些本可通过提前规划完全避免的问题。
前期稍作规划,长远来看既能节省时间又能避免挫折。
本节课我们将阐述一种通用方法,教你如何将创意转化为简单实用的程序。
设计步骤1:明确目标
要编写成功的程序,首先需明确目标。理想情况下,应能用一两句话概括目标。通常将目标表述为面向用户的成果更为有效。例如:
- 允许用户整理姓名与对应电话号码的列表。
- 生成随机地牢,创造外观奇特的洞穴场景。
- 生成高股息股票的投资推荐清单。
- 模拟从塔顶坠落的球体落地所需时间。
尽管此步骤看似显而易见,却至关重要。最糟糕的情况莫过于编写出与预期(或老板要求)完全背离的程序!
设计步骤2:定义需求
虽然明确问题能帮助你确定期望结果,但仍显模糊。下一步需思考需求。
需求这个术语既指解决方案必须遵守的约束条件(如预算、时间线、空间、内存等),也指程序为满足用户需求必须具备的功能。请注意,需求同样应聚焦于“做什么”而非“如何做”。
例如:
- 电话号码应被保存,以便后续调用。
- 随机生成的地下城必须始终包含从入口通往出口的路线。
- 股票推荐应基于历史价格数据。
- 用户应能输入塔楼高度。
- 需在7天内交付可测试版本。
- 程序应在用户提交请求后10秒内输出结果。
- 程序崩溃率应低于0.1%。
单一问题可能衍生多项需求,且解决方案必须满足所有需求方为“完成”。
设计步骤3:定义工具、目标及备份方案
当你成为资深程序员时,此时通常还需完成其他步骤,包括:
- 确定程序运行的目标架构和/或操作系统。
- 确定将使用的工具集。
- 确定是独立编程还是团队协作。
- 制定测试/反馈/发布策略。
- 确定代码备份方案。
但作为新手程序员,这些问题的答案通常很简单:你正在为个人使用编写程序,独自在自有系统上操作,使用下载的集成开发环境,且代码可能仅供你个人使用。这使得事情变得简单。
不过,若要处理非简单复杂度的项目,你仍需制定代码备份计划。仅将源代码目录压缩或复制到同一存储设备上的其他位置远远不够——一旦存储设备损坏或数据损毁,你将失去所有内容。复制或压缩到可移动存储设备(如U盘)虽更优,但遭遇盗窃、火灾或重大自然灾害时仍可能全盘尽失。
最佳备份策略是将代码副本存放在物理位置不同的机器上。实现方式多种多样:压缩后通过邮件发送给自己、上传至云存储服务(如Dropbox)、使用文件传输协议(如SFTP)上传至自有服务器,或采用部署在其他设备/云端的版本控制系统(如GitHub)。版本控制系统的额外优势在于,不仅能恢复文件,还能将文件回滚至历史版本。
设计步骤4:将难题分解为易解问题
现实生活中,我们常需处理极其复杂的任务。试图破解这些任务往往充满挑战。此时,我们通常采用自上而下top down的问题解决方法。即不直接解决单一复杂任务,而是将其拆解为多个子任务,每个子任务单独处理时都更容易解决。若子任务仍难以解决,可继续拆分。通过持续将复杂任务分解为简单任务,最终每个子任务都将变得可控,甚至变得简单。
让我们通过一个例子来说明。假设我们要打扫房子,当前任务层次结构如下:
- 打扫房屋
一次性完成全屋清洁任务量较大,因此将其分解为子任务:
- 打扫房屋
- 吸尘地毯
- 清洁卫生间
- 清理厨房
这样更易于管理,因为现在我们可以专注于每个子任务。但其中部分任务仍可进一步细分:
- 打扫房屋
- 吸尘地毯
- 清洁卫生间
- 刷洗马桶(好恶心!)
- 清洗水槽
- 清洁厨房
- 清理台面
- 擦拭台面
- 刷洗水槽
- 倒垃圾
现在我们建立了任务层级,每个子任务都不算太难。通过完成这些相对可控的子项,就能完成打扫整个房屋这个更困难的整体任务。
另一种建立任务层级的方法是从自底向上bottom up。此法从简单任务清单出发,通过分组构建层级结构。
以工作日通勤为例,假设我们要解决“上班”问题。若问清晨从起床到上班的流程,可能列出以下清单:
- 挑选衣物
- 穿衣
- 吃早餐
- 通勤
- 刷牙
- 起床
- 准备早餐
- 骑自行车
- 淋浴
运用自下而上法,可通过归纳相似性将任务重组为层次结构:
- 从起床到上班
- 卧室相关事项
- 关闹钟
- 起床
- 挑选衣服
- 浴室相关事项
- 淋浴
- 穿衣
- 刷牙
- 早餐相关事项
- 冲泡咖啡或茶
- 食用麦片
- 交通事项
- 骑自行车
- 通勤上班
- 卧室相关事项
事实证明,这类任务层次结构在编程中极具价值——一旦建立任务层次,便等同于定义了程序整体架构。顶层任务(如“打扫房屋”或“上班”)即成为main()函数(因其代表核心待解决问题)。子项则转化为程序中的函数。
若发现某项任务(函数)难以实现,只需将其拆分为多个子项/子函数。最终你将达到这样的境界:程序中的每个函数都变得简单易行。
设计步骤5:确定事件顺序
既然程序框架已定,接下来要确定如何串联所有任务。第一步是确定将要执行的事件顺序。例如,早晨起床时,你会按什么顺序完成上述任务?可能如下所示:
- 卧室事项
- 浴室事项
- 早餐事项
- 出行事项
若编写计算器程序,操作顺序可能是:
- 获取用户输入的第一个数字
- 获取用户输入的运算符
- 获取用户输入的第二个数字
- 计算结果
- 输出结果
至此,我们已准备好进入实现阶段。
实施步骤1:规划主功能
现在我们可以开始实施了。上述序列可用于规划主程序框架。暂时不必考虑输入输出问题。
int main()
{
// doBedroomThings();
// doBathroomThings();
// doBreakfastThings();
// doTransportationThings();
return 0;
}
或者以计算器为例:
int main()
{
// Get first number from user
// getUserInput();
// Get mathematical operation from user
// getMathematicalOperation();
// Get second number from user
// getUserInput();
// Calculate result
// calculateResult();
// Print result
// printResult();
return 0;
}
请注意,若采用这种“大纲式”方法构建程序,由于函数定义尚未存在,函数调用将无法编译通过。一种解决方式是将函数调用注释掉,待实现函数定义后再取消注释(本文将演示此方法)。另一种方案是为函数创建空函数体(即占位函数),这样程序就能通过编译。
实施步骤2:实现每个函数
在此步骤中,针对每个函数,你将完成三项工作:
- 定义函数原型(输入和输出)
- 编写函数
- 测试函数
若函数划分足够精细,每个函数都应相当简单明了。若某个函数仍显得过于复杂,可能需要分解为更易实现的子函数(也可能是操作顺序有误,需重新审视事件序列)。
让我们从计算器示例中的第一个函数开始:
#include <iostream>
// Full implementation of the getUserInput function
int getUserInput()
{
std::cout << "Enter an integer: ";
int input{};
std::cin >> input;
return input;
}
int main()
{
// Get first number from user
int value{ getUserInput() }; // Note we've included code here to test the return value!
std::cout << value << '\n'; // debug code to ensure getUserInput() is working, we'll remove this later
// Get mathematical operation from user
// getMathematicalOperation();
// Get second number from user
// getUserInput();
// Calculate result
// calculateResult();
// Print result
// printResult();
return 0;
}
首先,我们确定getUserInput函数不接受任何参数,并将返回一个整数值给调用方。这体现在函数原型中:返回值为int且无参数。接着,我们编写了函数主体,仅包含四个简单的语句。最后,我们在main函数中实现了一些临时代码,用于测试getUserInput函数(包括其返回值)是否正常工作。
此时我们可以多次运行程序并输入不同值,确保程序行为符合预期。若发现异常,则问题必然出在刚编写的代码中。
当确认程序运行符合预期后,即可移除临时测试代码,转而实现下一个函数(getMathematicalOperation)。本课不会完成整个程序,因为我们需要先讲解其他主题。
请记住:不要一次性实现整个程序。应分阶段推进,每完成一步就进行测试,确保后续步骤的可靠性。
相关内容
我们在第9.1课——代码测试入门 中更详细地介绍了测试相关内容。
实施步骤3:最终测试
当程序“完成”后,最后一步是测试整个程序并确保其按预期运行。若出现故障,请及时修复。
编程建议
初学编程时,请保持程序简单。新手常怀抱宏大愿景,渴望程序实现所有功能:“我想编写一款带有图形、音效、随机怪物和地牢的角色扮演游戏,还需包含可供玩家出售地牢战利品的城镇”。若初始阶段就试图编写过于复杂的程序,你将因进展缓慢而感到不知所措、丧失信心。相反,请将首个目标设定得尽可能简单,确保它完全在你的能力范围内。例如:“我想在屏幕上显示一个二维场景”。
逐步添加功能。当基础程序稳定运行后,再逐步扩展功能。例如:先实现场景显示,再添加可移动角色;角色能移动后,加入阻碍前进的墙体;墙体搭建完成后,用它们构建简易城镇;城镇建成后,引入商人角色。通过逐层添加功能,程序将循序渐进地复杂化,避免开发过程中的压力过载。
每次专注一个领域。切勿试图一次性编写所有功能,也别让注意力分散在多项任务上。每次只专注一个任务。与其同时处理六个半成品任务,不如专注完成一个完整任务。注意力分散容易导致错误和遗漏关键细节。
随写随测。新手常一次性写完整个程序,首次编译时却遭遇数百条报错。这不仅令人沮丧,更难追溯代码故障根源。正确做法是:编写一段代码后立即编译测试。若出现问题,能精准定位故障点并轻松修复。确认代码运行正常后,再推进下一段代码并重复此流程。虽然整体编写耗时可能增加,但最终能确保程序完整运行,避免耗费双倍时间排查故障。
切勿在早期代码上追求完美。功能(或程序)的初稿鲜少臻于完善。况且程序往往随时间演进——随着功能扩展和架构优化,你总会发现更优方案。若过早投入代码打磨(添加大量文档、严格遵循最佳实践、进行优化),一旦需要修改代码,所有投入都可能付诸东流。相反,应先让功能基本可用,再继续推进。随着对解决方案的信心增强,再逐步打磨细节。不要追求完美——非平凡程序永远不可能完美,总有改进空间。达到“足够好”的程度即可继续前进。
优化可维护性而非性能。唐纳德·克努特有句名言:“过早优化是万恶之源”。新手程序员常耗费过多时间钻研代码的微优化(例如纠结两个语句哪个更快),但这通常无关紧要。真正的性能提升源于良好的程序结构、针对具体问题选用合适工具与功能,以及遵循最佳实践。额外投入的时间应用于提升代码可维护性:消除冗余代码,将冗长函数拆解为短小函数,用更优方案替换笨拙或难用的代码。最终将获得更易于后续改进和优化的代码(待实际确定优化需求后),并减少缺陷。更多建议详见第3.10课——在问题爆发前发现隐患。
结论
许多新手程序员会省略设计过程(因为这看似工作量巨大且不如编写代码有趣)。然而对于任何非简单项目而言,遵循这些步骤最终将为您节省大量时间。前期稍作规划,后期便能省去大量调试工作。
关键洞察
在前期花些时间思考如何设计程序结构,将带来更优质的代码,并减少查找和修复错误所耗费的时间。『I would say this is arguably the most important thing in programming and some of us, like me at first, took it for granted. —Reader Emeka Daniel, COMMENT ON LEARNCPP.COM 』
随着你对这些概念和技巧越来越熟悉,它们将逐渐成为你的本能反应。最终你会达到这样的境界:只需极少的预先规划,就能写出完整的函数(甚至简短程序)。

浙公网安备 33010602011771号