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:实现每个函数

在此步骤中,针对每个函数,你将完成三项工作:

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

若函数划分足够精细,每个函数都应相当简单明了。若某个函数仍显得过于复杂,可能需要分解为更易实现的子函数(也可能是操作顺序有误,需重新审视事件序列)。

让我们从计算器示例中的第一个函数开始:

#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课——在问题爆发前发现隐患。

A complex system that works is invariably found to have evolved from a simple system that worked
—John Gall, Systemantics: How Systems Really Work and How They Fail p. 71

结论

许多新手程序员会省略设计过程(因为这看似工作量巨大且不如编写代码有趣)。然而对于任何非简单项目而言,遵循这些步骤最终将为您节省大量时间。前期稍作规划,后期便能省去大量调试工作。

关键洞察
在前期花些时间思考如何设计程序结构,将带来更优质的代码,并减少查找和修复错误所耗费的时间。

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

随着你对这些概念和技巧越来越熟悉,它们将逐渐成为你的本能反应。最终你会达到这样的境界:只需极少的预先规划,就能写出完整的函数(甚至简短程序)。

posted @ 2026-02-10 06:32  游翔  阅读(0)  评论(0)    收藏  举报