函数(二)
void 与模块化设计:大任务拆解与分工协作

从多任务终端菜单到经典结构化分解,理解“动作型函数、单一职责与自顶向下设计”

本课核心问题

社区慢病管理系统终端要完成更多随访任务,代码全塞进 main() 会怎样?

已有基础

上一课时已掌握数值计算型函数(如 calcBMI),实现公式的封装与复用。

新增挑战

新增“慢病分级评估、综合报告输出、居民交互菜单”:如何避免 main() 臃肿不堪?

解决机制

使用 void 动作型函数与单一职责原则,实现自顶向下分解与模块化装配。

第2课时学习目标

知识目标

  • 理解无返回值函数 void 的含义、使用规则与调用禁忌
  • 掌握数值计算型、条件判断型与 void 动作型函数的区别
  • 理解结构化程序设计的“自顶向下、逐步细化”与“单一职责”原则

能力目标

  • 能将多步骤复杂业务拆解为职责独立的函数群
  • 能绘制函数调用拓扑树,并将函数与 switch-case 菜单结构组合
  • 能借助 AI 协作审查模块接口的职责重叠与隐藏耦合,设计防御性边界
5.2 / 无返回值函数 void
有些函数负责“计算”,
有些函数负责“执行动作”
当函数的目标是向屏幕输出、改变硬件状态或执行控制流程,不需要带回数值时,用 void。
void printReport 独立语句调用 return; 提前退出

为什么需要无返回值函数?

数据计算:需要返回结果

calcBMI(70.0, 1.75) 计算后需要把 22.86 交代给主调程序,因此返回 double。

报告展示:执行输出动作

printReport(22.86) 的任务是向屏幕打印格式化慢病随访分析报告,并不产生一个需要继续参与运算的数字。

void printReport(double bmi)
{
    if (bmi <= 0) {
        printf("Error: invalid data\n");
        return;    /* Return early */
    }
    printf("Report: BMI = %.2f\n", bmi);
}
语法关键:void 表明该函数“不返回任何值”。末尾的 return; 不带表达式,用于立即跳出函数。

两类函数,调用语法截然不同

有返回值的函数(double / int)

double b = calcBMI(70.0, 1.75);
if (isSquare(16)) ...
printf("%f", cylinder(2, 3));

✓ 必须或通常作为表达式的一部分出现,可参与赋值、算术或打印。

无返回值的函数(void)

printReport(b);   /* Valid call */

/* Common syntax errors: */
x = printReport(b);              /* Error! */
printf("%d", printReport(b));    /* Error! */

✗ 只能作为一条独立的语句执行,绝不能出现在需要数值的表达式中!

有返回值函数 vs void 函数对比总结

对比维度 有返回值函数(double/int 等) 无返回值函数(void)
核心使命 计算或判断:向主调函数交付一个数据 执行操作:屏幕输出、格式化打印、状态修改
首部返回类型 具体数据类型,如 double、int 关键字 void
return 语句 必须写 return expr;,类型需匹配 可省略;若写只能是不带表达式的 return;
调用形式 变量接收 v = f(...) 或作为表达式因子 独立语句 f(...);

🤖 AI 协作互动:void 函数的语法陷阱与编译诊断

我正在学习 C 语言的 void 函数。请帮我设计 3 个故意写错的代码片段(分别展示:把 void 函数赋值给变量、在 void 函数体内部写 return 1、将 void 函数传给 printf 作为参数)。请列出对应的 GCC 编译器报错信息,并说明为什么 C 语法不允许这么做。

💡 课堂互动:同桌两人在 IDE 中敲入报错片段,观察编译器警告信息,验证与 AI 解释的一致性。

5.2 / 模块化程序设计
大任务拆解为小函数:
高内聚、低耦合、单一职责
贯穿案例升级:将社区慢病健康终端拆解为“数据录入、BMI计算、慢病分级、报告展示与菜单调度”。
单一职责 功能树 自顶向下 switch 菜单

大任务审视:三个步骤是一件事吗?

A · 数值计算

weight, height → bmi

纯数学计算,不受界面或语言文本影响。

B · 慢病风险分级

bmi → band (0/1/2)

社区慢病评判标准(偏瘦/正常/超重肥胖),规则变动仅在此调整。

C · 报告打印

band → 屏幕排版

终端人机交互,负责控制台美化与健康指导建议展示。

单一职责原则(Single Responsibility):一个函数只专心把一件事做好!
若慢病分级标准改变,只需修改 B;若计算公式改变,只需修改 A;若输出格式升级,只需修改 C!

第一步:为各个模块定义清晰接口

函数名称 形参列表(输入) 返回类型(输出) 职责说明
calcBMI double weight, double height double 计算体质指数 BMI,非法输入返回 -1.0
classifyBMI double bmi int 将 BMI 映射为慢病风险等级:0偏瘦、1正常、2超重肥胖
printReport double bmi void 调用 classifyBMI 并打印完整图文报告与健康建议
int classifyBMI(double bmi)
{
    if (bmi <= 0)     return -1;   /* Invalid data */
    if (bmi < 18.5)   return 0;    /* Underweight */
    if (bmi <= 23.9)  return 1;    /* Normal */
    return 2;                      /* Overweight or obese */
}

从“代码堆积”到层次分明的功能树

调度层:main()
交互控制、菜单驱动、调用业务功能
→
业务展示层:printReport()
协调分类与控制台图文排版
→
算法层
calcBMI()
规则层
classifyBMI()
模块化层次结构图

教材经典结构化设计:自顶向下、逐步求精、模块划分

main 负责流程协调,不塞入细节

void showMenu(void);
void handleMeasure(void);

int main(void)
{
    int choice;
    do {
        showMenu();
        scanf("%d", &choice);
        switch (choice) {
            case 1:  handleMeasure(); break;
            case 0:  printf("Exiting system.\n"); break;
            default: printf("Invalid choice, please retry.\n");
        }
    } while (choice != 0);
    return 0;
}

void showMenu(void) {
    printf("\n=== Chronic Disease Health System ===\n1. Input weight/height and evaluate BMI\n0. Exit\nChoice: ");
}
主函数是一张清晰的“组织架构图”:扫一眼 main(),就能完全理解系统的宏观运行流程!

AI 协作互动:审查模块划分与接口正交性

🤖 AI 协作互动:让 AI 诊断函数职责重叠与隐藏耦合

我正在用 C 语言为社区慢病健康管理终端划分函数:calcBMI(weight, height) 负责计算;classifyBMI(bmi) 负责慢病风险区间分类;printReport(bmi) 负责控制台打印;handleMeasure() 负责交互输入与串联。请帮我审查这份设计:是否有某个函数承担了过多非核心职责?如果未来我想把该程序移植为手机 App(界面不再是控制台字符终端),现有的 calcBMI 和 classifyBMI 能否不改一行代码直接复用?

💡 课堂互动:学生体会“计算与 IO 解耦”的深刻内涵:纯逻辑函数具有极高的跨平台可移植性。

教材例5-6
数字金字塔:
动作型函数的经典实战
定义和调用 void pyramid(int n) 输出对称数字图案,体会“图形化任务的函数封装”。
pyramid void 函数 循环嵌套 字符对齐

教材例5-6 题面描述

📋 题目要求

输入一个正整数 \(n\)(\(1 \le n \le 9\)),输出 \(n\) 行数字金字塔。
要求定义和调用函数 void pyramid(int n) 输出金字塔。

数字金字塔效果

金字塔排版规律:空格与数字分解

以 \(n = 5\) 为例,仔细观察第 \(i\) 行(\(i = 1 \dots n\))的字符构成:

行号 \(i\) 前置空格数(\(n - i\)) 打印内容(数字后带空格) 循环逻辑拆解
第 1 行 \(5 - 1 = 4\) 个空格 1 先循环打 4 个空格,再打 1 次 "1 "
第 2 行 \(5 - 2 = 3\) 个空格 2 2 先循环打 3 个空格,再打 2 次 "2 "
第 3 行 \(5 - 3 = 2\) 个空格 3 3 3 先循环打 2 个空格,再打 3 次 "3 "
第 \(i\) 行 \(n - i\) 个空格 \(i\) 次 "%d ", i 内层双循环:先输出空格,后输出数字

教材例5-6:函数实现与思考

#include <stdio.h>

void pyramid(int n);

int main(void)
{
    int n;
    scanf("%d", &n);
    pyramid(n);
    return 0;
}

void pyramid(int n)
{
    int i, j;
    for (i = 1; i <= n; i++) {
        for (j = 1; j <= n - i; j++)
            putchar(' ');
        for (j = 1; j <= i; j++)
            printf("%d ", i);
        putchar('\n');
    }
}

深度自问自答

  • 问:为什么定义成 void?
    因为该函数的核心任务是控制字符终端的光标与输出,没有计算出一个数值需要交还给 main。
  • 问:main 中能写 int res = pyramid(5); 吗?
    绝对不能!编译器会报错 void value not ignored。

鲁棒性思考与 AI 协作互动

边界异常分析

如果用户输入 \(n \le 0\),循环不执行,控制台无任何反应;如果输入 \(n \ge 10\),双位数宽度变为 2,金字塔斜边将严重错位歪斜!

契约式防护建议

在函数入口加入卫语句:

if (n < 1 || n > 9) {
    printf("Please enter an integer between 1 and 9\n");
    return;
}

🤖 AI 协作互动:图案打印函数的对齐与边界扩展

在 C 语言数字金字塔函数 void pyramid(int n) 中,如果我想让它支持 n=1 到 15,当数字变成两位数(如 10..15)时,原本每行打印的空格和数字格式 printf("%d ", i) 会导致右侧金字塔歪斜。如何利用 printf 字段宽度格式(如 %2d)保持两位数金字塔的完美对称?请给出推演。

💡 课堂互动:观察 AI 如何分析列宽格式对图形对齐的影响,培养细致的界面控制能力。

教材例5-4 · 例5-5
核心算法函数的健壮性:
不仅要能跑,更要能防错
最大公约数 gcd(m, n) 与素数判断 prime(m) 的边界与除零审查。
gcd prime 辗转相除 除零防御 边界测试

教材例5-4 题面描述与算法流程

📋 教材例5-4 题目要求

输入两个正整数 \(m\) 和 \(n\),定义并调用函数 gcd(m, n) 计算它们的最大公约数(Greatest Common Divisor)。

辗转相除法(欧几里得算法):
用 \(m\) 除以 \(n\) 得余数 \(r\);若 \(r = 0\),则 \(n\) 即为最大公约数;否则令 \(m = n, n = r\),继续求余。
辗转相除法流程图

辗转相除实现与致命除零漏洞

int gcd(int m, int n)
{
    int r;
    r = m % n;
    while (r != 0) {
        m = n;
        n = r;
        r = m % n;
    }
    return n;
}

🚨 致命漏洞检验

  • 若输入 gcd(18, 12):返回 6(正常)。
  • 若输入 gcd(12, 18):第一轮 \(12 \% 18 = 12\),自动互换为 \(18 \% 12\)(正常)。
  • 若调用者传入 gcd(0, 0) 或 gcd(18, 0):
    第 4 行 r = m % n; 将直接触发除以 0 致命崩溃(Floating Point Exception / SIGFPE)!
健壮性改造:函数入口必须先拦截非法值:if (n <= 0 || m <= 0) return -1;

教材例5-5:素数判断函数 prime(m)

📋 教材例5-5 题目要求

定义函数 int prime(int m),当 \(m\) 是素数时返回 1,否则返回 0。

int prime(int m)
{
    int i;
    if (m <= 1) return 0;   /* Numbers <= 1 are not prime */
    for (i = 2; i <= m / 2; i++) {
        if (m % i == 0)
            return 0;        /* Found factor, return 0 early */
    }
    return 1;                /* Loop finished, is prime */
}

边界测试清单

输入 \(m\)预期返回测试目的
101 既不是素数也不是合数
21最小且唯一的偶素数
40最小偶合数,首轮退出
971较大两位素数,遍历完毕
-70负数边界拦截

参数安全再反思:格雷戈里公式 funpi(e)

函数的职责

负责迭代累加并由 fabs(item) >= e 控制精度收敛,向调用者返回 \(\pi\) 的近似值。

参数隐患:若传入 \(e \le 0\)?

若调用者传入 \(e = 0\),浮点截断导致不可预知循环;若传入 \(e < 0\),fabs(item) >= e 恒成立,直接引发死循环!

契约式设计(Design by Contract):函数设计者绝不能假设“调用者一定会乖乖输入合法数据”!

🤖 AI 协作互动:经典算法函数边界漏洞自动化审计

请扮演一位严苛的 C 语言单元测试架构师。针对三个经典教材函数:int gcd(int m, int n)、int prime(int m)、double funpi(double e),分别列出 2 组最可能让原程序陷入除零、死循环或逻辑误判的“恶魔输入”,并提供入口防御代码。

💡 课堂互动:同桌核对 AI 提出的恶魔测试用例,在自己脑海中单步推演各分支的防御表现。

第2课时后测与总结
从“混乱杂糅”
到“结构清晰的软件架构”
通过返回类型辨析与模块维护性诊断,掌握结构化程序设计的底层逻辑。
返回类型辨析 维护性诊断 知识地图 下节悬念

课堂后测:两道出口诊断题

出口题一:为任务选择正确的函数接口

任务推荐首部原因
求两数最大公约数int gcd(int, int)需要取回一个整数结果
打印数字金字塔void pyramid(int)执行屏幕输出动作
判断是否为素数int prime(int)返回真假(1/0)标识

追问:如果把 pyramid 强行定义为 int 并返回 0,有什么代价?(无谓的返回值混淆调用意图)。

出口题二:哪种架构更容易维护?

方案 A(超大函数)
在一个 200 行函数里塞满 scanf、公式计算、打印、菜单。改一处可能全盘出错。
方案 B(职责解耦)
计算函数 + 分类函数 + 报表函数 + 菜单主控。接口独立,易测易改易复用。
结论:坚定选择 B!局部修改不扩散、多人协作无冲突。

本课核心思维地图:结构化设计四原则

1. 单一职责
One Job, One Function
→
2. 严选接口
按需选择数值/void
→
3. 自顶向下
main 协调,函数执行
→
4. 防御边界
卫语句拒绝非法输入

数值计算型

calcBMI、cylinder、gcd:纯算术,返回值参与后续处理。

条件判断型

isSquare、prime:返回 1/0,直接用作 if 判定条件。

动作执行型

printReport、pyramid、showMenu:void 类型,完成 IO 输出。

课后迁移练习与下节预告

编程实战与巩固

  • 上机编写运行教材例5-6(数字金字塔)与例5-4(最大公约数)
  • 为 gcd(m, n) 和 funpi(e) 增加防除零、防死循环卫语句
  • 为社区慢病健康管理系统实现完整的 switch-case 终端多功能交互系统

下节悬念:变量的作用域与生命期

  • 在函数 A 里定义的变量 x,函数 B 能直接读取或修改吗?
  • 函数执行完毕后,它的局部变量在内存中去哪儿了?
  • 如果多个函数想共享同一批测量数据,除了不断传参,还有什么机制?

函数让代码秩序井然,变量让数据各就其位。