》第7章 用函数实现模块化程序设计:知识要点、案例分析与课后习题解析)
1. 为什么需要函数学习 C 语言初期我们习惯把全部代码写进main函数。随着程序越来越复杂main会膨胀到几百行甚至上千行可读性、可维护性和复用性都会急剧下降。第 7 章引入的「函数」本质上就是把一个较大的程序按功能拆分成若干相对独立的小模块每个模块用一个函数实现。模块化程序设计带来的好处可以归纳为四点逻辑清晰每个函数只解决一个具体问题主函数像「总调度」调用顺序一目了然。代码复用同一个函数可以在不同地方被反复调用避免复制粘贴。便于分工与维护多人可以并行开发不同函数改一个模块时不影响其他模块。易于调试把程序隔离成小函数后可以单独测试每个函数是否正确。用一个简单的类比main相当于公司老板各个函数相当于不同部门的员工老板不关心每个员工内部如何实现只关心「交给他一个任务他能否返回一个结果」。2. 函数定义的一般形式2.1 无参函数的定义类型名 函数名(void){声明部分 语句部分}例如voidprintStar(void){printf(************\n);}2.2 有参函数的定义类型名 函数名(形式参数列表){声明部分 语句部分}例如求两个整数中较大值的函数intmax(intx,inty){intz;zxy?x:y;returnz;}注意几个要点函数类型即函数返回值的类型。如果函数不需要返回值应声明为void。形参定义函数时括号里的参数叫「形式参数」是函数内部的局部变量只有在函数被调用时才分配内存。return 语句用于把结果返回给调用者。void函数可以只写return;或省略不写。3. 函数的声明与调用3.1 函数声明C 语言要求「先声明、后使用」。如果函数定义出现在调用之后就必须在调用之前进行函数声明也叫函数原型作用是告诉编译器函数的名字、参数类型和返回值类型。intmax(intx,inty);/* 函数声明末尾有分号 */声明中形参名可以省略只保留类型intmax(int,int);3.2 函数调用函数名(实际参数列表)实参可以是常量、变量或表达式。例如amax(3,5);bmax(a2,c*3);调用过程可以概括为实参的值传给形参 → 执行函数体 → 返回结果给调用点。4. 函数的参数传递值传递C 语言中实参向形参传递数据的方式是单向的值传递。也就是把实参的值复制一份给形参形参在函数内部如何变化都不会反过来影响实参。经典的「交换两个数」反例可以很好地说明这一点#includestdio.hvoidswap(intx,inty){inttmp;tmpx;xy;ytmp;printf(swap 内部: x%d, y%d\n,x,y);}intmain(void){inta10,b20;swap(a,b);printf(main 内部: a%d, b%d\n,a,b);return0;}运行结果swap 内部: x20, y10 main 内部: a10, b20可以看到虽然swap内部交换了x和y但main中的a、b并没有改变。这是因为a、b只把自己的值复制给了x、y二者存储在不同的内存单元中。如果想真正修改主调函数中的变量需要借助指针第 8 章内容例如swap(a, b)。本章应重点理解「值传递是单向的」这一结论。5. 函数的返回值函数可以返回一个结果语法为return表达式;注意事项返回值的类型应与函数定义时的类型一致。类型不一致时会尝试隐式转换。一个函数可以有多个return语句但每次调用只会执行其中一个。比如intabsValue(intn){if(n0)returnn;elsereturn-n;}void函数不能返回具体的值。6. 函数的递归调用一个函数在它的函数体内直接或间接调用自身称为递归调用。递归是本章的难点也是许多初学者的「拦路虎」。6.1 递归的两个必要条件存在递归终止条件使递归在有限次调用后结束每次递归调用都向终止条件逼近。否则递归会无限进行最终导致栈溢出。6.2 经典问题求 n!#includestdio.hlongfact(intn){if(n0)return-1;/* 错误处理 */if(n0||n1)/* 递归终止条件 */return1;returnn*fact(n-1);/* 递归调用向终止条件逼近 */}intmain(void){intn5;printf(%d! %ld\n,n,fact(n));return0;}以fact(5)为例调用过程依次展开为fact(5) 5 * fact(4) 5 * 4 * fact(3) 5 * 4 * 3 * fact(2) 5 * 4 * 3 * 2 * fact(1) 5 * 4 * 3 * 2 * 1 1206.3 递归与迭代的对比对比项递归迭代循环实现难度代码简洁、逻辑直观代码相对繁琐资源开销每次调用占用栈空间开销较大只使用少量固定变量适用场景问题天然具有递归结构大多数常规循环问题实际工程中递归如果层级过深容易栈溢出能用简洁迭代解决的问题未必需要递归。但像汉诺塔、树的遍历等问题递归表达明显更清晰。7. 数组作为函数参数7.1 数组元素作为实参数组元素本质上是一个普通变量可以像变量一样作为实参inttmax(a[0],a[1]);7.2 一维数组名作为函数参数这是一个非常重要的考点数组名作为函数参数时传递的是数组首元素的地址而不是复制整个数组。因此函数内部对数组元素的修改会直接影响实参数组。#includestdio.h/* 求一维数组平均值 */floataverage(floatarr[],intn){floatsum0;inti;for(i0;in;i)sumarr[i];returnsum/n;}/* 把数组元素从大到小排序 */voidsortDesc(inta[],intn){inti,j,t;for(i0;in-1;i)for(ji1;jn;j)if(a[i]a[j]){ta[i];a[i]a[j];a[j]t;}}说明形参可以写成int a[]也可以写成int a[10]但 C 语言并不会真的检查数组长度写[]更通用。数组名作为参数时不再进行「值传递」函数内可修改原数组这一点和普通变量参数有着本质区别。8. 局部变量与全局变量8.1 局部变量在函数内部包括形参、复合语句内定义的变量叫局部变量。其特点是只在定义它的函数或复合语句范围内有效不同的函数可以定义同名局部变量互不干扰形参属于局部变量。voidf1(void){inta;/* f1 的局部变量 */}voidf2(void){inta;/* 与 f1 中的 a 互不影响 */}8.2 全局变量在函数外部定义的变量叫全局变量其作用范围从定义位置开始到本源文件结束。全局变量可以被其后的多个函数共享用于在函数之间传递数据。#includestdio.hintglobal100;/* 全局变量 */voidprintGlobal(void){printf(global%d\n,global);}intmain(void){printGlobal();/* 输出 100 */global200;/* 修改全局变量 */printGlobal();/* 输出 200 */return0;}建议全局变量会增加函数之间的耦合使程序难以理解和维护因此应尽量少用。非必要不定义全局变量优先通过参数和返回值传递数据。9. 变量的存储类别除了「作用域」变量还有「生存期」的概念。按存储类别可分为动态存储和静态存储。类别关键字生存期常见说明自动变量auto函数调用期间默认局部变量可省略 auto静态局部变量static程序运行期间函数退出后值仍保留只初始化一次寄存器变量register函数调用期间建议放入寄存器现代编译器会自行优化静态全局变量static程序运行期间作用域限定在本文件外部变量extern程序运行期间可引用其他文件的全局变量静态局部变量是最常考的考点看下面的例子#includestdio.hvoidcounter(void){staticintcount0;/* 只初始化一次 */count;printf(count %d\n,count);}intmain(void){counter();/* count 1 */counter();/* count 2 */counter();/* count 3 */return0;}如果去掉static每次调用count都会重新初始化为 0三次输出都是 1。对比记忆即可掌握。10. 内部函数与外部函数按照函数能否被其他源文件调用分为两类类型关键字说明内部函数static只能被本文件中的函数调用外部函数extern可以被其他文件中的函数调用默认即为 externstaticintinternalFun(void)/* 内部函数 */{return1;}externintexternalFun(void)/* 外部函数extern 可省略 */{return2;}在多文件程序中不希望外部文件访问的函数应加上static加以保护提高模块的封装性。11. 案例分析案例 1求两个数的最大公约数与最小公倍数利用辗转相除法欧几里得算法设计递归函数求最大公约数#includestdio.h/* 递归求最大公约数 */intgcd(inta,intb){if(b0)returna;returngcd(b,a%b);}/* 最小公倍数 两数之积 / 最大公约数 */intlcm(inta,intb){returna*b/gcd(a,b);}intmain(void){intm24,n36;printf(gcd(%d,%d)%d\n,m,n,gcd(m,n));printf(lcm(%d,%d)%d\n,m,n,lcm(m,n));return0;}运行结果gcd(24,36)12 lcm(24,36)72该案例说明递归并不一定只用于阶乘、斐波那契这类「教科书例题」辗转相除法同样具有清晰的递归结构。案例 2判断素数把判断素数的逻辑封装成函数主函数只负责遍历和输出#includestdio.h#includemath.hintisPrime(intn){inti;if(n2)return0;for(i2;isqrt(n);i)if(n%i0)return0;return1;}intmain(void){inti,count0;for(i100;i200;i){if(isPrime(i)){printf(%5d,i);count;if(count%80)printf(\n);}}printf(\n100~200 之间的素数个数为: %d\n,count);return0;}案例 3数组元素逆置通过函数反转一维数组验证「数组名作为参数会修改原数组」的特性#includestdio.hvoidreverse(inta[],intn){intleft0,rightn-1,tmp;while(leftright){tmpa[left];a[left]a[right];a[right]tmp;left;right--;}}intmain(void){inta[6]{1,2,3,4,5,6};inti;reverse(a,6);for(i0;i6;i)printf(%d ,a[i]);printf(\n);return0;}输出结果6 5 4 3 2 1reverse函数没有返回值却成功改变了main中的数组这正是因为数组名传递的是地址。12. 课后习题精选解析习题 1编写函数求两个整数之和#includestdio.hintadd(intx,inty){returnxy;}intmain(void){inta,b;printf(请输入两个整数: );scanf(%d %d,a,b);printf(和为: %d\n,add(a,b));return0;}此题考查的是最基本的函数定义、形参和返回值。习题 2用递归求 Fibonacci 数列第 n 项#includestdio.hintfib(intn){if(n1||n2)return1;returnfib(n-1)fib(n-2);}intmain(void){intn;printf(请输入 n: );scanf(%d,n);printf(fib(%d)%d\n,n,fib(n));return0;}提醒这种递归写法虽然直观但存在大量重复计算复杂度接近指数的增长。求较大项时应改用循环intfib2(intn){inta1,b1,c,i;if(n1||n2)return1;for(i3;in;i){cab;ab;bc;}returnb;}习题 3求一维数组的最大值和最小值#includestdio.hintgetMax(inta[],intn){intmaxa[0],i;for(i1;in;i)if(a[i]max)maxa[i];returnmax;}intgetMin(inta[],intn){intmina[0],i;for(i1;in;i)if(a[i]min)mina[i];returnmin;}intmain(void){inta[8]{12,5,78,34,9,56,23,41};printf(最大值: %d\n,getMax(a,8));printf(最小值: %d\n,getMin(a,8));return0;}习题 4输出 3×3 矩阵转置后的结果#includestdio.hvoidtranspose(inta[3][3]){inti,j,tmp;for(i0;i3;i)for(ji1;j3;j){tmpa[i][j];a[i][j]a[j][i];a[j][i]tmp;}}voidprintMatrix(inta[3][3]){inti,j;for(i0;i3;i){for(j0;j3;j)printf(%4d,a[i][j]);printf(\n);}}intmain(void){inta[3][3]{{1,2,3},{4,5,6},{7,8,9}};printf(原矩阵:\n);printMatrix(a);transpose(a);printf(转置后:\n);printMatrix(a);return0;}二维数组作为函数参数时行数可省略但列数不能省略这是初学者极易写错的地方。13. 本章知识小结知识点核心结论函数定义由返回类型、函数名、形参列表和函数体组成函数声明调用前先声明声明以分号结尾形参名可省略参数传递普通变量为单向值传递数组名为地址传递递归必须有终止条件并逐步向终止条件逼近数组作参数一维数组a[]、二维数组a[][列数]列数不能省局部/全局变量局部变量函数内有效全局变量可跨函数共享存储类别auto、static、register、extern需分清生存期与作用域内部/外部函数static限本文件extern可跨文件掌握本章后应当能自觉地把程序按功能拆分逐步建立「自顶向下、逐步细化」的模块化思维为后续学习指针、结构体和文件操作打下坚实基础。