第6章 函数
运行环境以Dev-C++、Visual Studio 2022、MacOS的命令行和Xcode为主
0.章节引言
-
1.随着程序的膨胀和复杂化,将所有代码“堆积”到
main()函数中不利于代码维护,把程序分解成不同的“功能段”会使工作简化、程序明晰,这些“功能段”即函数 -
2.函数由实现一定任务的语句序列组成
// void:函数没有返回值
void display() {
printf("hello!\n");
} // 此函数执行结束后,回到main()函数
int main() { // 先执行main函数
display(); // 函数调用,数据流立即跳转到display()函数中
return 0;
}
-
3.一个C程序由一个或多个函数组成,函数之间可以相互调用,但不能调用
main()函数-
标准函数,库函数:
scanf() printf() -
用户自定义函数:
display() add() sum()
-
1.函数的构成
- 1.1 函数的定义格式
// 类型说明符:函数的返回值类型
// 形参列表:函数所需要的参数,在函数调用中传递数据。若函数不需要传递数据,可省略参数,但括号不能省略
// {} 内部是函数体,是该函数具体要执行的功能
类型说明符 函数名(形参列表) {
声明部分;
语句部分;
}
int sum(int a, int b) {
int c = 0; // 声明部分
c = a + b; // 语句部分
return c;
}
// 若函数定义时省略"类型说明符",系统默认函数的返回类型为int。建议不要省略,以提升程序可读性
sum(int a, int b) {
int c = 0; // 声明部分
c = a + b; // 语句部分
return c;
}
-
1.2 函数的参数
-
形式参数:在函数定义时指定的参数,简称形参,如下述代码
max()函数的x和y -
实际参数:在函数调用时传递的数据,简称实参,如下述代码
main()函数的a和b
#include <stdio.h> int max(int x, int y) { // 不可简写为 int max(int x, y) int z = 0; z = x > y? x:y; return z; } int main() { int a = 0, b = 0, c = 0; printf("input a、b:"); scanf("%d %d", &a, &b); c = max(a, b); // 函数调用,实参a的值传递给形参x,实参b的值传递给形参y,函数执行结束后的返回值传递给c printf("较大值: %d", c); return 0; }-
注意事项
-
1.函数的形参可以有多个,用
,分隔,类型相同或不同均可。相同类型时不可随意简写 -
2.形参必须是变量,实参可以是变量、常量和表达式
-
3.实参类型必须与形参类型兼容,在符合数据类型转换规则的前提下,系统会按照赋值转换规则进行类型转换,如
float转为int、char转为int等 -
4.函数进行普通“值传递”时,将实参值传递给形参,传递方向为单向。传递结束后,若在函数中修改形参的值,并不会反作用于实参,两者在内存中分别占用不同的内存单元
-
-
-
1.3 函数的返回值
-
若希望获取函数调用后的执行结果,并在
main()函数中使用,则函数必须使用return语句将结果返回给调用函数 -
一般格式:
return (表达式); 或 return 表达式; -
注意事项
-
1.一个函数可以有多条
return语句,分别为指定条件返回各自的值。执行完第1个return语句后便终止函数的执行,将结果返回给调用函数并继续执行调用函数中的语句 -
2.函数默认的返回类型为int,可省略函数类型说明符,但不建议这么做
-
3.若
return的类型和函数类型不一致,以函数类型说明符为准 -
4.若函数没有
return语句,则返回一个不确定的值。建议需要函数返回值时就加上函数的类型,不需要返回值时定义函数类型为void
-
-
2.函数的调用与声明
-
2.1 函数调用
-
一般格式:
函数名(实参列表) -
注意事项
-
1.
()是函数调用操作符。若函数无参数,则括号内为空,但不能省略 -
2.多个参数之间使用逗号分隔
-
3.实参个数与形参相同,且类型赋值兼容
-
-
调用方式
- 1.函数语句
// 函数有或没有返回值都可以这样调用 max(a, b); calcu(n); // 该调用方法只执行函数中的语句,不使用返回值- 2.函数表达式
// 函数有返回值才可以这样调用,将函数的调用作为一个值参与表达式运算 c = 2 * max(a, b) + 5; // void型函数不允许使用这种方式- 3.作为函数参数
// 函数有返回值才可以这样调用,将函数的调用作为函数实参传递给函数形参 d = max(max(a, b), c); // void型函数不允许使用这种方式 -
案例分析
- 1.输入 n 的值,计算1 ~ n 的整数和
#include <stdio.h> int calcu(int x) { // 求和函数 int i = 0, total = 0; for(i = 1; i <= x; i++) { total += i; } return total; } int main() { int n = 0; scanf("%d", &n); printf("1+2+3+...+%d = %d", n, calcu(n)); // 传递n的值给形参x return 0; }
-
-
2.2 函数声明
-
一般格式:
类型说明符 函数名(形参列表); -
作用:令编译器在编译时对函数调用检查合法性,非法函数调用会编译报错
-
注意事项
-
1.为提高程序可读性,建议把函数原型写在
main()函数前,函数定义写在main()函数后。若函数定义在main()函数前,可省略函数原型 -
2.函数声明也称为函数原型,可写在主调函数中
int main() { int calcu(int x); // 函数声明 int n = 0; scanf("%d", &n); printf("1+2+3+...+%d = %d", n, calcu(n)); return 0; }- 3.函数声明中可省略形参名称,只写形参类型,如
int max(int, int);编译系统不检查形参名
-
-
-
2.3 函数嵌套调用与递归调用
-
递归调用:函数调用自身,必须有条件地进行(递归终止条件),否则无限递归会导致程序崩溃
-
注意事项
-
1.C程序中函数的定义不能嵌套,但函数的调用可以嵌套
-
2.
main()函数是程序执行的开始,一般不会像其他自定义函数显式调用,而是由编译器进行隐性调用 -
3.递归程序的关键在于找出递归关系和递归终止条件
-
4.递归算法容易理解,但递归有可能大大增加程序运行时间并消耗大量内存
-
-
案例分析
- 1.输入正整数 n 的值,求n!
// 当n = 0或1时,n! = 1;当n >= 1时,n! = n * (n - 1)! #include <stdio.h> float fac(int n) { float f = 0.0f; if(n <= 1) { // 避免无限递归 f = 1; } else { f = n * fac(n - 1); } return f; } int main(){ int n = 0; float result = 0.0f; printf("input an integer:"); scanf("%d", &n); result = fac(n); // 求n! printf("result=%.2f\n", result); // 输出计算结果 return 0; }![]()
- 2.输入正整数 n,求 Fibonacci 序列第 n 项的值
// 当n = 1或2时,fib(n) = 1;当n > 2时,fib(n) = fib(n - 1) + fib(n - 2) #include <stdio.h> int cnt = 0; // 全局变量,用于统计递归函数调用的次数 int fib(int n) { cnt++; if(n <= 2) { return 1; } else { return fib(n - 1) + fib(n - 2); } } int main(){ int n = 0, result = 0; scanf("%d", &n); result = fib(n); printf("FIB(%d) = %d\n", n, result); printf("递归次数=%d\n", cnt); return 0; }- 3.递归经典问题:汉诺塔
#include <stdio.h> int hanoi(int n, char x, char y, char z){ // n:塔的数量 a、b、c位柱子的名称 if(n > 0){ hanoi(n-1, x, z, y); printf("moving from %c to %c\n", x, z); hanoi(n-1, y, x, z); } return 0; } int main(){ int num = 0; char a = 0, b = 0, c = 0; printf("请输入塔的数量和三个柱子的名称:\n"); scanf("%d %c %c %c", &num, &a, &b, &c); hanoi(num, a, b, c); return 0; }
-
3.数组作为函数参数
-
3.1 数组元素作为函数参数
-
值传递:数组元素作为函数参数传递的本质是传递该元素的值,与普通变量作为参数的行为类似,函数内部对参数的修改不会影响原始数组中的元素
-
案例分析
- 1.找出数组中的最大值
#include <stdio.h> int max(int x, int y) { // 计算两数较大值的函数 return x > y? x : y; } int main() { int a[10] = {0}; int i = 0, m = 0; printf("input 10 integers:\n"); for (i = 0; i < 10; i++) { scanf("%d", &a[i]); } m = a[0]; // 假设下标为 0 的元素为最大值,后续遍历下标为 1 ~ N - 1 的元素 for (i = 1; i < 10; i++) { m = max(m, a[i]); // 调用函数 } printf("max is %d", m); return 0; }
-
-
3.2 数组名作为函数参数
-
地址传递
-
因为数组名表示数组的首地址,所以实参传递地址给形参,形参和实参所代表的数组共享同一段内存单元
-
若在函数中改变数组元素值,则修改了内存单元中存储的数据,实参数组对应元素值也会被修改
-
编译器只关心数组的首地址,不关心数组长度,因此会加一个函数形参来接收数组长度
-
-
案例分析
- 1.编写一个函数显示数组的值
#include <stdio.h> void display(int b[], int n) { int i = 0; for (i = 0; i < n; i++) { printf("%4d", b[i]); } } int main() { int a[5] = {10, 20, 30, 40, 50}; display(a, 5); // 数组名作为实参 return 0; }
-
-
3.3 多维数组作为函数参数
-
注意事项
-
数组名作为函数参数传递时传递的是数组的首地址
-
函数形参若是二维数组,可省略第一维的大小,不能省略第二维的大小,否则函数无法确定所传数组的行列数
-
实参数组可以与形参数组的第一维大小不同,在函数形参中常添加一个整型参数表示二维数组第一维的大小
-
-
案例分析
- 1.将一个
4 * 4的矩阵转置
// 分析:矩阵转置就是第i行第j列的元素与第j行第i列的元素交换 #include <stdio.h> void reverse(int b[][4], int n) { // 实现矩阵转置 int i = 0, j = 0, temp = 0; for (i = 0; i < n; i++) { for (j = 0; j <= i; j++) { // 只遍历数组主对角线及左下角的元素 temp = b[i][j]; bi[][j] = b[j][i]; b[j][i] = temp; } } } void printfarr(int a[][4], int n) { // 实现打印数组元素 int i = 0, j = 0; for(i = 0; i < 4; i++) { for(j = 0; j < 4; j++) { printf("%4d", a[i][j]); } printf("\n"); } } int main() { int a[4][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}}; printf("输出转置前的矩阵:\n"); printfarr(a, 4); reverse(a, 4); printf("\n输出转置后的矩阵:\n"); printfarr(a, 4); return 0; } - 1.将一个
-
4.变量的作用域
-
4.1 局部变量
-
定义:在函数内部或代码块中(
{}内部)声明的变量,包括函数的形参 -
作用域:变量所在的局部范围;仅在包含该变量声明的函数中才起作用,在函数外不能使用
-
注意事项
-
1.局部变量的生命周期为从定义开始,到函数或代码块结束
-
2.在程序的各个部分允许使用同名变量,实参和形参名也允许同名,如案例分析3
-
3.
main()函数中的变量也是局部变量 -
4.允许在复合语句(分程序或程序块)中定义变量,它仅在复合语句内有效,如案例分析1
-
-
案例分析
- 1.分析以下代码的执行情况
#include <stdio.h> int main(int argc, const char * argv[]) { int i = 0; // i 的作用域是整个 main() 函数 for (i = 0; i < 3; i++) { ; } printf("%d\n", i); return 0; } // 可正常编译运行 #include <stdio.h> int main(int argc, const char * argv[]) { for (int i = 0; i < 3; i++) { ; } printf("%d\n", i); // 超出了i的作用域 return 0; } // 编译报错,提示变量 i 未声明 // 变量 i 在for循环中声明,因此其生效范围仅在 for 循环中![image]()
- 2.分析以下代码的执行情况
#include <stdio.h> void local_value() { int a = 1, b = 2; // a、b的作用域仅限于local_value函数中 printf("%d %d", a, b); } int main(int argc, const char * argv[]) { printf("%d %d", a, b); return 0; } // 编译报错,main() 函数中并未定义变量 a、b![]()
- 3.分析以下代码的执行情况
#include <stdio.h> void local_value() { int a = 1, b = 2; // a、b的作用域仅限于local_value函数中 printf("%d %d", a, b); } int main(int argc, const char * argv[]) { int a = 5; // a、b的作用域仅限于main()函数中 float b = 2.5; printf("%d %f\n", a, b); return 0; } // 可正常编译运行 -
函数的地址
-
定义:函数名本质上是指向函数代码起始地址的常量指针,可通过打印函数名或使用函数指针来获取函数的地址
-
与变量作用域的关系:正是因为不同函数在内存中的地址不同,即占用的内存空间不同,所以才允许在不同函数中使用相同的变量名
![]()
-
案例分析
#include <stdio.h> void local_value() { int a = 1, b = 2; printf("%d %d", a, b); } int main(int argc, const char * argv[]) { int a = 5; float b = 2.5; printf("main函数的地址为: %p\n", (void *)main); // 将函数指针强制转换为void*类型以匹配%p格式说明符 printf("local_value函数的地址为: %p\n", (void *)local_value); return 0; }![]()
-
-
-
4.2 全局变量
-
定义:在函数外部(
{}外部)定义的变量,也称外部变量或全程变量 -
作用域:整个工程;从定义位置到文件结束,在这个范围的任何地方都可使用,包括函数内
-
注意事项
-
1.全局变量的生命周期即整个程序的生命周期
-
2.全局变量的作用域使得其值容易修改,降低了程序的可读性,容易产生逻辑错误
- 模块之间高耦合度导致某个模块的修改牵涉到其他模块修改,增加了代码的脆弱性和复杂性
-
3.允许局部变量和全局变量重名但不建议这么做,在局部变量作用域内全局变量被"屏蔽"
-
4.程序设计要满足"高内聚,低耦合",全局变量破坏了这一要求
-
-
案例分析
- 1.分析以下代码的执行情况
#include <stdio.h> int a = 5, b = 10; // 全局变量a、b void modify() { int a = 5, b = 10; // 局部变量a、b,此时全局变量a、b被屏蔽 a++; b--; printf("pos1: a=%d, b=%d\n", a, b); // a的值为 6,b的值为 9 } int main(int argc, const char * argv[]) { modify(); printf("pos2: a=%d, b=%d\n", a, b); // a的值为 5,b的值为 10 return 0; }
-
5.变量的存储类别
-
5.1 存储方式
-
时空分析
-
时间角度:生存期,静态存储方式和动态存储方式
-
空间角度:作用域,局部变量和全局变量
-
-
内存划分
-
栈区
-
存放局部变量和函数调用时的上下文信息,如现场保护、返回地址等
-
容量有限,递归过深或局部变量过大都可能会导致栈溢出
-
-
堆区
-
程序运行时动态分配的内存(通过
malloc/calloc/realloc申请) -
适合存储生命周期不确定的数据,如链表、动态数组
-
程序员负责申请和释放(
free)内存,忘记释放会导致内存泄露
-
-
静态区
-
存放静态变量和全局变量
-
程序执行期间始终占据固定存储单元
-
-
-
变量属性
-
数据类型:定义变量的取值范围和允许的操作
-
存储类别:数据在内存中存储分配和释放的时机以及存储结构
-
-
-
5.2 自动变量
-
定义:函数中的局部变量未加
static声明就属于自动变量,包括函数内的局部变量、函数形参和复合语句中的变量 -
注意事项
-
1.调用函数时为自动变量分配内存,调用结束后释放内存
-
2.声明关键字为
auto,如auto int z;,可省略auto -
3.
C 标准(如 C89、C99、C11)明确规定,函数形参列表中不能显式使用存储类别说明符,如auto
-
-
-
5.3 静态变量
-
1.静态局部变量
-
定义:使用
static关键字修饰的局部变量 -
特征:属于静态存储类型,在静态存储区分配内存;程序运行之初就分配内存,而非函数调用时,运行结束后释放
-
适用场合
-
1.保留调用后的变量值
-
2.确定某函数的调用次数
-
-
注意事项
-
1.未初始化的静态局部变量系统自动为其初始化为0
-
2.静态局部变量的初始化在编译时进行,可通过汇编代码查看,静态局部变量的部分没有汇编代码。也可以分步调试查看,调试期间会自动跳过静态变量所属语句
![]()
-
3.静态局部变量在程序运行期间都存在,作用域仍为函数内部
-
4.静态局部变量长期占用内存,应尽量少用
-
-
案例分析
- 1.分析以下代码的执行情况
#include <stdio.h> int i = 0; void sub() { int a = 0; static int b = 0; // 静态局部变量b,保留上一次函数调用后的值 a++; b++; printf("%d: a=%d, b=%d\n", i, a, b); } int main() { for(i = 1; i <= 3; i++) { sub(); } return 0; } // 1: a=1, b=1 // 2: a=1, b=2 // 3: a=1, b=3
-
-
2.静态全局变量
-
全局变量的外部链接属性变成了内部链接属性
-
其他
.c源文件不能再使用该变量,使用时感觉作用域变小
-
-
3.静态函数
-
定义:用
static关键字修饰的函数 -
特征:只能在定义它的源文件内部被调用,其他源文件无法对其调用,即便其他文件使用
extern声明也不行 -
注意事项
-
不同源文件有同名的静态函数并不会引发冲突,因为它们的作用域相互独立
-
静态函数具有内部链接属性,它不会被链接器导出,不会和其他文件中同名的函数产生冲突
-
-
-
-
5.4 寄存器变量
-
定义:存储在CPU的寄存器中的变量,用
register修饰 -
作用:访问寄存器的速度远高于访问内存的速度,因此能够提高程序运行效率
-
注意事项
-
1.只有局部变量和形式参数才允许声明为寄存器变量,通过不断分配与释放寄存器可提高寄存器利用率
-
2.寄存器变量的数量受到计算机寄存器数目的限制
-
3.多数系统只允许定义
int、char和指针类型变量为寄存器变量;多数系统将register作为自动变量处理,存储在内存中 -
4.若部分未定义为寄存器类别的变量使用频繁,编译系统自动将之存放在寄存器中
-
-
-
5.5 外部变量
6.函数应用实例
- 1.将序列的最大值和第 1 个数交换,最小值和最后一个数交换,并输出新序列
// 分析:遍历数组,定位最大值和最小值的下标,完成交换
// 注意1:代码通过最值的下标定位到最值,而不是直接记录最值的数值,因为数组中对元素的引用借助下标实现
// 注意2:第1个数恰好是最小值的特殊情况
#include <stdio.h>
void exchange(int b[], int n) { // 完成交换的核心代码
// max 为最大值下标,min 为最小值下标,初值均为0,即假设下标为 0 的数为最大/小值
int max = 0, min = 0;
int i = 0, temp = 0;
for (i = 1; i < n; i++) {
if (b[i] > b[max]) {
max = i;
}
if (b[i] < b[min]) {
min = i;
}
}
printf("最大值为 %d, 最小值为 %d\n", b[max], b[min]);
// 最大值和下标为 0 的数交换
temp = b[0];
b[0] = b[max];
b[max] = temp;
// 若下标为 0 的位置恰好是最小值,它被替换到了下标为 max 的位置
if (0 == min) {
min = max;
}
// 最小值和下标为 n - 1 的数交换
temp = b[n - 1];
b[n - 1] = b[min];
b[min] = temp;
}
void print_arr(int a[], int n) { // 完成打印数组元素的核心代码
int i = 0;
while (i < n) {
printf("%4d", a[i]);
i++;
}
printf("\n");
}
int main() {
int a[10] = {-5, 3, 8, 9, -1, -3, 5, 6, 0, 4};
int i = 0;
printf("交换前: ");
print_arr(a, 10);
exchange(a, 10);
printf("\n交换后: ");
print_arr(a, 10);
}
- 2.编写一个函数,在一个整数序列中查找某个整数,若存在,返回该整数在序列中的位置;否则返回 -1 。要求在主函数中调用,输出结果
// 分析:遍历数组,逐个对比当前数组元素与目标值看是否相等
// 本题可延伸至二分查找,具体参考案例分析4
#include <stdio.h>
int search(int a[], int n, int target) { // 实现查找的核心代码
int i = 0;
for (i = 0; i < n; i++) {
if (a[i] == target) {
return i; // 找到目标,返回下标
}
}
return -1; // 未找到目标
}
int main() {
int arr[] = {10, 20, 30, 40, 50};
int size = sizeof(arr) / sizeof(arr[0]);
int target = 30;
int result = search(arr, size, target);
if (result != -1) {
printf("目标整数 %d 在数组中的位置是: %d\n", target, result);
}
else {
printf("目标整数 %d 不在数组中\n", target);
}
return 0;
}
- 3.编写一个函数,将字符串转换成相应整数。转换时,遇到非数字字符停止转换。要求在主函数中调用,输出结果。例如:"123"——>123;"12a3"——>12;"a123"——>0
#include <stdio.h>
#include <ctype.h>
int convert(char s[]) {
int result = 0;
int i = 0;
// 跳过前导空格
while (s[i] == ' ') {
i++;
}
// 处理正负号(默认为正数)
int sign = 1;
if (s[i] == '-') {
sign = -1;
i++;
}
else if (str[i] == '+') {
i++;
}
// 转换数字字符,遇到非数字则停止
while (isdigit(s[i])) {
result = result * 10 + (s[i] - '0');
i++;
}
return sign * result;
}
int main() {
char test1[] = "123";
char test2[] = "12a3";
char test3[] = "a123";
char test4[] = " -456xyz";
char test5[] = " +789";
printf("%s -> %d\n", test1, convert(test1));
printf("%s -> %d\n", test2, convert(test2));
printf("%s -> %d\n", test3, convert(test3));
printf("%s -> %d\n", test4, convert(test4));
printf("%s -> %d\n", test5, convert(test5));
return 0;
}
- 4.现有一按升序排列的数组
{3, 9, 10, 13, 16, 27, 30},输入正整数x,使用二分查找的方式查询它是否在数组中
// 代码1
#include <stdio.h>
int main(int argc, const char * argv[]) {
// insert code here...
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int k = 17, sz = sizeof(arr) / sizeof(arr[0]);
int left = 0, right = sz - 1;
while (left <= right) { // 中间有元素被查找,要带等于
// int mid = (left + right) / 2; // mid的求取在循环内
int mid = left + (right - left) / 2; // 避免left + right越界溢出,将right比left多的部分的一半再加到left上,这样两者就一样了
if (arr[mid] < k) {
left = mid + 1;
}
else if (arr[mid] > k) {
right = mid - 1;
}
else {
printf("找到了, 下标是: %d\n", mid);
break;
}
}
if (left > right) {
printf("找不到\n");
}
return 0;
}
// 代码2
#include <stdio.h>
int main(){
int a[7] = {3, 9, 10, 13, 16, 27, 30};
int left = 0, right = 6;
int mid = 0, x = 0;
printf("请输入要查找的数:\n");
scanf("%d", &x);
while(left <= right){
mid = (left + right) / 2;
if(a[mid] > x){
right = mid - 1;
}
else if(a[mid] < x){
left = mid + 1;
}
else{
printf("查找的数据在第%d个\n", mid + 1);
return mid;
}
}
printf("查找的数据不存在!\n");
return 0;
}
// 代码3
#include <stdio.h>
int binary_sort(int s[], int n, int x){
int left = 0, right = n - 1, mid;
while(left <= right){
mid = (left + right) / 2;
if(s[mid] > x){
right = mid - 1;
}
else if(s[mid] < x){
left = mid + 1;
}
else{
return mid;
}
}
return -1;
}
int main(){
int a[7] = {3, 9, 10, 13, 16, 27, 30};
int index = 0, x = 0;
printf("请输入要查找的数:\n");
scanf("%d", &x);
index = binary_sort(a, 7, x);
if(index >= 0){
printf("要查找的 %d 下标为 %d\n", x, index);
}
else {
printf("查找的数据不存在!\n");
}
return 0;
}







浙公网安备 33010602011771号