1 20 第5 章 函数 为了便于管理和维护,一个大程序一般被划分为若干模块,每个模块实现特定功能,一 个函数就是一个独立的功能模块,学会设计和使用函数很重要。本章详细描述函数的机制, 包括函数定义、函数声明、函数调用、存储类型、参数数目可变的函数、递归函数以及多文件 的C程序。 5.1 模块化程序设计 模块化程序设计就是采用“自顶向下逐步细化”的方式,把一个大的程序按照功能划分 为若干相对独立的模块,每个模块完成一个确定的功能,在这些模块之间建立必要的数据联 系,互相协作完成总的任务。 5.1.1 函数与模块化编程 当设计解决一个复杂问题的程序时,提倡将一个复杂的任务划分为若干子任务,若子任 务较复杂,还可以将子任务继续分解,直到分解成为一些容易解决的子任务为止。每个子任 务设计成一个子程序,称为模块,子程序在程序编制上相互独立,而对数据处理上又相互联 系;完成总任务的程序由一个主程序和若干个子程序组成,主程序起着任务调度的总控作 用,而每个子程序各自完成一个单一的任务。这种自上而下逐步细化的设计方法称为模块 化程序设计方法。 模块化程序设计的优点是:①子程序代码公用,当需要多次完成同样功能时,只需要一 份代码,可多次调用,从而省去重复代码的编写;②程序结构模块化,可读性、可维护性及可 扩充性强;③简化程序的控制流程,程序编制方便,易于修改和调试。 在C语言中,子程序称为函数,主程序称为主函数,一个解决实际问题的C程序由一个 主函数和若干个其他函数组成,其他函数将最终直接或间接被主函数调用,以解决总任务。 下面通过一个简单的实例说明模块化设计思想以及函数的设计(即定义)和使用。 【例5.1】 利用函数实现输出所有的水仙花数。 分析:水仙花数是一个三位数,程序需要循环判断100~999的每个数是不是水仙花 数,因此可以将“判断一个数是不是水仙花数”这个功能用函数实现,函数名为isNarcissus, 参数为x,有意义的名字能够清楚地表明函数的功能。main函数每次用不同的x值调用 isNarcissus判断该数是否水仙花数,主程序结构如下: for(a=100;a<1000;a++) { /*枚举每一个三位数*/ if(isNarcissus(a)) /*a 是水仙花数,则输出a*/ printf("%5d",a); } 可以把函数看成一个“黑盒子”,给它一定的输入,它就会产生特定的结果并输出一个值 1 21 给使用者,函数的使用者并不需要考虑黑盒的内部行为。对于函数isNarcissus,使用者需要 给它一个整数,然后它能判断出这个整数是不是水仙花数,并把结果值1(代表是水仙花数) 或0(代表不是水仙花数)给使用者,这个值称为函数的返回值或函数值。 程序每次循环给函数isNarcissus不同的a值,如果a值(如153)是一个水仙花数,则函 数返回值是1,即函数调用表达式isNarcissus(a)的值是1,相当于if(1),那么执行printf语 句输出a值;如果a值不是一个水仙花数,则函数返回值是0,相当于if(0),那么不执行 printf语句。完整的程序如下。 #include int isNarcissus(int x); /*函数原型*/ int main() { int a; for(a=100;a<=999;a++) { if(isNarcissus(a)) /*函数调用*/ printf("%5d",a); } return 0; } /********************************************** 函数功能:判断某整数是不是水仙花数 函数参数:x--待判断的整数 函数返回值:x 是水仙花数,则返回1;否则,则返回0。 ***********************************************/ int isNarcissus(int x) /*函数定义*/ { int i,j,k; int ans=0; /*默认不是水仙花数*/ i=x/100; j=(x-i*100)/10; k=x%10; if(x==i*i*i+j*j*j+k*k*k) ans=1; /*是水仙花数,将ans 设置为1*/ return ans; /*返回结果*/ } 本程序包含两个函数:main 函数和isNarcissus 函数。在main 函数中调用了 isNarcissus函数,因此,main是调用函数,isNarcissus是被调用函数。标识符isNarcissus 在程序中出现了三次,分别出现在函数原型、函数调用和函数定义中。 首先看以下函数定义的头部: int isNarcissus(int x) /*无分号*/ 因为该函数要接收一个整型参数,所以圆括号里包含一个名字为x的int类型变量声 明,也可以使用符合标识符命名规则的其他名字,变量x 称为形式参数。函数调用 1 22 isNarcissus(a)把a的值给x,称函数调用传递一个值,这个值称为实际参数,函数实际上判 断a是不是水仙花数,并将判断结果保存在变量ans中。 函数体部分可以看作一个黑盒子,里面定义的一切变量都是局部的,对调用函数main 来说都是不可见的。变量ans是函数isNarcissus私有的,但是最后的return语句把ans的 值(1或0)返回给了调用函数。下面的if语句相当于将ans的值作为判断条件。 if(isNarcissus(a)) 函数最后如果没有return语句,那么变量ans的值送不出来,调用函数main也就不知 道a到底是不是水仙花数。 在函数main中能用下面的语句来代替if(isNarcissus(a))吗? isNarcissus(a); /*返回值未赋值,被丢弃*/ if(ans) 答案是不行的,因为ans是局限于被调用函数的变量,调用函数main看不见它,不能 使用。由 于函数返回值是1或0,所以变量ans的类型声明为int,函数定义头部的最前面也要 用int指明返回值的类型,两者一般要保持一致。 再看main函数前面的一行: int isNarcissus(int x); /*有分号*/ 它是函数声明语句,又称函数原型,它说明了函数isNarcissus接收一个int类型的参 数,返回一个int类型的值。编译器在调用函数isNarcissus前看到了这个原型,就可以根据 原型中的描述对实际参数的个数和类型进行检查,以确保数量和类型上的一致,详细解释参 见5.2.2节。 可见,函数之间可以通过参数传递和返回值实现相互通信,调用函数将实际参数传递给 被调用函数的形式参数,被调用函数通过return语句将函数的返回值传给调用函数。 函数之间也可以不通信或者单方向通信,下面给出一个函数之间单方向通信的例子,这 个例子也说明了伪随机数的使用。 5.1.2 蒙特卡洛模拟: 猜数程序 模拟算法是最基本的算法。例如,编程实现抛硬币、掷骰子和玩牌等现实世界中的随机 事件要用模拟算法。在程序设计中,可使用随机数函数来模拟现实中不可预测情况,这称为 蒙特卡洛模拟。随机数以其不确定性和偶然性等特点在很多地方都有具体的用处,比如,在 软件测试中产生具有普遍意义的测试数据,在加密系统中产生密钥,在网络中生成验证 码等。在 C语言中,用rand函数生成随机数,该函数称为随机数发生器,该发生器从称为种子 (一个无符号整型数)的初始值开始用确定的算法产生随机数。显然,通过种子产生第一个 随机数后,后续的随机序列也就是确定的了,这种依靠计算机内部算法产生的随机数称为伪 随机数。由此可见,随机数的产生依赖于种子,为了使程序在反复运行时能产生不同的随机 数,必须改变这个种子的值,这称为初始化随机数发生器,由函数srand来实现。 1 23 【例5.2】 编写一个猜数的游戏程序。在这个程序中,计算机想一个数(即随机产生一 个1~1000的整数),玩家来猜。玩家输入所猜的数,如果猜得不正确,继续猜,直到正确为 止。为了帮助玩家一步一步得到正确答案,计算机会不断地发出信息“Toohigh”或“Too low”。 分析:程序应该允许玩家反复玩,通过询问的方式,由玩家自己选择是否继续,主程序 结构如下。 do { 计算机想一个数 玩家猜数直至猜对 printf("Play again?(Y/N) "); /*询问是否继续玩*/ scanf("%1s", &choice); /*玩家输入选择*/ } while (choice == 'y' ||choice == 'Y'); /*输入y/Y,则继续*/ 将“计算机想一个数”这个功能设计成函数getNum,其函数原型如下。 int getNum(void); 括号中的void表示该函数没有输入参数,即不接收任何参数,因为它不需要来自调用 函数的任何信息,函数自己随机产生一个数。但是,函数需要返回所产生的数,函数名前的 int说明返回值的类型为整型。因此,调用函数和getNum 函数之间的通信是单向的,仅由 getNum 函数通过返回值向调用函数传递信息。该函数定义如下。 int getNum(void) /*随机产生一个1~MAX_NUMBER 的整数*/ { int x; x=rand(); /*调用标准库函数rand 产生一个随机数*/ x= x %MAX_NUMBER + 1; /*将该数限制在1~MAX_NUMBER 内*/ return x; /*返回这个随机数给调用者*/ } 函数体内的rand是stdlib.h中的一个函数,它返回一个非负并且不大于常量RAND_ MAX的随机整数,RAND_MAX是在stdlib.h中定义的一个符号常量,其值取决于系统,有 的为32767,有的为2147483647。MAX_NUMBER 是编程者用#define定义的符号常量, 其值为1000。当执行return语句时,其后表达式x的值被传递给调用函数。 另外,将“玩家猜数直至猜对”这个功能设计成函数guessNum,其函数原型如下。 void guessNum(int x); 括号里的intx说明该函数有一个int类型的输入参数,因为调用函数需要传给它被猜 数。玩家在猜的过程中函数只需要将猜的结果直接在屏幕显示,不需要向调用函数返回特 定的数值,函数名前的void说明该函数无返回值,因此,函数之间的通信也是单向的,仅由 调用函数通过参数向guessNum 函数传递信息。该函数定义如下。 void guessNum(int x) /*玩家根据提示反复猜数,直至猜对为止*/ { int guess; 1 24 for (;;) { printf("guess it: "); scanf("%d", &guess); /*玩家输入猜的数*/ if (guess == x) { /*猜对*/ printf("Right !\n"); return; /*返回,但不带回值*/ } else if (guess < x) /*猜小了*/ printf("Too low. Try again.\n"); else /*猜大了*/ printf("Too high. Try again.\n"); } } 函数体内的for(;;)是无限循环,一直循环到执行return语句时结束,关键字return 表明函数的执行到此结束,将控制返回到调用处,但不向调用函数传递值。 这样设计后,主函数main的编写就比较简单了,main调用相应的函数,解决总的任务。 具体程序代码如下。 #include #include /*标准函数库的头文件*/ #include /*日期和时间函数库的头文件*/ #define MAX_NUMBER 1000 /*被猜数的最大值*/ int getNum (void); /*getNum 函数原型*/ void guessNum(int) ; /*guessNum 函数原型*/ int main(void) { char choice; int magic; printf("This is a guessing game\n\n"); srand(time(NULL)); /*用系统时间初始化随机数生成器*/ do { printf("A magic number between 1 and %d has been chosen.\n",MAX_NUMBER); magic = getNum( ); /*调用getNum 函数产生随机数供玩家猜*/ guessNum(magic); /*调用guessNum 函数让玩家猜出这个数*/ printf("Play again?(Y/N) "); /*询问是否继续玩*/ scanf("%1s", &choice); /*玩家输入选择*/ } while (choice == 'y' || choice== 'Y'); /*数入y 则继续*/ return 0; } 用蒙特卡洛法模拟该猜数游戏,在开始玩游戏前(do语句前),需要调用函数srand初始 化随机数种子,可以采用系统时间(由time函数得到)作为种子,调用语句如下。 srand(time(NULL)); 函数time返回自1970年1月1日以来经历的秒数,将该秒数赋值给系统设置的种子变 量,因而每次运行程序产生的随机数是不同的。函数s 中,函数time的原型在头文件ti 头文件。 ntme( 如果将语句srad(i 以通过输出magic的值来验证), 5.1.3  C 程序的一般结构 C程序由一个或多个函数组成, 始。除mairand等,用户程序只需包含相应的头文件即可直接调用; 数,如例5.sacsu例5. 1的iNris、 组成一个C程序的各个函数可以放在一个源文件中, 个C源文件中可以有0个( 量)或多个函数。各C源文件中要用到的一些外部变量说明、 函数原型和编译预处理指令等可编辑成一个. 每个源文件可单独编译生成目标文件, 显示了C语言程序的基本结构。 5.2 自定义函数 程序中若要使用自定义函数实现所需的功能, 定任务的函数, ③在需要使用函数时调用函数。 5.2.1 函数定义 1.函数定义的形式 125rand和rand的原型在头文件stdlib.h me.h中,需要在源程序的头部用#include指令包含这两个 NULL));去掉,则每次运行程序产生的随机数都是一样的(可 这显然达不到随机玩游戏的效果。 其中有且只有一个main函数,程序的执行总是从main开 n以外的其他函数分两类:一类是由系统提供的标准函数,又称库函数,如printf、 另一类是需要由程序员自己编写的函 2的getNum和guessNum,这些函数称为“自定义函数”。 也可以编辑成多个C源文件,每一 源文件中可以没有函数,仅有一些说明组成,如定义一些全局变 枚举类型声明、结构类型声明、 h头文件,然后在每个源文件中包含该头文件。 组成一个C程序的所有源文件都被编译之后,由连接 程序将各目标文件中的目标函数和系统标准函数库的函数装配成一个可执行C程序。图5-1 图5-1 C语言程序的基本结构 要做三件事:①按语法规则编写完成指 即函数定义;②有些情况下在调用函数之前要进行函数声明,即函数原型; 把描述函数做什么的C代码称为函数定义。函数定义的一般形式为: 1 26 类型名 函数名(参数列表) { 声明部分 语句部分 } 说明:第一个花括号(左花括号)前的那部分称为函数头部,由类型名、函数名和参数列 表组成;在两个花括号之间的部分称为函数体,包括声明部分和语句部分。 良好的编程风格是:在函数的顶端用“/*……*/”格式增加函数头部注释,如例5.1所 示。函数头部注释包括函数名称、函数功能、函数参数、函数返回值等,如有必要还可增加作 者、创建日期、修改记录(备注)等相关内容。虽然函数头部注释在语法上不是必需的,但可 以提高程序的质量和可维护性,在程序设计时要遵从这一编程规范。为了节省篇幅,例5.2 采用简化的注释,希望读者写程序时按例5.1的格式写头部注释。 为函数命名时,要选择有意义的名称,以增加程序的可读性。Windows风格函数名用 “大小写”混排的方式,每个词的第一个字母大写,通常用“动词”或者“动词+名词”(动宾词 组)形式,如DrawBox。而UNIX通常采用“小写加下画线”的方式,如draw_box。 2.参数列表 参数列表说明函数入口参数的名称、类型和个数,它是一个用逗号分隔的变量声明表, 其中的变量名称为形式参数(简称形参)。从函数有无参数的角度,函数分为有参函数和无 参函数两类。 (1)有参函数。参数列表里有参数,每一个形参都必须声明其类型,不能像普通的变量 声明那样来声明同类型的参数。例如: int max(int a,b) /*错误的函数头,b 未指定类型*/ int max(int a,int b) /*正确的函数头*/ (2)无参函数。参数列表是空的或说明为void,表示函数没有参数。例5.2中无参函数 GetNum 的函数头可以是下面任意一个。 int GetNum(void) int GetNum( ) 3.函数的返回值 函数名前的类型名说明函数返回值的数据类型,简称函数类型或函数值的类型,可以是 除数组以外的任何类型,C99标准要求函数必须明确指定返回值的类型。从有无返回值的 角度,函数分有返回值函数和无返回值函数两类。 (1)有返回值函数。这类函数执行完后将送出一个数值给调用者,这个值称为函数返 回值,该值是通过return语句送出的。例如,例5.2的GetNum 函数返回int类型数据,函数 体最后的语句: return x; 将变量x的值返回到调用处。下面这条语句的作用相当于把x的值赋给magic。 magic = GetNum( ); 1 27 如果没有这条return语句,那么x值送不出去,变量magic就得不到函数GetNum 产生 的随机整数。所以,有返回值的函数必须使用带表达式的return语句,return中表达式的值 就是函数的返回值。一般来说,表达式值的类型应该与函数定义时指明的类型一致。例如, GetNum 函数的类型是int,所以x的类型也声明为int。如果两者不一致,对于基本类型,将 把表达式值的类型自动转换为函数的类型;对于指针类型,必须使用强制类型符将表达式值 的类型显示转换为函数的类型。 对于函数返回的值,程序可以使用它,也可以不使用它。 getchar(); /*返回值不被使用*/ c=getchar(); /*返回值被使用*/ (2)无返回值函数。当返回值类型为void 时,函数将不返回任何值。例5.2 的 GuessNum 函数就是无返回值函数,它仅输出结果到屏幕,没有给调用者返回数值。无返回 值函数可以使用如下不带表达式的return语句。 return; 说明:该return语句的作用仅将控制返回到调用处,但不带回值。 无返回值函数也可以不包含return语句,如果没有return语句,当执行到函数结束的 右花括号时,控制返回到调用处,这种情况称为离开结束。但是,在例5.2中,GuessNum 函 数for循环体内的return语句必须有,否则函数将一直循环执行,无法离开结束。 在一个函数中可以有多个return语句,这种情况下的return语句通常被作为选择语句 的子句出现,最终被执行的只是其中的一个。 【例5.3】 写一个函数isPrime,判断整数n是否为素数。如果n是素数,则返回1,否则 返回0。 分析:如果n是2,则n是素数,返回1;如果n是偶数,则n不是素数,返回0;如果n是 奇数,则循环找因子,一旦找到一个因子,n就不是素数,返回0,如果函数执行到正常退出循 环,说明n是素数,返回1。一旦某个return语句被执行,控制立即返回到调用处,其后的代 码不可能被执行。 int isPrime(int n) { int k,limit; if (n==2) return 1; /*2 是素数,返回1*/ if (!(n %2)) return 0; /*偶数不是素数,返回0*/ limit=n/2 ; for (k=3;k<=limit;k+=2) /*对于奇数*/ if (!(n %k)) return 0; /*有因子不是素数,返回0*/ return 1; /*是素数,返回1*/ } return后面只能跟一个表达式。也就是说,通过return语句,函数只能返回一个值到调 用处。怎样使函数送回多个值? 通过外部变量(见5.3.3节)和指针参数(见8.2.2节)可使函 1 28 数间接送回多个值。 *4.内联函数 内联函数是指用关键字inline修饰的函数,也称内嵌函数,它主要的作用是提高程序的 运行效率。inline是C99增加的关键字,它用在函数定义的前面,告诉编译器对该函数的调 用进行内联优化,即采用插入的方式处理函数调用,而不是在调用时发生控制转移。例如: inline int add(int a, int b) { return a+b; } add是内联函数,编译时使用函数体替换调用处的函数名,类似宏替换(见6.1节),这种 方式不会产生调用和返回所带来的时间开销,但会增加目标程序代码量,进而增加空间开 销。内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控 制来实现的,不会产生处理宏的一些问题。 inline仅仅是对编译器的建议,能否真正内联,还要看编译器的意思,它如果认为函数不 复杂,能在调用点展开,就会真正内联。因此,inline的使用是有限制的,内联函数应该简洁, 只有几个语句,不能有循环或switch等复杂的结构,必须在调用之前定义,否则编译器将该 函数视为普通函数。 5.2.2 函数原型 和变量一样,函数也应该先声明后使用,有两种声明函数的方法:一是给出完整的函数 定义;二是提供函数原型。为确保程序正确性,在函数调用之前必须给出它的函数定义或函 数原型,或者两者都给出。 1.函数定义起函数声明的作用 C程序书写格式很自由,函数定义的次序可以随意,如果函数定义出现在函数调用之 前,那么函数定义起函数声明作用。例如,例5.2中的三个函数定义的次序可为: #include void guessNum(int x); /*函数原型*/ int getNum( ) /*getNum 函数定义*/ { ... } int main() { … magic = getNum( ); /*getNum 函数调用*/ guessNum(magic); /*guessNum 函数调用*/ … }v oid guessNum(int x) /*guessNum 函数定义*/ { … } main中调用了函数getNum 和guessNum,getNum 的定义在main函数之前,函数定义 起到了函数声明的作用,因此前面可以不写getNum 函数的原型声明(写上也无妨)。而函 1 29 数guessNum 的定义在main函数之后,函数定义不能起到函数声明的作用,因此前面必须 写guessNum 函数的原型声明。有了函数原型信息,编译器就可以检查函数调用是否和其 原型声明相一致,比如检查参数个数是否正确、参数类型是否匹配等。如果参数个数不对, 如函数调用写成guessNum(a,b),则编译器会给出一个错误信息,告知调用函数guessNum 时传递的参数太多。如果参数类型不匹配,如函数调用为guessNum(6.8),则编译器会根据 形式参数的类型来转换实际参数值,将6.8转换为6再传递给形参x。 2.函数原型 如果函数定义出现在函数调用之后或者被调用函数在其他文件中定义(对于多文件的 C程序),则必须在函数调用之前给出函数原型。函数原型告诉编译器函数返回值类型、参 数个数和各参数类型。编译器用函数原型来检查函数调用的正确性,从而避免错误的函数 调用所导致的致命运行错误或者微妙而难以检测的非致命逻辑错误。函数原型的一般形 式为: 类型名 函数名(参数类型表); 说明:函数原型是声明语句,必须以分号结束,参数类型表通常是用逗号隔开的形参类 型列表,而形参名可以省略,除此之外,函数原型和函数定义的头部是相同的。例如,函数 max的原型声明可以是下面中的任意一个。 int max(int,int); int max(int x,int y); int max(int a,int b); 该函数原型表明:函数max返回值类型为int,有两个类型为int的参数,类型后的变量 名是虚设的名字,不必与函数定义中的形参名一致,编译器忽略函数原型中像x和y这样的 名字,使用形参名的目的是为了方便阅读文档。 对于无参函数,函数原型的参数类型表需要指定为void。例5.2中getNum 函数原型 应为: int getNum(void); 函数原型一般位于文件开头部分(即所有函数定义的前面),这样该函数可被本文件中 的任意函数调用。函数原型也可以位于调用函数体内,如例5.2中的main函数调用了 getNum 和guessNum 函数,其函数原型可出现在main函数的声明语句部分。 int main( ) { int getNum(void); /*函数原型位于调用函数体中*/ void guessNum(int); 该函数原型表明main函数可以调用它。如果有其他函数要调用它,则在调用它的函数 体内也要写函数原型代码。 为充分利用C语言的类型检查能力,在程序中应包含所有函数的函数原型。系统库函 数的函数原型在相应的头文件中,所以,程序中用到的库函数要使用#include预处理指令 包含相应的头文件。