第九章 模块化开发
关键考点总览
跳转到:关键考点总览- 模块化程序设计的优缺点
- 模块划分的原则与方法
- 头文件的作用与设计(#ifndef/#define/#endif 防止头文件重复包含,避免同一编译单元内重复定义)
- 库的概念与设计(接口 .h + 实现 .cpp)
- 枚举类型在模块间共享数据类型的应用
- 静态全局变量作为模块内部状态
- 石头剪刀布游戏的模块划分示例
- 随机数库的设计与实现
- 龟兔赛跑用随机数模拟概率的方法
详细知识点
跳转到:详细知识点9.1 模块化程序设计
跳转到:9.1 模块化程序设计定义/语法
跳转到:定义/语法- 模块化开发:把一个整体任务拆分成若干个子任务,每个子任务当成独立程序编写,作为软件的一个模块;不同模块之间只通过接口相连,不需要了解其他模块内部的运作情况。 [来源:第9章 p5,模块化开发的诞生]
规则/规范
跳转到:规则/规范-
模块化优点:
- 方便单个模块功能调试、升级
- 多人协作互不干扰
- 方便模块间组合,代码更简洁
- 模块可以多次重用,减少工作量 [来源:第9章 p6,为什么用模块化软件架构]
-
模块化弊端:
- 可能引入额外开销:抽象层增多、间接调用增加;若模块边界跨进程/网络(微服务、RPC),还会叠加序列化与通信开销(同进程内函数调用开销通常很小)。
- 依赖风险:被依赖模块“下架/取消发布、接口变更、版本不兼容”等都会导致构建或运行失败(left-pad 属于依赖包被取消发布引发的大范围构建失败案例)。 [来源:第9章 p7,模块化开发的弊端]
易错点与辨析
跳转到:易错点与辨析- ⚠️ left-pad 事件教训(2016年):在依赖树极深的生态里,一个极小的依赖包(left-pad)被作者从 npm 取消发布(unpublish)就能造成大量项目构建/安装失败;该事件凸显了依赖治理(版本、镜像、锁文件等)的重要性。 [来源:第9章 p7,Notes]
- 生态改进措施:
- 不可变发布(以 npm 为例):注册表数据不可变,package@version 一旦发布内容不能被覆盖修改,且同名同版本永不可再次发布(即使后来 unpublish);但 unpublish 在满足官方条件时允许,否则更推荐 deprecate 而不是直接下架。
- 命名空间支持:引入
@scope/package格式 - 锁定依赖版本:使用
package-lock.json或yarn.lock固定依赖树 [来源:第9章 p13,left-pad 事件的影响]
9.2 模块划分
跳转到:9.2 模块划分规则/规范
跳转到:规则/规范-
模块划分原则(没有唯一“标准答案”,但应遵循高内聚、低耦合、单一职责、信息隐藏/接口稳定等工程原则):
- 先把总需求分解成子任务
- 将子任务抽象为模块(通常由接口 .h + 实现 .cpp 组成;也可能由多个源文件/类共同实现)
- 模块内部将完成某一具体功能的指令序列封装到函数中
- 安排不同模块之间的连接关系,集成联调 [来源:第9章 p23,模块划分]
-
划分依据:功能强关联的指令序列放在一个模块中,联系不紧密、相互之间没有太多数据传递的指令序列放到不同模块中。 [来源:第9章 p23,Notes]
代码模式
跳转到:代码模式- 石头剪刀布游戏模块划分示例:
- 主模块
main.cpp:main() - 获取选择模块
select.cpp:selection_by_player()和selection_by_machine() - 比较模块
compare.cpp:compare() - 输出模块
print.cpp:report(),prn_game_status()和prn_help()[来源:第9章 p25,模块划分]
- 主模块
9.3 头文件设计
跳转到:9.3 头文件设计定义/语法
跳转到:定义/语法-
头文件作用:模块和外界的接口,用来存放:
- 公用函数声明
- 常量和数据类型定义 [来源:第9章 p26,头文件]
-
#include 头文件本质是预处理阶段的“文本包含/展开”:它把头文件内容原样插入当前编译单元(translation unit);函数/变量的定义在各 .cpp 分别编译后,由链接器在链接阶段解析,与 .cpp 文件名是否同名无关。 [来源:第9章 p26,头文件]
规则/规范
跳转到:规则/规范- 防止头文件重复包含的预处理结构:
[来源:第9章 p36,头文件的设计]#ifndef 标识符#define 标识符// 头文件内容...#endif
代码模式
跳转到:代码模式-
枚举类型定义头文件示例:
p_r_s.h #ifndef p_r_s_h#define p_r_s_henum p_r_s {paper, rock, scissor, game, help, quit};enum outcome {win, lose, tie, error};#endif[来源:第9章 p36,重写 p_r_s.h]
-
函数声明头文件示例:
select.h #ifndef select_h#define select_h#include "p_r_s.h"p_r_s selection_by_player();p_r_s selection_by_machine();#endif[来源:第9章 p37,其他模块的头文件]
易错点与辨析
跳转到:易错点与辨析-
⚠️ 同一编译单元内多次 include 会造成类型重复定义:若某个 .cpp 通过不同路径(直接/间接)重复
#include "p_r_s.h",而p_r_s.h又缺少防重复包含保护,就会触发枚举/结构体等重复定义的编译错误。 [来源:第9章 p35,多次include造成类型重复定义] -
⚠️ 解决方法:使用
#ifndef/#define/#endif结构,ifndef检查该定义是否已出现,若已出现则跳过,否则执行define部分。 [来源:第9章 p36,Notes]
9.4 模块内部状态
跳转到:9.4 模块内部状态定义/语法
跳转到:定义/语法- 模块内部状态:对外部模块不可见、仅在当前 .cpp(编译单元)内共享的状态(通常由本模块内多个函数共同读写)。可用
static(内部链接)或匿名命名空间实现。 [来源:第9章 p32,print模块的设计与实现]
代码模式
跳转到:代码模式- 静态全局变量示例:
[来源:第9章 p33,print模块的设计与实现]
print.cpp // 模块的内部状态static int win_cnt = 0, lose_cnt = 0, tie_cnt = 0;
9.5 库的设计与实现
跳转到:9.5 库的设计与实现定义/语法
跳转到:定义/语法-
库:一些模块/函数的集合体。
- 特点:内部互动不多,功能有所关联
- 如标准库
iostream包含输入输出功能,cmath包含数学运算函数 [来源:第9章 p39,设计自己的库]
-
库的组成:
- 库的接口(头文件 .h):库的用户必须了解的内容,包括函数原型、符号常量和自定义类型
- 具体实现(源文件 .cpp):函数的具体实现代码 [来源:第9章 p40,库的设计]
规则/规范
跳转到:规则/规范-
(常见约定)库的头文件与实现文件常使用相同“基名”:如
random.h+random.cpp,便于维护与定位;但这只是命名惯例,不是语法/编译器强制要求,一个库也可能包含多个 .cpp。 [来源:第9章 p41,库的实现] -
库的设计要考虑通用性:库函数的功能是从一类相似的应用中抽象概括而来,要考虑尽可能广泛的适用范围。 [来源:第9章 p39,设计自己的库]
-
实现文件格式要求:
- 注释:简单介绍库的功能
#include此 cpp 文件所需的头文件,便于编译器检查函数定义和原型声明的一致性- 每个函数的实现代码(前面必须有注释) [来源:第9章 p41,库的实现]
代码模式
跳转到:代码模式-
随机数库接口设计:
random.h #ifndef random_h#define random_h// 函数:RandomInit// 用法:RandomInit()// 作用:此函数初始化随机数种子void RandomInit();// 函数:RandomInteger// 用法:n = RandomInteger(low, high)// 作用:此函数返回一个low到high之间的随机数,包括low和highint RandomInteger(int low, int high);#endif[来源:第9章 p43,库接口的设计]
-
随机数库实现:
random.cpp #include <cstdlib>#include <ctime>#include "random.h"void RandomInit(){srand(time(NULL));}int RandomInteger(int low, int high){return (low + (high - low + 1) * rand() / (RAND_MAX + 1));}[来源:第9章 p44,函数实现]
9.6 随机数模拟概率
跳转到:9.6 随机数模拟概率执行逻辑
跳转到:执行逻辑- 用随机数模拟概率的方法:生成 0-9 之间的随机数,根据概率分配区间:
- 50% 概率 → 随机数 0-4(5个数)
- 20% 概率 → 随机数 5-6(2个数)
- 30% 概率 → 随机数 7-9(3个数) [来源:第9章 p50,如何用随机数模拟概率]
代码模式
跳转到:代码模式-
龟兔赛跑中乌龟移动函数:
int move_tortoise(){int probability = RandomInteger(0, 9);if (probability < 5) return 3; // 快走 50%else if (probability < 7) return -6; // 后滑 20%else return 1; // 慢走 30%}[来源:第9章 p51,Move模块]
-
龟兔赛跑中兔子移动函数:
int move_rabbit(){int probability = RandomInteger(0, 9);if (probability < 2) return 0; // 睡觉 20%else if (probability < 4) return -9; // 大后滑 20%else if (probability < 5) return 14; // 快走 10%else if (probability < 8) return 3; // 小步跳 30%else return -2; // 慢后滑 20%}[来源:第9章 p51,Move模块]
9.7 例题结论
跳转到:9.7 例题结论例题结论
跳转到:例题结论-
【石头剪刀布游戏设计要点】
- 用枚举类型
p_r_s表示出拳选项:paper, rock, scissor, game, help, quit - 用枚举类型
outcome表示结果:win, lose, tie, error - 模块间共享类型定义放在公共头文件中
- 使用
rand() % 3生成 0-2 的随机数模拟计算机出拳 - 累计战绩用模块内部静态变量保存 [来源:第9章 p27-34,石头剪刀布完整示例]
- 用枚举类型
-
【龟兔赛跑程序结构】
- 主循环:
while (rabbit < RACE_END && tortoise < RACE_END) - 每轮更新位置:
tortoise += move_tortoise(); rabbit += move_rabbit(); - 使用
RandomInit()初始化随机数种子 - 胜负判断:按“是否到达终点(>= RACE_END)”判定;若同一回合双方都到达则平局,否则先到达(或到达者)获胜(不要仅用
rabbit > tortoise直接比较位置)。 [来源:第9章 p48-53,龟兔赛跑示例]
- 主循环:
9.8 团队管理(扩展知识)
跳转到:9.8 团队管理(扩展知识)规则/规范
跳转到:规则/规范-
两张披萨原则(Jeff Bezos / Amazon):团队应小到“两张披萨能喂饱”,常见表述为理想少于 10 人(不同资料会给出不同范围,但核心是减少沟通链路与官僚成本)。 [来源:第9章 p14,模块开发团队管理]
-
小团队优势(经验规律/定性结论):
- 人数更少通常意味着沟通链路更短、决策更快、责任更清晰;
- 但也可能带来资源不足、跨团队对齐成本上升、重复建设等问题;
- 如需引用定量数据,请补充可追溯出处(报告/论文/链接)以便核验。 [来源:第9章 p21,管理效果(请以课程材料原文为准)]
总结要点(来自 PPT 总结页)
跳转到:总结要点(来自 PPT 总结页)- 学会利用模块化程序设计技术来解决一个大问题
- 学会在模块中保存内部状态(使用 static 全局变量)
- 学会从程序中抽取出库以及如何设计和使用库 [来源:第9章 p54,总结]