第5章 函数与模块化设计 函数是程序设计语言的重要里程碑之一,它标志着程序模块化设计和软件重用的开始。 简单地说,模块化设计就是把一个复杂的问题按照功能划分为若干简单的功能模块,以模 块为单位进行程序设计。模块化的目的是降低程序复杂度,使程序设计、调试和维护等操作 简单化。一般将复杂问题划分为不同的模块后更适合软件的团队开发,不同的模块由不同 的程序员设计,只需要确定好模块之间的接口关系,就可以实现模块之间的相互调用,而模 块内部的具体实现可以由程序员自己设计。在C语言中,模块是通过函数来实现的。 本章主要介绍函数的定义、函数中参数的传递、函数的调用、全局变量与局部变量的 作用域。并通过大量的例题分析,让读者可以灵活、熟练地掌握函数(包括递归函数)的定 义与调用。 5.1 概述 在前面的章节中,学习了如何编写一些简单的C程序。在这些程序中,经常调用诸 如scanf()、printf()之类的系统函数。这些函数都是C语言的标准库函数,是系统预先定 义好的,可以实现输入输出等基本功能。C语言中提供了许多标准的库函数,存放在不同 的头文件(以h为扩展名)中,如数学函数放在math.h头文件中,字符串函数放在string.h头 文件中等。用户在使用时,只需要通过#include<头文件名>将其包含到程序中即可。 但标准的库函数并不能满足用户所有的需求,用户还可以根据程序的需要自定义函数,通 过对函数的调用来完成相应的功能。 例5-1 计算组合数。 【问题描述】 C(n,m )表示从n 个数中选择m 个的组合数。计算公式为 C(n,m )= n! m !(n -m )! 计算C(n,m )。 【输入格式】 第一行是正整数N ,表示有N 组要计算的组合数。接下来N 行,每行两个整数n, m (0≤m ≤n≤20)。 【输出格式】 输出N 行。每行输出一个整数表示C(n,m )。 【样例输入】 321 166 3 2 4 0 【样例输出】 231 【问题分析】 根据C(n,m )的定义,C(n,m )的计算过程实际上是3个阶乘的计算。根据第3章计 算阶乘的方法,编写相关的程序。 【参考代码1】 1. #include<bits/stdc++.h> 2. using namespace std; 3. int main() 4. { 5. int num,n,m,i; 6. long long ans,fac1,fac2,fac3; 7. cin>>num; 8. while(num){ 9. cin>>n>>m; 10. fac1=fac2=fac3=1; 11. for(i=1;i<=n;i++) 12. fac1*=i; 13. for(i=1;i<=m;i++) 14. fac2*=i; 15. for(i=1;i<=n-m;i++) 16. fac3*=i; 17. ans=fac1/(fac2*fac3); 18. cout<<ans<<endl; 19. num--; 20. } 21. return 0; 22. } 【代码分析】 程序中通过3次循环分别求出n!、m !和(n-m )!。而这3次循环中,除了循环的次 数(即终值)不同以外,其他的代码完全一样。为减少代码的重写,可以把这部分通用代码 从程序中抽出来,利用函数来实现。参考如下代码。 【参考代码2】 1. #include<bits/stdc++.h> 2. using namespace std; 167 3. long long fac(int n){ 4. long long ans=1; 5. int i; 6. for(i=1;i<=n;i++) 7. ans*=i; 8. return ans; 9. } 10. int main() 11. { 12. int num,n,m; 13. long long ans; 14. cin>>num; 15. while(num){ 16. cin>>n>>m; 17. ans=fac(n)/(fac(m)*fac(n-m)); 18. cout<<ans<<endl; 19. num--; 20. } 21. return 0; 22. } 【代码分析】 从例中可以看出,自定义函数fac()在主函数中被调用了3次,这样处理有两个优点: ①可以省略重复代码的编写,使程序的逻辑结构更加清楚;②利用函数可以使程序更加 模块化,程序员可以把主要精力投入程序的整体设计。 对于函数的使用,有以下3点需要说明。 (1)从用户使用的角度看,函数包括两种:①由系统提供的、无须用户定义的标准函 数,这些函数根据不同的功能存放在不同的头文件中;②用户根据需要编写具有特定功 能的自定义函数。 (2)C语言中所有的函数都是平行的,它们之间都是互相独立的。函数只能嵌套调 用,不能嵌套定义。 (3)一个源程序文件中,可以由一个或多个函数及其他有关内容(如数据声明与定 义)组成,但必有一个main()函数。程序总是从main()函数开始和结束的。 5.2 函数的定义与调用 5.2.1 函数的定义 在例5-1中定义了fac()函数,具体如下: 1. long long fac(int n){ 168 2. long long ans=1; 3. int i; 4. for(i=1;i<=n;i++) 5. ans*=i; 6. return ans; 7. } 在该函数中,fac是函数名,它的命名规则应当满足C语言中标识符的命名规则,主 调函数通过函数名来完成对函数的调用;第一个longlong代表函数的返回类型,原则上 它应当与函数内部的返回值ans类型保持一致;intn代表函数的形参,是函数与其他函 数的接口,用户通过该接口向函数传递参数;由花括号括起来的部分为函数体,完成具体 的操作。 函数定义的一般形式如下: 数据类型函数名(数据类型形参1, 数据类型形参2,…, 数据类型形参n) { 函数体 } 函数体包括函数的声明部分和执行部分。例如,可以定义如下的函数: 1. int test(int n){ 2. int k; //变量的声明 3. k=n+1; 4. return k; 5. } 在对函数进行定义时,有以下4点需要注意。 (1)在对函数的形参进行定义时,需要声明每个变量的类型,不能像通常的变量声明 那样,使用变量列表,如: int max(int a, int b) //正确的参数定义 int max(int a,b) //错误的参数定义 (2)函数的返回类型原则上应与函数体内return语句中表达式的类型保持一致,如 果二者出现不一致的情况,以函数的返回类型为准。当数据的类型是数值型时,系统会进 行自动类型转换。 (3)如果函数不需要返回值,函数的返回类型可定义为空类型void。此时函数中可 以没有return语句,或有return语句,但语句后没有表达式,即“return;”。 (4)在一个函数中可以有多个return语句,但只要执行到其中一个return语句,就 结束该函数的调用。例如,求最大值的函数可以定义如下: 1. int max(int x, int y){ 2. if(x>y) 169 3. return x; 4. return y; 5. } 例5-2 闰年判断。 【问题描述】 给定一个年份,判断这一年是不是闰年。 【输入格式】 输入包含一个整数y,表示当前的年份。 【输出格式】 输出一行,如果给定的年份是闰年,则输出yes,否则输出no。 【样例输入】 2013 【样例输出】 no 【问题分析】 设计函数时,被调函数与主调函数之间的参数传递是非常关键的,简单地将主调函数 要实现的工作放在函数里实现是没有任何意义的。以本题为例,用户要输入的年份可以 在主调函数里实现,被调函数的工作是判断给定的年份是不是闰年,可以通过返回0或1 来表示。 【参考代码】 1. #include<bits/stdc++.h> 2. using namespace std; 3. int judge(int year){ 4. if((year%4==0 && year%100!=0)||(year%400==0)) 5. return 1; 6. else 7. return 0; 8. } 9. int main(){ 10. int year; 11. cin>>year; 12. if(judge(year)==1) 13. cout<<"yes"; 14. else 15. cout<<"no"; 16. return 0; 17. } 例5-2 170 【代码分析】 在上述程序中,函数judge()的功能相对单一,判断给定的年份是不是闰年,年份的输 入以及程序的输出由主调函数实现。此外,在judge()函数中,通过return返回函数运行 结果,由于函数遇到return会自动结束,因此上述程序中第6行的else可以省略。 例5-3 求最大值。 【问题描述】 输入3个实数,求其中的最大值,使用函数编程解决。 【样例输入】 1.1 2.2 3.3 【样例输出】 3.3 【参考代码】 1. #include<bits/stdc++.h> 2. using namespace std; 3. double imax(double a, double b){ 4. float c; 5. if(a>b) 6. c=a; 7. else 8. c=b; 9. return c; 10. } 11. int main(){ 12. double a,b,c; 13. cin>>a>>b>>c; 14. cout<<imax(imax(a,b),c); 15. return 0; 16. } 【代码分析】 程序运行时,如果输入3个double型的实数1.1、2.2和3.3,由于函数返回值的数据 类型与函数类型不一致,函数imax()将返回值的类型自动从float型变为double型,上述 程序输出的值是3.3。 5.2.2 函数的调用 对函数进行正确的定义以后,就可以在主函数或其他函数中对函数进行调用。在 例5-1中,通过fac(n)、fac(m)、fac(n-m)完成对函数的调用。函数调用的一般形式如下: 171 函数名(实参1, 实参2,…, 实参n); 例如,调用函数test()时,只需要在主函数中加入一行“test(4);”即可。 在进行函数调用时,需要注意以下3点。 (1)函数的参数有两种:定义函数时使用的参数称为形参,调用函数时使用的参数 称作实参。形参必须是变量,而实参可以是常量、变量、表达式或函数,但必须有确定的 值。例如: c=max(9,5); //实参是常量 c=max(x,y); //实参是变量 c=max(x+2,y*3); //实参是表达式 c=max(max(x,y),z); //求3 个数的最大值,实参是函数 (2)实参与形参的顺序和类型应保持一致。 (3)执行函数调用时,形参和实参是两个不同的存储单元。具体地讲,定义函数时, 形参并不占用内存存储单元;进行函数调用时,函数的形参被临时分配内存单元;调用结 束时,形参占用的内存存储单元被释放,但实参单元仍然保留原值,没有改变。 例5-4 判断完全平方数。 【问题描述】 写一个判断平方数的函数,判断输入的x是否为平方数并输出结果。 【输入格式】 有多组数据,每组输入一个整数x。 【输出格式】 每组输出一行,如果x是平方数输出1,否则输出0。 【样例输入】 1 25 99 【样例输出】 110 【问题分析】 判断一个整数是不是完全平方数主要有两种方式:一是判断它是否为某个数的平 方;二是基于完全平方数是从1开始的若干奇数的和这个结论进行判断。 【参考代码1】 1. #include<bits/stdc++.h> 2. using namespace std; 3. int judge(int n){ 例5-4 172 4. int i; 5. i=(int)sqrt(n); 6. return i*i==n; 7. } 8. int main() 9. { 10. int n; 11. while(cin>>n){ 12. if(judge(n)==1) 13. cout<<1<<endl; 14. else 15. cout<<0<<endl; 16. } 17. return 0; 18. } 【参考代码2】 1. #include<bits/stdc++.h> 2. using namespace std; 3. int judge(int n){ 4. int i=1; 5. while(n>0){ 6. n-=i; 7. i+=2; 8. } 9. return n==0; 10. } 11. int main() 12. { 13. int n; 14. while(cin>>n){ 15. if(judge(n)==1) 16. cout<<1<<endl; 17. else 18. cout<<0<<endl; 19. } 20. return 0; 21. } 5.2.3 函数的声明 C语言中,在一个函数(称为主调函数)中可以调用另一个函数(称为被调函数)。在 173 对函数进行调用时,首先被调函数必须是已定义的函数(库函数或用户自定义的函数),并 且对被调函数进行声明。 (1)如果被调函数是库函数,如printf()、scanf()等,应该将调用库函数的信息包含 到本文件中。例如,在调用基本输入输出函数时,应使用语句#include<stdio.h>将头文 件stdio.h中的库函数引入文件中;在调用sqrt()函数时,应使用语句#include<math.h>将 头文件math.h中与数学运算有关的库函数引入文件中。 (2)如果被调函数是用户自定义的函数,有两种处理方式:①被调函数在主调函数 之前进行定义,则不用进行函数声明,主调函数可以直接调用被调函数。②被调函数在主 调函数之后进行定义,则应在主调函数调用之前对被调函数进行函数声明。 函数声明的主要作用是把用户自定义的函数信息通知编译器,包括函数名、参数的个 数、参数的类型等。这样在遇到函数调用时,编译系统可以正确识别函数并且检查相关调 用是否合法。下面对这种处理方式进行详细说明。 在介绍函数声明以前,首先说明一下函数声明的目的。函数声明是对函数基本特征 (包括函数类型、函数名、参数表)的描述,它与函数定义是有区别的。而函数定义除了描 述函数的基本特征外,核心内容是对函数功能的具体描述。函数声明只是描述函数原型 (函数首部称为函数原型),其一般形式如下: 函数类型函数名(形参类型1 形参1,形参类型2 形参2,…); 编译系统在对函数进行编译时,并不检查函数中的参数名,因此有时也将函数原型中 的形参名省略,其形式如下: 函数类型函数名(形参类型1,形参类型2,…); 同时,函数原型中定义的形参类型、形参顺序和形参数目应与函数定义中的这些函数 基本特征保持一致,否则在函数调用时容易产生错误。 例5-5 素数回文数。 【问题描述】 151既是素数又是个回文数,找出某个范围内的素数回文数。 【输入格式】 两个整数a和b(2≤a<b≤10000000)。 【输出格式】 对每组数据,按从小到大输出a,b之间所有满足条件的素数回文数(包括a和b)。 【样例输入】 5 500 【样例输出】 57 11 101 例5-5 174 131 151 181 191 313 353 373 383 【问题分析】 本题对给定的数字进行两个条件的判断:一是素数;二是回文数。对回文数的判断, 可以通过将数字逆序,判断逆序后的数字与给定的数字是否相等进行判断。 【参考代码】 1. #include<bits/stdc++.h> 2. using namespace std; 3. int judge1(int); //函数声明 4. int judge2(int); //函数声明 5. int main() 6. { 7. int a,b,i; 8. cin>>a>>b; 9. for(i=a;i<=b;i++){ 10. if(judge1(i) && judge2(i)) 11. cout<<i<<endl; 12. } 13. return 0; 14. } 15. int judge1(int n){ //判断是否为素数 16. if(n==1 || n==0) 17. return 0; 18. for(int i=2;i*i<=n;i++){ 19. if(n%i==0) 20. return 0; 21. } 22. return 1; 23. } 24. int judge2(int n){ //判断是否为回文数 25. int m=0,temp=n; 26. while(temp){ 27. m=m*10+temp%10; 28. temp/=10; 29. } 30. return m==n; 31. }