📘 第七章 · 函数

谭浩强《C程序设计》第五版 — 交互式教学演示

📋第七章知识体系总览(谭浩强 P.145-207)

  • 7.1 概述 — 为什么要用函数
  • 7.2 怎样定义函数
  • 7.3 调用函数
  • 7.4 递归调用
  • 7.5 数组作为函数参数
  • 7.6 局部变量和全局变量
  • 7.7 内部函数和外部函数
  • 变量的存储类别
💡 本章重点:函数是C程序的基本模块。掌握函数的定义调用参数传递是编程的基础。

🗺️本章学习路线图

① 概念
函数概述
② 定义
函数格式
③ 调用
参数传递
④ 递归
自调用
⑤ 数组
参数传递
⑥ 作用域
变量规则

📚谭浩强教材配套说明

教材章节对应:

📌 7.1 概述(P.145-147):函数分类,库函数与用户自定义函数

📌 7.2 怎样定义函数(P.148-162):函数定义形式,旧式定义

📌 7.3 调用函数(P.163-175):函数调用方式,被调用函数声明

📌 7.4 递归调用(P.176-186):递归概念,阶乘与斐波那契

📌 7.5 数组作为函数参数(P.187-198):数组元素/数组名/二维数组作参数

📌 7.6 局部变量和全局变量(P.199-210):作用域,存储类别

📌 7.7 内部函数和外部函数(P.211-214):函数作用域

📖7.1 概述 — 为什么要使用函数

函数的作用:

📌 结构化程序设计:将复杂问题分解为若干个小问题,每个小问题由一个函数实现。

📌 代码复用:一段代码多次使用时,封装成函数避免重复编写。

📌 便于维护:修改一个函数,所有调用处同时生效。

📌 分工合作:不同模块可由不同程序员编写。

📂函数的分类

分类方式 类型 说明 示例
按来源 库函数 系统提供的标准函数 printf(), scanf(), sqrt()
用户自定义函数 程序员自己编写的函数 max(), sort()
按有无返回值 有返回值函数 函数返回计算结果 int max(int a, int b)
无返回值函数 执行特定操作,不返回值 void print(int n)
按参数 有参函数 函数需要参数 int max(int a, int b)
无参函数 函数不需要参数 void menu()

💻简单程序示例

/* 例7.1 求3个数中的最大值 */ int main() { int a, b, c, max; printf("请输入3个整数:\n"); scanf("%d%d%d", &a, &b, &c); max = a; if (b > max) max = b; if (c > max) max = c; printf("最大值是:%d\n", max); return 0; }
💡 改进:将求最大值封装为函数,提高代码复用性。

🔸 用函数改写

/* 求最大值函数 */ int max(int x, int y) { return (x > y) ? x : y; } int main() { int a, b, c; scanf("%d%d%d", &a, &b, &c); printf("最大值是:%d\n", max(max(a, b), c)); return 0; }

💻7.2 怎样定义函数

函数定义的一般形式:
类型标识符 函数名(形式参数表列) { 声明部分 语句部分 }

🔸 示例:求1到n的累加和

int sum(int n) /* 类型标识符 函数名(参数表列) */ { /* { 函数体开始 */ int i, s = 0; /* 声明部分:变量声明 */ for (i = 1; i <= n; i++) s += i; return s; /* 语句部分:返回语句 */ } /* } 函数体结束 */

⚠️旧版本C的函数定义(谭浩强P.150)

注意:谭浩强教材中介绍的旧式定义方法在Turbo C等老式编译器中常用,但已被淘汰。现代C程序应使用标准形式。
/* 旧式定义(不推荐)— 谭浩强P.150 */ int max(a, b) /* 参数类型在参数名后面单独声明 */ int a, b; { return (a > b) ? a : b; } /* 标准定义(推荐)— 现代C */ int max(int a, int b) /* 参数类型直接写在参数表中 */ { return (a > b) ? a : b; }

📝无参函数和有参函数

🔸 无参函数

void printMenu() /* 无参函数 */ { printf("==========\n"); printf("1. 开始游戏\n"); printf("2. 退出\n"); printf("==========\n"); }

🔸 有参函数

/* 例7.2 判断素数 */ int prime(int n) /* 有参函数,参数为待判断的数 */ { int i; for (i = 2; i < n; i++) if (n % i == 0) return 0; /* 能整除,非素数 */ return 1; /* 是素数 */ }

📤函数的返回值

return语句:

📌 return 表达式的值就是函数的返回值
📌 函数返回值的类型必须与函数定义时的类型一致
📌 如果函数没有返回值,应将类型定义为 void
/* 例7.3 比较大小,返回最大值 */ int max(int x, int y, int z) { int max = x; if (y > max) max = y; if (z > max) max = z; return max; /* 返回最大值 */ }

🧪练习:编写函数

编写一个函数,计算圆的面积。

提示:面积 = π × 半径²,需要包含 math.h 使用 M_PI
/* 你的答案: */ double area(double r) { /* 请在此处填写代码 */ }

📞7.3 调用函数

函数调用的一般形式:
函数名(实参表列)

🔸 函数调用的方式

调用方式 说明 示例
函数语句 调用无返回值函数 print();
函数表达式 调用有返回值函数,参与运算 c = max(a, b) + 1;
函数参数 调用有返回值函数,作为参数 printf("%d", max(a, b));

📢被调用函数的声明(谭浩强P.163)

重要规则:如果被调用的函数定义在调用者之后,需要先声明函数原型。
/* 例7.4 函数声明的必要性 */ int main() { int a, b, c; int max(int, int); /* 函数原型声明,可省略参数名 */ scanf("%d%d", &a, &b); c = max(a, b); /* 调用函数 */ printf("max = %d\n", c); return 0; } /* max函数定义在调用之后 */ int max(int x, int y) { return (x > y) ? x : y; }

🔄参数传递 — 值传递(谭浩强P.167)

C语言函数调用采用"值传递":

📌 实参的值被复制一份传给形参
📌 函数内部修改形参,不影响实参
📌 这是C语言与其他语言的重要区别

🔸 值传递示例

void swap(int x, int y) /* 值传递:x,y是副本 */ { int t; t = x; x = y; y = t; /* 只交换了副本 */ } int main() { int a = 10, b = 20; swap(a, b); /* 调用后 a=10, b=20(不变!) */ printf("a=%d, b=%d\n", a, b); }
实参 a
10
实参 b
20
↓ 值传递 ↓
形参 x(副本)
10
形参 y(副本)
20
调用后
a=10, b=20
点击"演示"观察值传递的结果...

📍用指针实现"引用传递"

重点:C语言没有真正的引用传递,但可以通过传递指针来达到修改实参的目的。
/* 用指针实现真正的交换 — 谭浩强P.170 */ void swap(int *p, int *q) /* 接收地址 */ { int t; t = *p; *p = *q; *q = t; /* 通过指针修改实参 */ } int main() { int a = 10, b = 20; swap(&a, &b); /* 传地址! */ printf("a=%d, b=%d\n", a, b); /* 输出:a=20, b=10 */ }
a
10
b
20
点击 swap 按钮观察指针如何实现真正的交换...

🔁7.4 递归调用

什么是递归?

📌 递归是指函数直接或间接地调用自身

📌 递归必须有两个条件:
    ① 递归终止条件(基本情况)
    ② 递归调用(将问题规模减小)

🧮例7.5 求阶乘 n!

/* 例7.5 用递归方法求n! — 谭浩强P.177 */ /* n! = n × (n-1)!,且 1! = 1 */ long factorial(int n) { if (n <= 1) /* 递归终止条件 */ return 1; return n * factorial(n - 1); /* 递归调用 */ }

🔸 递归调用过程分析

factorial(5) 的执行过程:

factorial(5) = 5 × factorial(4)
factorial(4) = 4 × factorial(3)
factorial(3) = 3 × factorial(2)
factorial(2) = 2 × factorial(1)
factorial(1) = 1 (终止)
—— 返回 ——
factorial(2) = 2 × 1 = 2
factorial(3) = 3 × 2 = 6
factorial(4) = 4 × 6 = 24
factorial(5) = 5 × 24 = 120
5!
点击"演示"观察递归的调用过程...

📈例7.6 斐波那契数列

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, ...
通项公式:F(n) = F(n-1) + F(n-2),且 F(1)=1, F(2)=1
/* 例7.6 斐波那契数列 — 谭浩强P.181 */ long fib(int n) { if (n <= 2) /* 递归终止条件 */ return 1; return fib(n - 1) + fib(n - 2); /* 递归调用 */ }
F(1)1
F(2)1
F(3)2
F(4)3
F(5)5
F(6)8
F(7)13
输入n的值,点击"计算"查看斐波那契数列...

⚖️递归与循环(迭代)的比较

递归的缺点:

📌 每次调用都要占用栈空间,递归层次过深可能导致栈溢出

📌 递归的执行效率通常比循环低

📌 斐波那契数列的递归实现存在大量重复计算

🔸 用循环实现阶乘

/* 迭代版本(推荐)*/ long fact_iter(int n) { long result = 1; for (int i = 2; i <= n; i++) result *= i; return result; }
💡 建议:能用循环解决的问题,优先使用循环。只有在问题本身具有递归特性时(如树结构、汉诺塔)才使用递归。

📦7.5 数组作为函数参数

数组元素作函数参数
数组元素作为实参时,与普通变量一样,是值传递
/* 例7.7 数组元素作为函数参数 — 谭浩强P.187 */ int max(int x, int y) /* x, y是形参 */ { return (x > y) ? x : y; } int main() { int a[10] = {...省略...}; int m = max(a[0], a[1]); /* 数组元素作为实参 */ }

📍数组名作为函数参数(重点!谭浩强P.190)

重要区别:

📌 用数组名作为参数时,传递的是地址(即数组首元素的地址)

📌 形参数组名实际上是一个指针变量

📌 在函数内修改形参数组元素,会直接影响实参数组!

🔸 例7.8 在函数中改变数组元素

/* 例7.8 用函数处理数组 — 谭浩强P.190 */ void inv(int x[], int n) /* 数组名作为参数 */ { int t, i, j; for (i = 0, j = n-1; i < j; i++, j--) { t = x[i]; x[i] = x[j]; x[j] = t; /* 逆置数组 */ } }
[0]1
[1]2
[2]3
[3]4
[4]5
初始数组
点击"逆置"观察用函数修改数组的过程...

📐二维数组作为函数参数(谭浩强P.195)

二维数组作为函数参数:

📌 必须指定第二维的大小
📌 第一维的大小可以省略
/* 二维数组作为函数参数 — 谭浩强P.195 */ /* 求3×4矩阵所有元素之和 */ float sum(float score[][4], int m) /* 第二维必须指定 */ { float s = 0; for (int i = 0; i < m; i++) for (int j = 0; j < 4; j++) s += score[i][j]; return s; }

🔸 二维数组内存布局

行\列
[0]
[1]
[2]
[3]
[0]
090
185
292
388
[1]
478
582
680
775
[2]
895
988
1090
1185
💡 二维数组在内存中是按行连续存储的:score[0][0], score[0][1], ..., score[0][3], score[1][0], ...

🎯7.6 局部变量和全局变量

局部变量:在函数内部定义的变量,作用域仅限于该函数内部。
/* 局部变量示例 */ int func(int a) /* a 是函数参数,也是局部变量 */ { int b = 10; /* b 是局部变量 */ return a + b; } /* a 和 b 在此不可见 */
全局变量:在函数外部定义的变量,作用域是从定义处到文件结束。
int max = 100; /* 全局变量,整个文件可见 */ void func() { printf("%d", max); /* 可以访问全局变量 */ }

🖥️作用域层级演示

全局作用域
globalVar = 100
函数作用域
funcLocal = 50 参数 param
块作用域
blockLocal = 30

⚠️变量的"遮蔽"现象

注意:局部变量会遮蔽同名的全局变量。
int a = 10; /* 全局变量 a = 10 */ void func() { int a = 20; /* 局部变量 a,遮蔽了全局变量 */ printf("%d", a); /* 输出 20(局部变量)! */ }

📢外部变量(extern)

extern 关键字:声明在其它地方定义的全局变量。
/* 文件1: global.c */ int global_var = 100; /* 全局变量定义 */ /* 文件2: main.c */ extern int global_var; /* extern 声明 */ int main() { printf("%d", global_var); /* 输出 100 */ }

💾变量的存储类别(谭浩强P.203)

存储类别决定:

📌 变量的存储位置(内存区域)
📌 变量的生命周期(存在时间)
📌 变量的作用域(可见范围)

📊四种存储类别(谭浩强P.203)

存储类别 关键字 存储位置 生命周期 作用域
自动变量 auto(可省略) 动态存储区(栈) 函数调用期间 函数内部
静态局部变量 static 静态存储区 程序运行期间 函数内部
寄存器变量 register CPU寄存器 函数调用期间 函数内部
外部变量 extern 静态存储区 程序运行期间 整个程序(需声明)

🧪例7.9 static 局部变量(谭浩强P.206)

static 局部变量的特点:
📌 仅在第一次调用时被初始化
📌 函数调用结束后不释放,保留原值
📌 整个程序运行期间都存在
/* 例7.9 static 局部变量 — 谭浩强P.206 */ int f(int a) { auto int b = 0; /* 自动变量,每次调用都重置为0 */ static int c = 3; /* static变量,仅初始化一次 */ b = b + 1; c = c + 1; return a + b + c; }
点击"调用"观察 static 和 auto 变量的区别...

register 变量(谭浩强P.208)

register 变量:请求将变量存储在CPU寄存器中,以加快访问速度。
/* 例7.10 register 变量 — 谭浩强P.208 */ long fac(int n) { register int i; /* 寄存器变量 */ long result = 1; for (i = 2; i <= n; i++) result *= i; return result; }
💡 适用于循环计数器等频繁使用的变量。现代编译器会自动优化,无需手动指定。

🎮汉诺塔问题(谭浩强P.184 递归应用)

问题描述:

📌 有三根柱子(A、B、C),A柱上有 N 个圆盘(从小到大)

📌 目标:将所有圆盘从 A 移动到 C

📌 规则:每次只能移动一个圆盘,大盘不能放在小盘上面

💻递归算法(谭浩强P.185)

/* 汉诺塔递归算法 — 谭浩强P.185 */ void hanoi(int n, char one, char two, char three) { if (n == 1) move(one, three); /* 直接移动 */ else { hanoi(n-1, one, three, two); /* 1. 借助three移动n-1个到two */ move(one, three); /* 2. 移动最大盘 */ hanoi(n-1, two, one, three); /* 3. 借助one移动n-1个到three */ } }
💡 移动步数公式:2^N - 1
3个盘子需要7步,4个需要15步,5个需要31步,6个需要63步

🖥️汉诺塔可视化

A柱B柱C柱
选择盘子数并点击"演示"观察汉诺塔解法...

🧠综合随堂测验

📝本章小结

  • 函数定义:类型标识符 + 函数名 + 参数表 + 函数体
  • 函数调用:函数语句 / 函数表达式 / 函数参数
  • 参数传递:值传递(普通参数)vs 地址传递(指针/数组名)
  • 递归调用:必须有不调用自身的终止条件
  • 数组参数:数组名传递的是地址,可在函数中修改实参
  • 局部变量:在函数/块内部定义,作用域限于定义处
  • 全局变量:在函数外部定义,从定义处到文件结束可见
  • static变量:程序运行期间存在,保持上次调用结果