回顾: 形式参数 vs 实际参数
回顾: 按值传递 (如何理解 + swap 为什么不生效)
函数化改造: 素数判定 (return 让代码更简洁)、素数统计
课堂编码: 阶乘改写
数组作参数: 一维、排序封装、二维、生命游戏、成绩案例 (转置留作练习)
函数化四步: 全部例子的总结收口
函数调用栈 (概念与例子)
作用域与生命期: 局部变量何时诞生、何时消失
课堂编码: 挑战 · 组合数
课堂小测
void f(int n) { n = n + 1; } /* 改的是副本 */
void swap(int a, int b) { int t = a; a = b; b = t; }
int x = 10, y = 5;
f(x); /* x 仍是 10 */
swap(x, y); /* x、y 都没变 */
形式参数: 定义里的占位名; 进入函数体就是一个已初始化的局部变量 (拿到实参的副本)
实际参数: 调用时传入的值——常量、变量或任意同类型表达式
按值传递: 复制一份给形参, 之后两者互不影响——swap 失效的根因; 相当于把文件复印一份交给函数
数组是例外 (函数能改到原数组)——为什么, 后面指针部分详细介绍
原版 (判断逻辑埋在 main 里):
#include <stdio.h>
int main(void) {
int n, prime = 1;
scanf("%d", &n);
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) {
prime = 0;
break;
}
}
if (prime == 1) {
printf("是素数\n");
} else {
printf("不是素数\n");
}
return 0;
}
改造三步: ① 输入是什么? ② 输出是什么? ③ 函数叫什么名字?——后面每个例子都这么问
口头回答: 三步怎么答? 下一页对照
提示: 输入是 n, 输出是 "是/不是" (1/0), 函数名 is_prime
⚡ 打开代码工作区 ⚡ 在 VS Code 中打开 prime-re.c#include <stdio.h>
int is_prime(int n) {
if (n <= 1) return 0;
for (int d = 2; d <= n / d; d++)
if (n % d == 0) return 0;
return 1;
}
int main(void) {
int n;
printf("输入一个数: ");
scanf("%d", &n);
if (is_prime(n)) printf("是素数\n");
else printf("不是素数\n");
return 0;
}
divisor <= n / divisor 是 \(\sqrt{n}\) 的等价写法 (不用 math.h), 不会溢出; 原版的 d * d 写法在 n 近 int 上限时溢出 (有符号溢出是未定义行为)——结果为负时条件恒真, 循环停不下来
顺手补上 n <= 1——原版对 n = 1 会误判; 改造不只是搬代码, 也顺手修 bug
返回 1/0, if (is_prime(n)) 直接当条件——与 8-1 一致
/* 啰嗦版: 用 if 包装 */
int is_even(int n) {
if (n % 2 == 0) {
return 1;
}
return 0;
}
/* 简洁版: 布尔表达式直接 return */
int is_even(int n) { return n % 2 == 0; }
int is_digit(char c) { return c >= '0' && c <= '9'; } /* 数字 */
double average(double a, double b) { return (a + b) / 2; } /* 两个参数, 照样一行 */
判断的结果本身就是 1 或 0——不需要 if 包装, 直接 return 表达式
一类经典小函数都是 "一行函数": 偶数、数字、大小写字母……参数也可以有多个——average(a, b) 两个参数照样一行 return
提前 return 也让代码更简洁: 找到就返回, 不需要 flag 变量——is_prime 就是这么写的
统计 2~max 的素数个数 (非函数版):
/* 原版: 判断逻辑埋在 main 里 */
#include <stdio.h>
int main() {
int max;
scanf("%d", &max);
int count = 0;
for (int i = 2; i <= max; i++) {
// 判断 i 是否素数
int prime = 1;
for (int j = 2; j <= i / j; j++) {
if (i % j == 0) {
prime = 0;
break;
}
}
if (prime == 1) {
count++;
}
}
printf("\n素数个数: %d", count);
return 0;
}
口头回答: 把 "判断 i 是否素数" 的逻辑抽成 is_prime(i)
进一步: 把统计循环也抽成 count_primes(max)——main → count_primes → is_prime 两层调用链
复用前面刚写的 is_prime
⚡ 在 VS Code 中打开 primes-re.c
/* 函数版: is_prime + count_primes 两层 */
#include <stdio.h>
int is_prime(int n) {
if (n <= 1) {
return 0;
}
for (int j = 2; j <= n / j; j++) {
if (n % j == 0) {
return 0;
}
}
return 1;
}
int count_primes(int max) { /* 统计也抽成函数 */
int count = 0, i;
for (i = 2; i <= max; i++) {
if (is_prime(i) == 1) { /* 调用 is_prime */
count++;
}
}
return count;
}
int main() {
int max;
scanf("%d", &max);
printf("\n素数个数: %d", count_primes(max)); /* 一行调用 */
return 0;
}
两层调用链: main → count_primes → is_prime——函数不光能被 main 调用, 还能调用别的函数
is_prime 被复用了max-1 次 (i = 2..max), 与前面判定素数的函数是同一个——写一次, 处处用
main 只剩读入和一行输出——主函数开始像一份 "说明书": 只写步骤, 细节藏进函数
#include <stdio.h>
int main(void)
{
int n;
long f = 1;
printf("输入 n: ");
scanf("%d", &n);
for (int i = 1; i <= n; i++) { /* 计算 n! */
f = f * i;
}
printf("%d! = %ld\n", n, f);
return 0;
}
任务: 抽出 long factorial(int n), main 只剩输入、调用、输出
提示: 阶乘函数在后面的递归课上还会再次登场 (n! = n × (n-1)!)
⚡ 在 VS Code 中打开 factorial-re.c求数组元素之和 (非函数版):
/* 求两组数据的和: 非函数版 */
#include <stdio.h>
int main(void)
{
int b[5] = {3, 7, 1, 9, 4};
int c[3] = {10, 20, 30};
int sum = 0;
for (int i = 0; i < 5; i++) { /* 第一组 */
sum += b[i];
}
printf("sum = %d\n", sum);
sum = 0;
for (int i = 0; i < 3; i++) { /* 第二组: 又抄了一遍 */
sum += c[i];
}
printf("sum = %d\n", sum);
return 0;
}
现场演示: 把求和循环抽成函数, main 只剩调用和输出——数组的传法今天第一次出现, 跟着演示边改写边学
边改写边介绍: 数组怎么传 (int a[])? 长度怎么传 (单独一个参数)? ——下一页是标准写法及原因
提示: 同一个求和循环写了两遍——如果要对十组数据求和呢? 今天要解决的就是这个
⚡ 在 VS Code 中打开 sum-array-re.c/* 求和: 函数版 */
#include <stdio.h>
int sum_array(int a[], int n) /* 形参: 数组+长度 */
{
int sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
return sum;
}
int main(void)
{
int b[5] = {3, 7, 1, 9, 4};
int c[3] = {10, 20, 30};
printf("%d\n", sum_array(b, 5)); /* 调用: 只写数组名 */
printf("%d\n", sum_array(c, 3)); /* 换一组数据 */
return 0;
}
形参 int a[] 只拿到数组的开头位置 (首元素的地址 1000), 不携带长度——长度必须由调用者单独传 n
函数里 a[i] = 从开头位置往后数 i 个——所以改 a[i] 就是改 b[i], 这就是 "函数里改数组 = 改原数组" 的由来
为什么传进来的是地址, 原因在后面的指针部分揭晓 (数组名传的是地址)
之前学过的选择排序 (非函数版):
/* 选择排序: 非函数版 */
#include <stdio.h>
int main(void)
{
int a[10], n = 10;
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
if (a[j] < a[min]) {
min = j;
}
}
int t = a[i];
a[i] = a[min];
a[min] = t;
}
for (int i = 0; i < n; i++) {
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
口头回答: 把排序逻辑抽成函数, main 里只剩读入、一行调用、输出
动手前想清楚: 数组怎么传? 长度怎么传?——和 sum_array 是同样的两个问题
⚡ 在 VS Code 中打开 sort-re.c之前学过的排序把整段循环写在 main 里——想给另一组数据排序, 就得再写一遍
void sort(int a[], int n) /* 把选择排序装进函数 */
{
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
if (a[j] < a[min]) {
min = j;
}
}
int t = a[i]; a[i] = a[min]; a[min] = t;
}
}
sort(a, 10); /* 排 a 的前 10 个元素 */
sort(b, 5); /* 同一段逻辑, 换个数组再用 */
函数化改造: 把重复的逻辑抽出来封装成函数——本周的核心任务
求矩阵元素之和 (非函数版):
/* 求 m 的元素之和: 非函数版 */
#include <stdio.h>
int main(void)
{
int m[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int sum = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
sum += m[i][j];
}
}
printf("sum = %d\n", sum);
return 0;
}
口头回答: 把求和逻辑抽成函数, main 只剩调用和输出
动手前想清楚: 数组怎么传? 行数怎么传? 列数怎么传?——二维比一维多一个问题, 下一页揭晓
⚡ 在 VS Code 中打开 sum-matrix-re.cint sum_matrix(int a[][4], int rows) /* 列数 4 不能省 */
{
int sum = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
sum += a[i][j];
}
}
return sum;
}
int m[3][4] = { ... };
sum_matrix(m, 3); /* 数组名 + 行数 */
二维数组作参数, 只能省略第一维——列数必须写出来
为什么列数不能省? 下一页用内存图细讲——先记住 a[][4] 的 4 不能省
调用: 传数组名 + 行数——写法朴素, 但数组传的是开头位置, 函数能改到原数组 (和标量不同)
二维数组在内存里按行连续存放: 第 0 行存完, 紧接着第 1 行、第 2 行……
要定位 a[i][j], 先跳过 i 行, 再走 j 个: 偏移 = i × 列数 + j——这是相对 a (即 a[0][0]) 的元素偏移, 乘上 sizeof(int) 才是字节偏移
"列数"就是行与行之间的步长——形参 a[][4] 的 4 不能省: 编译器要用它算 a[i][j] 的地址
反过来: 行数不影响步长, 所以第一维可以省略
/* 演化一代: 数邻居 + 套规则 + 打印, 全在 main 里 */
#include <stdio.h>
#define SIZE 8
int main(void)
{
int board[SIZE + 2][SIZE + 2] = {{0}};
int next[SIZE + 2][SIZE + 2] = {{0}};
/* 初始: 一个滑翔机 (初始画面见右栏) */
board[2][3] = 1; board[3][4] = 1;
board[4][2] = 1; board[4][3] = 1; board[4][4] = 1;
for (int i = 1; i <= SIZE; i++) {
for (int j = 1; j <= SIZE; j++) {
int cnt = -board[i][j];
for (int m = i - 1; m <= i + 1; m++) {
for (int n = j - 1; n <= j + 1; n++) {
cnt += board[m][n];
}
}
if (board[i][j] == 1) {
next[i][j] = (cnt == 2 || cnt == 3);
} else {
next[i][j] = (cnt == 3);
}
}
}
for (int i = 1; i <= SIZE; i++) { /* 打印新一代 */
for (int j = 1; j <= SIZE; j++) {
printf("%c", next[i][j] ? '*' : '.');
}
printf("\n");
}
return 0;
}
先看效果: 左上角的小三角叫滑翔机——演化一代后, 它移动了一格:
初始: 下一代:
........ ........
..*..... ........
...*.... ..*.*...
.***.... ...**...
........ ...*....
规则: 活格子活邻居 2 或 3 → 活, 否则死; 死格子活邻居恰好 3 → 活; 边界技巧——四周加一圈 0, 数邻居不用判断边界
同桌讨论: 打算拆成几个函数? 各自的输入、输出?
提示: 想演化 10 代? 非函数版再套一层——五层嵌套; 函数版只需一个十代循环
⚡ 在 VS Code 中打开 game-of-life-re.cint count_neighbors(int board[][SIZE + 2], int i, int j)
{
int cnt = -board[i][j];
for (int m = i - 1; m <= i + 1; m++) {
for (int n = j - 1; n <= j + 1; n++) {
cnt += board[m][n];
}
}
return cnt;
}
void next_generation(int board[][SIZE + 2])
{
int new_board[SIZE + 2][SIZE + 2] = {{0}};
for (int i = 1; i <= SIZE; i++) {
for (int j = 1; j <= SIZE; j++) {
int cnt = count_neighbors(board, i, j);
if (board[i][j] == 1) {
new_board[i][j] = (cnt == 2 || cnt == 3);
} else {
new_board[i][j] = (cnt == 3);
}
}
}
/* 先算后写: 后面的格子还要读旧的邻居状态, 不能边算边写 */
for (int i = 1; i <= SIZE; i++) {
for (int j = 1; j <= SIZE; j++) {
board[i][j] = new_board[i][j];
}
}
}
count_neighbors: 一个格子数邻居——cnt = -board[i][j] 先把自己减掉, 因为自己也被数进去了
next_generation: 对每个格子调用 count_neighbors 并套三条规则, 结果写进 new_board 再写回 board——数组形参让这个写回能改到原数组
为什么两块棋盘? 先全部算到 new_board 再整体写回——本代已改的格子会污染后面格子的邻居判断
函数之间嵌套调用: next_generation 调 count_neighbors——函数可以调用函数
void print_board(int board[][SIZE + 2])
{
for (int i = 1; i <= SIZE; i++) {
for (int j = 1; j <= SIZE; j++) {
printf("%c", board[i][j] ? '*' : '.');
}
printf("\n");
}
}
int main(void)
{
int board[SIZE + 2][SIZE + 2] = {{0}};
board[2][3] = 1; board[3][4] = 1;
board[4][2] = 1; board[4][3] = 1; board[4][4] = 1;
for (int i = 0; i < 20; i++) {
print_board(board);
next_generation(board);
sleep(1); /* 停 1 秒 */
system("clear"); /* 清屏重画 → 动画 */
}
return 0;
}
只改 main 的循环——加上停顿和清屏, 就能看到滑翔机在屏幕上移动:
print_board: 打印棋盘——和演化逻辑分开, 想改输出格式只动这里
main 只剩三件事: 初始化滑翔机 → 二十代循环 {打印, 演化, 停顿, 清屏}——读起来像说明书
二维数组作参数的核心规则: int board[][SIZE + 2] 列数必须写 (编译器要算 board[i][j] 的位置)
加动画只改 main: sleep(1) 每代停 1 秒, system("clear") 清屏重画——三个函数一行未动
先猜再验: 运行起来观察滑翔机的运动——你发现什么规律? (每 4 代平移一格, 斜着飞过屏幕)
求每个学生的总分 (非函数版):
/* 5 名学生 x 3 门课: 非函数版 */
#include <stdio.h>
int main(void)
{
int scores[5][3] = {{80, 85, 90}, {70, 75, 80}, {90, 92, 88},
{60, 65, 70}, {85, 80, 90}};
int sums[5];
for (int i = 0; i < 5; i++) {
sums[i] = 0;
for (int j = 0; j < 3; j++) {
sums[i] += scores[i][j];
}
}
for (int i = 0; i < 5; i++) {
printf("student %d total: %d\n", i + 1, sums[i]);
}
return 0;
}
3 分钟, 自己写: 把每行求和抽成函数, main 只剩调用和输出——二维数组进, 结果往哪放? 下一页对照
提示: 二维数组进 (scores)、一维数组出 (sums)——两个都是 "数组作参数"
/* scores: n 名学生 × 3 门课; sums: 存每个人的总分 */
void row_sums(int scores[][3], int n, int sums[])
{
for (int i = 0; i < n; i++) {
sums[i] = 0;
for (int j = 0; j < 3; j++) {
sums[i] += scores[i][j];
}
}
}
int scores[5][3] = { ... };
int totals[5];
row_sums(scores, 5, totals); /* totals 里是 5 个学生的总分 */
求每个学生的总分 (每行求和), 结果写回一个一维数组:
二维数组进 (scores)、一维数组出 (sums)——都是"数组作参数"
练习: 按列求和呢? sums[j] 的清零该放在哪个循环之外? 结果数组的长度是多少?
函数化四步: ① 找输入 ② 定返回/输出参数 ③ 搬代码 ④ 改调用——今天的例子全部按这四步走; 标量返回值, 数组靠输出参数 (改原数组/写进另一个数组):
| 例子 | 输入 | 返回 / 输出参数 | 调用 |
|---|---|---|---|
is_prime |
int n |
1/0 | if (is_prime(n)) |
count_primes |
int max |
个数 | count_primes(max) |
sum_array |
a[], n |
和 | sum_array(b, 5) |
sort |
a[], n |
无 (改原数组) | sort(a, 10) |
sum_matrix |
a[][4], rows |
和 | sum_matrix(m, 3) |
transpose |
a[][4], b[][3], rows |
无 (写进 b) | transpose(a, b, 3) |
row_sums |
scores[][3], n, sums[] |
无 (写进 sums) | row_sums(scores, 5, totals) |
combination |
m, n |
值 | 你自己写 |
函数不执行的时候是什么? 编译后它只是代码区里静静躺着的一段指令——像书架上的菜谱, 不做事也不占台面; 一被调用, 系统才为这一次调用在栈上放一张便签。同一张菜谱能反复做菜——同一个函数每次调用都是独立的一次:
先想一个问题: 函数返回之后, 它里面的局部变量去了哪里? 下次再调用, 为什么又是全新的?
每次函数调用, 系统在调用栈 (call stack) 上放一张 "便签"——术语叫栈帧 (stack frame): 记下返回地址、参数、局部变量
返回地址: 函数结束后, 回到调用处继续执行的下一行
函数返回时揭掉这张便签——局部变量 "出了函数就没了", 就是这个原因
栈是后进先出: 最后调用的函数最先返回——像一摞便签, 后放的压在最上面, 只能先揭最上面那张
/* primes.c 函数版: 两层调用链 */
#include <stdio.h>
int is_prime(int n) {
if (n <= 1) return 0;
for (int d = 2; d <= n / d; d++) {
if (n % d == 0) return 0;
}
return 1;
}
int count_primes(int max) {
int count = 0;
for (int i = 2; i <= max; i++) {
if (is_prime(i)) count++;
}
return count;
}
int main(void) {
printf("%d\n", count_primes(10));
return 0;
}
is_prime 返回时, 栈顶的便签被撕掉: 局部变量 d 随之消失, 控制权按 "返回地址" 回到 count_primes
count_primes 的循环一次次调用 is_prime, 每次都放一张全新的便签——所以每次的局部变量都是新的
数组作参数时, 便签上记的不是整个数组, 而是数组的开头位置——函数顺着位置改到原数组; 普通参数记的是值的副本——这就是 "数组是例外" 的原因
后面的递归课: 函数一层层调用自己, 栈上便签一层层叠高——今天先记住这张图
🎬 打开调用栈动画 (逐帧压栈/弹栈) 🌐 在 Python Tutor 中演示 (粘贴左栏代码, 逐行看栈帧)int x = 0; /* 文件作用域 */
void f(void)
{
int x = 1; /* 块作用域, 屏蔽全局 x */
printf("%d", x); /* 打印 1 */
}
作用域: 标识符可以被使用的那段代码范围
块作用域: 函数体内 {} 覆盖的范围——局部变量、形式参数
文件作用域: 从定义位置到文件末尾——全局变量
内层屏蔽外层: 内层同名变量会遮蔽外层的
局部变量: 定义在函数内, 出了函数就不可见
全局变量: 定义在函数外, 整个文件可见; 少用为妙 (谁都可能改它)
动态变量(默认): 函数被调用时诞生, 函数返回时消亡
动态变量与递归: 递归的每一层调用都是一次独立的函数调用——各层的参数和局部变量随层生、随层亡, 互不干扰 (上一页"每层一套自己变量"的底层原因就在这里)
静态变量(static): 与程序同生共死, 函数返回后保留原值
void f(void)
{
static int count = 0; /* 只初始化一次 */
count++;
printf("第 %d 次调用\n", count);
}
static 局部变量 = 带记忆的局部变量 (下次调用接着用上次的值)
void f(void) {
int i; /* 没有初始化 */
printf("i = %d\n", i);
}
int main(void) { f(); return 0; }
一次运行的结果 (每次可能不同!):
i = 32764
动态局部变量诞生时不会自动清零——里面是内存里残留的"垃圾值"
每次运行、每台机器都可能不同; 调试器里恰好是 0 只是假象
教训: 局部变量必须初始化后再使用; 编译加 -Wall 有时能提示 "used uninitialized"
后面讲递归时也一样: 每层调用的局部变量各自独立, 忘初始化同样读到垃圾值
挑战: 用今天写好的 factorial 写 combination——C(m,n) = m! / (n!(m-n)!), 原型 long combination(int m, int n); main → combination → factorial 两层调用链, 运行时栈上就叠着刚才那样的两层便签
factorial 被调用了三次、参数各不同——小函数可以组合成更大的函数
追问: 试试 m = 30——为什么结果不对? (提示: 30! 超出 long 的容量——Windows 上 long 只有 32 位, 13! 就溢出; Mac/Linux 64 位也到 20!)
⚡ 在 VS Code 中打开 combination.c (只写 combination)count_primes(10) 返回什么? 为什么?
int a[][], 能编译吗? 为什么? 正确的写法是什么?