函数(一)
函数定义与调用:封装可复用的规则

从社区慢病管理(体质指数 BMI 计算)到经典数值与判断函数,理解“接口设计、值传递与边界测试”

本课核心问题

同样的体质指数(BMI)计算规则,要在程序里复制粘贴多少遍?

旧代码困境

同一公式在多处重复书写,一旦规则调整,极易漏改或前后不一致。

今天推进

把相对独立的计算或判断封装为自定义函数,实现“一次定义,多次调用”。

核心机制

函数声明、函数定义、函数调用,以及实参到形参的单向值传递。

第1课时学习目标

知识目标

  • 理解自定义函数的接口要素:函数名、形参列表、返回类型
  • 区分函数定义、函数原型声明与函数调用
  • 理解实参向形参的“按值传递(Pass by Value)”与 return 的作用

能力目标

  • 能将“根据身高与体重计算体质指数(BMI)”等慢病评估规则编写为独立函数
  • 能完成数值计算型函数(例5-1、5-2)与判断型函数(例5-3)的设计
  • 能结合 AI 协作审查函数边界条件,设计正常、边界与非法输入测试
5.1 / 函数的定义与调用
变化的是每次输入的数据,
不变的是背后的计算规则
贯穿案例:将社区慢病管理中“由身高与体重计算体质指数”封装成可复用的 calcBMI 函数。
函数接口 函数定义 原型声明 边界防御

数据场景:社区慢病建档——计算体质指数(BMI)

社区慢病随访记录

居民体检采集:体重 70.0 kg,身高 1.75 m。

背景:体质指数(BMI)是社区高血压、糖尿病等慢病随访建档的核心指标。

体质指数计算公式

统一按标准公式计算:

70.0 ÷ (1.75 × 1.75) ≈ 22.86

double bmi1 = 70.0 / (1.75 * 1.75);

double bmi2 = 82.0 / (1.80 * 1.80);

double bmi3 = 55.0 / (1.60 * 1.60);
思考:如果社区有数十位慢病居民随访数据,或者要统一增加“身高不能为0或负数”的合法性检查,直接复制表达式会遇到什么困难?

先不写代码:提取共同规律与接口四问

输入体重 weight
身高 height
→
检查数据合法性
执行计算公式
→
返回体质指数 BMI
double
接口设计问题 在本例中的约定
1. 函数叫什么名字? calcBMI(名称清晰表达单一职责)
2. 需要调用者提供什么输入(形参)? double weight(体重/kg)、double height(身高/m)
3. 向调用者交还什么结果(返回类型)? double(计算出的体质指数 BMI)
4. 输入不合法时如何响应? 若 weight <= 0 或 height <= 0,统一返回错误标识 -1.0

第一次定义函数:把规则写成独立模块

double calcBMI(double weight, double height)

{

    if (weight <= 0 || height <= 0)

        return -1.0;

    return weight / (height * height);

}

函数首部(Header)

  • 返回类型:double(与返回值类型一致)
  • 函数名:calcBMI
  • 形参表:(double weight, double height),每个参数都必须单独写明类型

函数体(Body)

  • 先做边界卫语句校验,拒绝非正体重与非正身高(防止除以0)
  • 再用平方除法计算,通过 return 把结果交还调用者

main 怎样使用一个自定义函数?

#include <stdio.h>



double calcBMI(double weight, double height);   /* 1. Function prototype */



int main(void)

{

    double bmi = calcBMI(70.0, 1.75);           /* 2. Function call */

    printf("BMI = %.2f\n", bmi);

    return 0;

}



double calcBMI(double weight, double height)    /* 3. Function definition */

{

    if (weight <= 0 || height <= 0)

        return -1.0;

    return weight / (height * height);

}

函数声明

告诉编译器函数的返回类型、名称和参数类型,末尾有分号。

函数调用

在 main 中给出实参 (70.0, 1.75),启动函数并接收返回值。

函数定义

真正实现计算逻辑的代码块,首部末尾无分号。

边界测试与 AI 协作审查

测试类型 weight height 期望返回 设计目的
正常数据 70.0 1.75 22.86 验证常规计算公式正确性
零值边界 0.0 1.75 -1.0 验证非正体重被合法拦截
除零边界 70.0 0.0 -1.0 拦截除以 0 的严重运行错误
非法负值 -5.0 / 70.0 1.75 / -1.75 -1.0 暴露“只判断 height == 0”的漏洞

🤖 AI 协作互动:让 AI 补充边界测试,不代写代码

我正在设计 C 语言社区慢病体质指数计算函数 double calcBMI(double weight, double height),约定输入合法时返回 BMI 指数,非法时返回 -1.0。我已经设计了 (70.0, 1.75)、(0, 1.75)、(70.0, 0) 三组测试。请只帮我检查是否遗漏了边界或极端测试数据,按【测试输入—预期返回—检验目的】列出,不要直接生成函数代码。

💡 课堂互动:学生先写出自己的测试表,再提问 AI 并逐条人工核验 AI 建议的测试点是否真正对应代码分支。

5.1 / 参数传递与执行流
调用函数时,
数据是怎样进出的?
理清实参到形参的单向值复制过程,消除“修改形参会改变实参”的常见误区。
控制流跳转 实参与形参 单向值传递 return 机制

一次函数调用的执行路线

1. main 暂停
遇到 calcBMI(70.0, 1.75)
→
2. 实参复制给形参
70.0 → weight
1.75 → height
→
3. 执行函数体
算出 22.86
→
4. return 返回
回到调用处赋给 bmi
函数执行完毕后,程序不会从头重新运行 main,而是精确回到刚才调用它的位置继续往下走。

认知冲突:在函数里修改形参,会影响原变量吗?

void tryChange(double x)

{

    x = 100.0;

}



int main(void)

{

    double weight = 70.0;

    tryChange(weight);

    printf("%.1f\n", weight);

    return 0;

}

先预测再解释:屏幕最终打印 70.0 还是 100.0?

main 的变量区

weight 始终保存在 main 自己的内存单元中,值为 70.0。

tryChange 的形参区

形参 x 只是复制了一份 70.0 的副本;x = 100.0 只改了副本,函数结束即销毁。

值传递(Pass by Value)的规则与边界

实参(Actual Argument)

  • 出现在函数调用圆括号中
  • 可以是常量、变量或表达式,如 calcBMI(w + 2.5, 1.75)
  • 调用时必须已经有确定的值

形参(Formal Parameter)

  • 出现在函数定义首部圆括号中
  • 必须是变量名,用来接收实参传来的值
  • 仅在函数被调用期间分配内存,返回后释放
值传递边界说明:本节讨论普通数值变量(int、double、char)作为参数,数据单向从实参传给形参;后续学习指针与数组时,再讨论如何让函数修改调用者的数据。

🤖 AI 协作互动:值传递内存快照推演

在 C 语言中,如果我写了一个交换函数 void swap_wrong(int a, int b) { int t = a; a = b; b = t; },并在 main 中调用 swap_wrong(x, y),为什么 x 和 y 的值并没有交换?请用【调用前—进入函数时—交换后—函数返回后】四步内存表帮我可视化解释。

💡 课堂互动:同桌对照 AI 生成的四步内存表,解释为什么“改了形参 a、b 却没改实参 x、y”。

教材例5-1 · 例5-2
把数学公式变成工具:
一次封装,多次组合调用
从圆柱体体积 cylinder(r, h) 到五边形分割求面积 area(x, y, z)。
cylinder area 海伦公式 多次调用

教材例5-1 题面描述

📋 题目要求

输入圆柱体的半径 \(r\) 和高 \(h\),计算并输出圆柱体的体积 \(V = \pi r^2 h\)。
要求定义和调用函数 cylinder(r, h) 计算圆柱体的体积。

\[V = \pi r^2 h\]

教材例5-1:圆柱体体积函数实现

#include <stdio.h>

double cylinder(double r, double h);



int main(void)

{

    double height, radius, volume;

    scanf("%lf%lf", &radius, &height);

    volume = cylinder(radius, height);

    printf("Volume = %.3f\n", volume);

    return 0;

}



double cylinder(double r, double h)

{

    double result;

    result = 3.1415926 * r * r * h;

    return result;

}

结构对照与检验

  • 对照 calcBMI:同样是“两个数值参数 → 公式计算 → 返回 double”。
  • 参数对应:实参 radius 传给形参 r,height 传给 h,按位置顺序一一对应。
  • 预测结果:输入 2.0 3.0,输出 Volume = 37.699。

教材例5-2 题面描述与几何分解

📋 教材例5-2 题目要求

将一个五边形分割成三个三角形(如右图),输入各边长 \(a_1 \sim a_7\),定义并调用计算三角形面积的函数 area(x, y, z),求五边形面积。

海伦-秦九韶公式

已知三角形三边 \(x, y, z\),半周长 \(p = \frac{x+y+z}{2}\):

\[\text{area} = \sqrt{p(p-x)(p-y)(p-z)}\]

五边形分割为三个三角形

教材例5-2:一个函数,被三组数据重复调用

#include <stdio.h>

#include <math.h>



double area(double x, double y, double z);



int main(void)

{

    double a1, a2, a3, a4, a5, a6, a7, s;

    scanf("%lf%lf%lf%lf%lf%lf%lf", &a1, &a2, &a3, &a4, &a5, &a6, &a7);

    s = area(a1, a5, a6) + area(a4, a6, a7) + area(a2, a3, a7);

    printf("The area of the Pentagon is %.2f\n", s);

    return 0;

}



double area(double x, double y, double z)

{

    double p = (x + y + z) / 2;

    return sqrt(p * (p - x) * (p - y) * (p - z));

}
函数的复用价值:把“求一个三角形面积”写好并验证一次,就能用不同实参组合 (a1,a5,a6)、(a4,a6,a7)、(a2,a3,a7) 连续调用三次!

深度追问:任意输入三个数都能算出面积吗?

潜在风险

如果输入的边长是 1, 2, 10,半周长 p = 6.5,则 p - z = -3.5,导致 sqrt 对负数开方,产生 NaN(非数值)!

几何合法性前提

三边均为正数,且满足任意两边之和大于第三边:

x > 0 && y > 0 && z > 0 && x + y > z && x + z > y && y + z > x

🤖 AI 协作互动:海伦公式函数的防御性改造

教材例5-2的三角形面积函数 double area(double x, double y, double z) 直接计算海伦公式。如果用户输入了不能构成三角形的三边(如 1, 2, 5 或含负数),sqrt 会出现什么现象?请帮我为 area 函数设计前置合法性校验条件,并提供 3 组退化/非法三角形测试数据。

💡 课堂互动:对比“完全信任输入”与“在函数入口处校验参数”的差异,理解函数契约。

教材例5-3
函数不仅可以算数值,
还可以做“真假判断”
设计判断型函数 isSquare(n):用返回值 1 和 0 表达“是”与“否”。
判断型函数 isSquare 减奇数法 0与负数边界

教材例5-3 题面描述

📋 题目要求

定义一个判断完全平方数的函数 isSquare(n),当 \(n\) 为完全平方数时返回 1,否则返回 0。
(注:不允许调用数学库函数 sqrt)

不用 sqrt,怎样判断完全平方数?

思路A:枚举试乘

对于非负整数 \(n\),让 \(i\) 从 \(0, 1, 2, \dots\) 递增,测试 \(i \times i\) 是否等于 \(n\)(当 \(i \times i > n\) 时停止)。

思路B:连续减奇数法(教材法)

利用数学性质:前 \(k\) 个连续正奇数之和等于 \(k^2\):

\(1 + 3 + 5 + \dots + (2k-1) = k^2\)

把 \(n\) 依次减去 \(1, 3, 5, 7, \dots\),如果最终恰好减到 0,说明它是完全平方数!

连续减奇数法:代码实现与过程跟踪

int isSquare(int n)

{

    int i;

    for (i = 1; n > 0; i = i + 2) {

        n = n - i;

    }

    if (n == 0)

        return 1;

    else

        return 0;

}
思考:循环里修改了形参 n,会影响 main 里的原变量吗?(不会,因为是值传递!)
初始 n 依次减去的奇数 i 循环结束时 n 返回值
9 9-1=8 → 8-3=5 → 5-5=0 0 1(是)
10 10-1=9 → 9-3=6 → 6-5=1 → 1-7=-6 -6 0(否)
0 n > 0 不成立,循环 0 次 0 1(\(0=0^2\))
-4 n > 0 不成立,循环 0 次 -4 0(否)

判断型函数怎么在 main 中调用?

int isSquare(int n);



if (isSquare(n)) {

    printf("%d is a square number.\n", n);

} else {

    printf("%d is not a square number.\n", n);

}

🤖 AI 协作互动:两种无 sqrt 算法的对比与边界核验

判断非负整数 n 是否为完全平方数(不用 sqrt),方法一是 for(i=1; n>0; i+=2) n -= i; 判断 n==0;方法二是 for(i=0; i*i<n; i++); 判断 i*i==n。请对比这两种方法在 n=0、n=-1 以及 n 接近 int 最大值(如 2147483647)时的安全性差异,指出哪种写法可能发生整数乘法溢出。

💡 课堂互动:用 -1, 0, 1, 15, 16 五个测试值核验函数,并观察 AI 指出的 i * i 潜在溢出风险。

第1课时后测与总结
从“写一段代码”
到“设计一个可测试的函数”
通过两道出口诊断题检验函数签名与边界防御,梳理自定义函数设计四步法。
出口诊断 函数签名 边界修复 课后迁移

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

出口题一:哪行函数首部合法?

A. double calcBMI(double weight, height)

B. double calcBMI(double weight, double height)

C. calcBMI(double weight, double height)

D. double calcBMI(double weight, double height);

结论:选 B

A 漏写第二个形参类型;C 漏写返回类型;D 末尾多了分号(变成了声明而非定义首部)。

出口题二:找出边界漏洞并修复

double calcBMI(double weight, double height)

{

    if (height == 0) return -1.0;

    return weight / (height * height);

}

暴露问题的测试输入:(70.0, -1.75) 或 (-5.0, 1.75)

修复建议:改为 if (weight <= 0 || height <= 0) return -1.0;

本课小结:两类有返回值函数的设计模式

1. 识别重复规则
→
2. 明确接口契约
参数、返回类型、异常约定
→
3. 编写声明与定义
→
4. 边界测试与核验
函数类型 典型代表 返回类型与含义 调用方式
数值计算型 calcBMI(w, h)、cylinder(r, h)、area(x, y, z) double / int:计算出的具体数值(或错误码) 参与赋值、算术运算或直接作为 printf 参数
条件判断型 isSquare(n) int:返回 1 表示真(满足),0 表示假(不满足) 常直接放进 if (isSquare(n)) 条件表达式中

课后迁移练习与下节预告

基础与教材练习

  • 完成教材例5-1(圆柱体积)、例5-2(五边形面积)上机运行
  • 用 -1, 0, 1, 15, 16 五组数据测试例5-3 isSquare(n)
  • 模仿 isSquare(n),编写判断整数是否为偶数的函数 even(n)

AI 协作与进阶思考

  • 为例5-2 的 area(x, y, z) 补全三角形三边合法性检查,记录一条 AI 测试建议与自己的验证结果
  • 下节悬念:如果一个函数只负责在屏幕上打印报告或数字金字塔,不需要返回任何数值,该怎么定义?

写函数前,先定好“输入什么、交还什么、边界怎么处理”。