📋第七章知识体系总览(谭浩强 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 概述(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
📌 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
提示:面积 = π × 半径²,需要包含 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语言与其他语言的重要区别
📌 实参的值被复制一份传给形参
📌 函数内部修改形参,不影响实参
📌 这是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
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
通项公式: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
📌 规则:每次只能移动一个圆盘,大盘不能放在小盘上面
📌 有三根柱子(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步
3个盘子需要7步,4个需要15步,5个需要31步,6个需要63步
🖥️汉诺塔可视化
A柱B柱C柱
选择盘子数并点击"演示"观察汉诺塔解法...
🧠综合随堂测验
📝本章小结
- 函数定义:类型标识符 + 函数名 + 参数表 + 函数体
- 函数调用:函数语句 / 函数表达式 / 函数参数
- 参数传递:值传递(普通参数)vs 地址传递(指针/数组名)
- 递归调用:必须有不调用自身的终止条件
- 数组参数:数组名传递的是地址,可在函数中修改实参
- 局部变量:在函数/块内部定义,作用域限于定义处
- 全局变量:在函数外部定义,从定义处到文件结束可见
- static变量:程序运行期间存在,保持上次调用结果