跳转到内容

第九章 模块化开发

  1. 模块化程序设计的优缺点
  2. 模块划分的原则与方法
  3. 头文件的作用与设计(#ifndef/#define/#endif 防止头文件重复包含,避免同一编译单元内重复定义)
  4. 库的概念与设计(接口 .h + 实现 .cpp)
  5. 枚举类型在模块间共享数据类型的应用
  6. 静态全局变量作为模块内部状态
  7. 石头剪刀布游戏的模块划分示例
  8. 随机数库的设计与实现
  9. 龟兔赛跑用随机数模拟概率的方法

9.1 模块化程序设计

跳转到:9.1 模块化程序设计
  • 模块化开发:把一个整体任务拆分成若干个子任务,每个子任务当成独立程序编写,作为软件的一个模块;不同模块之间只通过接口相连,不需要了解其他模块内部的运作情况。 [来源:第9章 p5,模块化开发的诞生]
  • 模块化优点:

    1. 方便单个模块功能调试、升级
    2. 多人协作互不干扰
    3. 方便模块间组合,代码更简洁
    4. 模块可以多次重用,减少工作量 [来源:第9章 p6,为什么用模块化软件架构]
  • 模块化弊端:

    1. 可能引入额外开销:抽象层增多、间接调用增加;若模块边界跨进程/网络(微服务、RPC),还会叠加序列化与通信开销(同进程内函数调用开销通常很小)。
    2. 依赖风险:被依赖模块“下架/取消发布、接口变更、版本不兼容”等都会导致构建或运行失败(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 事件的影响]

  • 模块划分原则(没有唯一“标准答案”,但应遵循高内聚、低耦合、单一职责、信息隐藏/接口稳定等工程原则):

    1. 先把总需求分解成子任务
    2. 将子任务抽象为模块(通常由接口 .h + 实现 .cpp 组成;也可能由多个源文件/类共同实现)
    3. 模块内部将完成某一具体功能的指令序列封装到函数中
    4. 安排不同模块之间的连接关系,集成联调 [来源:第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,模块划分]

  • 头文件作用:模块和外界的接口,用来存放:

    1. 公用函数声明
    2. 常量和数据类型定义 [来源:第9章 p26,头文件]
  • #include 头文件本质是预处理阶段的“文本包含/展开”:它把头文件内容原样插入当前编译单元(translation unit);函数/变量的定义在各 .cpp 分别编译后,由链接器在链接阶段解析,与 .cpp 文件名是否同名无关。 [来源:第9章 p26,头文件]

  • 防止头文件重复包含的预处理结构:
    #ifndef 标识符
    #define 标识符
    // 头文件内容...
    #endif
    [来源:第9章 p36,头文件的设计]
  • 枚举类型定义头文件示例:

    p_r_s.h
    #ifndef p_r_s_h
    #define p_r_s_h
    enum 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]


  • 模块内部状态:对外部模块不可见、仅在当前 .cpp(编译单元)内共享的状态(通常由本模块内多个函数共同读写)。可用 static(内部链接)或匿名命名空间实现。 [来源:第9章 p32,print模块的设计与实现]
  • 静态全局变量示例:
    print.cpp
    // 模块的内部状态
    static int win_cnt = 0, lose_cnt = 0, tie_cnt = 0;
    [来源:第9章 p33,print模块的设计与实现]

9.5 库的设计与实现

跳转到:9.5 库的设计与实现
  • 库:一些模块/函数的集合体。

    • 特点:内部互动不多,功能有所关联
    • 如标准库 iostream 包含输入输出功能,cmath 包含数学运算函数 [来源:第9章 p39,设计自己的库]
  • 库的组成:

    1. 库的接口(头文件 .h):库的用户必须了解的内容,包括函数原型、符号常量和自定义类型
    2. 具体实现(源文件 .cpp):函数的具体实现代码 [来源:第9章 p40,库的设计]
  • (常见约定)库的头文件与实现文件常使用相同“基名”:如 random.h + random.cpp,便于维护与定位;但这只是命名惯例,不是语法/编译器强制要求,一个库也可能包含多个 .cpp。 [来源:第9章 p41,库的实现]

  • 库的设计要考虑通用性:库函数的功能是从一类相似的应用中抽象概括而来,要考虑尽可能广泛的适用范围。 [来源:第9章 p39,设计自己的库]

  • 实现文件格式要求:

    1. 注释:简单介绍库的功能
    2. #include 此 cpp 文件所需的头文件,便于编译器检查函数定义和原型声明的一致性
    3. 每个函数的实现代码(前面必须有注释) [来源:第9章 p41,库的实现]
  • 随机数库接口设计:

    random.h
    #ifndef random_h
    #define random_h
    // 函数:RandomInit
    // 用法:RandomInit()
    // 作用:此函数初始化随机数种子
    void RandomInit();
    // 函数:RandomInteger
    // 用法:n = RandomInteger(low, high)
    // 作用:此函数返回一个low到high之间的随机数,包括low和high
    int 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模块]


  • 【石头剪刀布游戏设计要点】

    • 用枚举类型 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 总结页)
  1. 学会利用模块化程序设计技术来解决一个大问题
  2. 学会在模块中保存内部状态(使用 static 全局变量)
  3. 学会从程序中抽取出库以及如何设计和使用库 [来源:第9章 p54,总结]