函数与模块化编程 一、函数的概念: 1、我们为什么要使用函数: 在之前的学习过程中,我们已经可以编写一些中小型的程序了,不过对于一些大型程序而言还是力不从心。因为程序的功能比较多,规模比较大,如果把所有的代码都写在main()函数内,不仅会使main()函数变得十分复杂,而且代码的调试、维护、重构等工作会变得十分困难。此外,若需要反复实现某项功能,则需要多次重复编写一段代码,这样会使得程序冗长,不够精炼。 因此我们引入“函数”的概念。使用函数后,我们可以: 1)避免使主函数main()变得庞杂、逻辑混乱。 2)避免程序冗长、不精练 3)如果需要多次实现某功能,不使用函数的话则需要重复编写某段代码,工作量大。 …… 2、函数的概念: 函数(function)的概念是从数学上引入的,在计算机学里指一段在一起的用来执行某功能的代码。针对不同的功能,我们编写不同的函数,这就是模块化程序设计的思路。在设计较大程序的时候往往把它分割成若干个小模块,每个模块包含一个或多个函数,每个函数实现一个特定功能。 注:在JAVA中,函数被称之为“方法”,本质类似。 除了自己实现的函数,C语言还提供各种已经实现好的库函数来方便程序开发者使用,如之前用过的printf()、scanf()、strcpy()、strcat()、strlen()等 注意:使用系统库函数需要包括相应的头文件 在程序设计过程中要善于利用函数,不仅能够减少重复书写代码的工作量,也更利于实现模块化的程序设计。 示例:使用函数,打印以下结果: ******************** How do you do! ******************** #include int main() { void print_star(void);//函数声明 void print_message(void);//函数声明 void print(void);//函数声明 print();//函数调用 return 0; } void print()//函数名 {//函数体 print_star();//函数调用 print_message();//函数调用 print_star();//函数调用 } void print_star()//函数名 { printf("********************\n");//函数体 } void print_message()//函数名 { printf("how do you do!\n");//函数体 } 调用关系: main()----->print()----->print_star()和print_message() 3、模块化编程 引入了函数的概念之后,人们自然就提出了“模块化编程”的编程思想。简单来说就是,通过编写各种函数事先实现各种不同的功能,例如sin()表示求正弦函数,abs()表示求一个数的绝对值……我们把事先编写好的各种函数放置在函数库中,需要的时候调出相应功能的函数,这样就可以实现功能。 这种编程的思想就是“模块化编程”。我们在程序开发过程中,若程序规模较大,则我们可以把程序分隔成若干模块,每个模块包含一个或多个功能函数,每个函数实现一个特定的功能。一个C语言程序由一个主函数main()和若干个子函数构成。主函数调用子函数,函数和函数间也可互相调用,甚至函数自己可以调用自己。 使用模块化编程不仅让程序脉络清晰、功能划分详细,而且还大大减少了重复代码的书写量,而且也更加方便程序的设计与编写。 二、函数的使用: 与变量相同,函数必须要先定义再使用 1、函数定义: 定义一个函数需要包括以下几个内容: 1)指定函数的名称以便后续调用 2)指定函数类型,即函数返回值的类型 3)指定函数的参数的名字和类型,以便在调用函数时进行数据传递 4)指定函数应当完成什么操作,即函数的功能实现。 5)(可选)对定义的函数添加适当的注释进行说明 函数的一般形式: 类型名 函数名(参数列表) { 函数体 …… } 例如上文的print_star()函数就是一个函数定义,只不过没有参数 例如以下定义的max函数是用来求出2个数中更大的数 int max(int x,int y) { int z; z=x>y?x:y; return z;//返回值 } 这个函数就是有参数的函数。 2、函数定义的几个关键要素详解: 1)函数名:函数的标识。函数名是其他函数用来调用这个函数的标识,函数名应符合C语言的标识符命名规范,且不得重名,不得与系统函数(如printf()等库函数或main()函数)重名。命名应做到“见文知意”而避免起简单的函数名(如aa()等不好的命名) 函数名命名通常有3种命名规则 ⒈大驼峰法:函数名的所有单词的首字母大写。如:SetStudentName()。这种是Windows的命名规则。 ⒉下划线法:函数名的所有单词的首字母都不大写,单词与单词之间使用下划线_来分隔开。如:set_student_name()。这种是Linux的命名规则。 ⒊小驼峰法:函数名的所有单词,除首个单词首字母不大写外,其他单词首字母都大写。如setStudentName()。这种是苹果IOS的命名规则。 初学者在命名函数的时候应遵循其中一个命名规则,而不要自主随意命名。 2)类型名:函数的类型,即函数的返回值。函数的返回值可理解为函数的计算结果。函数可以没有返回值,但如果有返回值,返回值类型需要与函数类型匹配。例如上文的max()函数,函数类型是int类型,表示这个函数的计算结果是int类型。因此函数的返回值(return z;这句)也是int类型。若该函数不需要返回值(如上文print_star()函数),则应将函数类型定义为void(空类型) 3)参数列表:指函数需要的用于计算的数据。参数可以有0个或多个。参数有形参和实参两种,有关形参和实参的讲解见下面章节。 4)函数返回值:返回值必须与函数函数类型匹配。对于void型函数,可以没有返回值或直接写return; 5)函数体:函数的执行代码逻辑。 注意:若某函数定义在调用它的函数后,需要在调用函数中添加这个函数的声明。 示例:先写一个max()函数求出2个数中的最大值,再在main()函数中调用它 #include int max(int x,int y) { int z; z=x>y?x:y; return z; } int main() { int a,b,c; printf("请输入2个待比较的数:"); scanf("%d%d",&a,&b); c=max(a,b);//调用max函数,传递两个参数a,b进行运算,返回值给c printf("最大数是%d\n",c); return 0;//main()函数也需要返回值,因为main()函数的类型是int,所以返回一个整数。返回0表示程序正常结束 } 练习1:编写一个函数,求一个数x的n次方。其中x和n作为函数的参数传递进去。之后写main()函数测试 答案: #include int n_ci_fang(int x,int n) { int i,sum=1; for(i=0;i int max(int a,int b,int c) //始终让a保存3个数中的最大值,然后返回a { if(a float avgrage(float array[]) { int i; float aver,sum=0; for(i=0;i<10;i++) { sum+=array[i]; } aver=sum/10; return aver; } int main() { float score[10],avg; int i; for(i=0;i<10;i++) { scanf("%f",&score[i]); } avg=avgrage(score); printf("average score is %.2f\n",avg); return 0; } 注意: ⒈实参与形参的类型要一致。 ⒉在被调函数内,试图通过任何方式求出或声明形参内数组的大小都是不起任何作用的,因为C语言编译器对数组类型的形参当做指针去处理,不会检查形参的数组是否有越界。在调用时,主调函数的实参将首地址传递给被调函数的形参数组名。例如,如果我们将示例程序写成: #include float avgrage(float array[]) { int i; int n = sizeof(array)/sizeof(array[0]);//试图在被调函数内求出数组元素个数 float aver,sum=0; for(i=0;i float avgrage(float array[],int n) { int i; float aver,sum=0; for(i=0;i void bubble_sort(int array[],int n) { int i,j; for(i=0;iarray[j+1]) { array[j]^=array[j+1]; array[j+1]^=array[j]; array[j]^=array[j+1]; } } } } int main() { int a[10],i; for(i=0;i<10;i++) { scanf("%d",&a[i]); } bubble_sort(a,sizeof(a)/sizeof(a[0])); for(i=0;i<10;i++) { printf("%d ",a[i]); } printf("\n"); return 0; } 3)局部变量与全局变量 在C语言程序中,定义一个变量有以下3种情况: ⒈在函数的开头定义 ⒉在函数的复合语句中定义(如for()循环中临时定义的int i等) ⒊在函数的外部定义 在函数内部定义的变量只在本函数范围内有效;在函数的复合语句中定义则只在这个复合语句中有效。以上的两种情况变量都只在一定范围内有效,因此这样的变量叫“局部变量”。 例如,在函数fun1()中定义两个变量a,b,在函数fun2()中定义两个变量a,c。则fun1()之中的a和fun2()之中的a不是一个变量(即使同名),因为这两个a变量只在自己的函数作用范围内有效。 注意: ⒈在main()函数之中定义的变量也只在main()函数之中有效,而并非对整个程序有效。 ⒉不同函数间的局部变量可以同名,但即使同名也不是同一个变量,这点要特别注意。 ⒊参数列表中的形参也是局部变量。 如果有一个变量可以被本文件中所有的函数共用,则这个变量叫“全局变量”(或外部变量)。全局变量的作用域是从定义变量的位置开始直到本文件结束。 定义全局变量的方法:在所有函数的外部定义变量即可。 示例: int p=1,q=5; float f1(int a) { int b,c; } char c1,c2; char f2(int x,int y) { int i,j; } int main() { int m,n; return 0; } 分析: 函数f1()可用的变量:p,q,a,b,c 函数f2()可用的变量:p,q,c1,c2,x,y,i,j 函数main()可用的变量:p,q,c1,c2,m,n 使用全局变量的作用是增加了函数间数据联系的渠道。若有好几个函数间需要数据传递,则可建立一个全局变量,这样几个函数都可以访问这个全局变量。 练习:在一维数组中存储10个学生成绩,编写2个函数,一个计算总分和平均值并输出,一个输出所有学生的成绩并求最大最小值 答案: #include int score[10]={89,95,87,100,67,97,58,84,73,90}; int sum=0,max=-1,min=101; float avg=0; void average(int array[],int n)//注意数组作为形参的写法 { int i; for(i=0;imax) max=array[i]; if(array[i] int a=3,b=5; int max(int a,int b) { int c; c=a>b?a:b; return c; } int main() { int result1 = max(a,b); printf("结果1是%d\n",result1); int a=8,result2; result2=max(a,b); printf("结果2是%d\n",result2); return 0; } 答案:输出“结果1是5,结果2是8” 在这里我们故意设置了局部变量与全局变量同名。我们会发现,当局部变量与全局变量同名时,在局部变量的作用范围内,全局变量会被“屏蔽”,即只有局部变量在起作用。 4)值传递与地址传递 一般情况下,被调函数靠返回值将函数的计算结果返回给主调函数。但是return语句只能执行1次。若有其他语句写在return语句之后,则在执行return语句之后其他的语句都不会被执行了。 若我们的函数可能得到不止一个结果,则在这种情况下无法通过设置多个return的方式同时获得好几个返回值。这时候我们必须采用别的方式将被调函数的数据传回主调函数。 请看以下示例。在示例中我们编写一个swap函数,希望将2个数交换。 示例1: void swap(int x,int y) { int temp; temp = x; x = y; y = temp; } int main() { int a=5,b=3; swap(a,b); printf("a是%d\nb是%d\n",a,b); return 0; } 输出结果:a是5,b是3 我们发现调用swap函数并没有使变量a和b的值发生改变。是我们的swap函数逻辑错误吗?我们修改示例1,在swap函数的最后添加一行测试输出: 示例2: void swap(int x,int y) { int temp; temp = x; x = y; y = temp; printf("x是%d\ny是%d\n",x,y);//添加测试输出 } int main() { int a=5,b=3; swap(a,b); printf("a是%d\nb是%d\n",a,b); return 0; } 输出结果: x是3,y是5 a是5,b是3 可以看到,我们的swap函数的逻辑并没有问题,在swap函数内变量的值确实发生了交换。 思考:为什么会出现这样的情况? 答案:因为在swap函数中,变量x和y是swap函数的局部变量。在进行计算的时候,main函数的变量a和b分别将自己的值传递给变量x和y,x和y在代替a和b进行计算。计算完毕后(即函数执行完毕后)函数被系统回收,相应地变量x和y也被系统回收,而没有把真正的把计算结果返回给main函数中的a和b。 这种将需运算的变量的值传递给函数形参的方式称之为“值传递”。 如果我们需要正确地完成swap函数的功能,我们就不能使用值传递,而是需要使用另一种参数传递方法:地址传递。 地址传递:将需运算的变量的地址传递给函数形参的方式称之为“地址传递”。 使用地址传递完成swap函数如下: 示例3: //使用地址传递来完成swap函数的交换功能 void swap(int *x,int *y) { int temp; temp = *x; *x = *y; *y = temp; } int main() { int a=5,b=3; swap(&a,&b);//注意这里需要取地址运算符& printf("a是%d\nb是%d\n",a,b); return 0; } /************ 运算符*为指针运算符,表示访问该指针指向的内容 运算符&为取地址运算符,表示计算该变量所在的内存地址 *************/ 输出结果:a是3,b是5 可以发现,使用地址传递成功地实现了swap函数的功能。 关于地址传递需要注意以下几点: ⒈地址也是一种特殊的值,因此地址传递可看成特殊情况下的值传递。只不过地址传递的方式传递的是地址(而不是简单的变量的值)因此将值传递与地址传递两种方式人为区分。 ⒉通常情况下,我们可以这样区分值传递与地址传递:若主调函数传参是普通变量的值,被调函数的参数列表也是普通变量值,则可认为使用的是值传递;若主调函数传参是变量的地址,而被调函数的参数列表是地址,则可认为使用的是地址传递。 ⒊使用地址传递时,被调函数内对形参的操作采取的是间接寻址的方法,即通过这个地址找到变量的实际值,进而进行操作。(通过堆栈区存放的地址访问主调函数的实参变量) 主调函数传递给被调函数数据,如果想要保护数据(即不希望被调函数修改主调函数的值),则我们应使用值传递;如果主调函数希望被调函数修改自己的变量值,则应使用地址传递。 地址传递的作用是:若一个子函数需要求出多个返回值的时候,这时候无法使用多个return语句,这时我们可以使用地址传递的方式将多个返回值传递回主调函数。 练习:数组中存放10个学生的成绩。编写一个函数,求出10个学生的成绩的总和和平均值,返回成绩低于平均值的学生数量。 提示:总和和平均值使用地址传递,低于学生平均值的学生数量使用函数返回值。 答案: #include int fun(int a[],int n,int *sum,float *avg) { int i,count=0; *sum = 0; *avg = 0; for(i=0;i int age(int n) { int c; if(n==1)//递归出口 c = 10; else c = age(n-1)+2;//递归逻辑 return c; } int main() { printf("第5个学生的年龄是%d\n",age(5)); return 0; } 注意:我们在使用递归的时候一定要设置递归的终止条件(称之为“递归出口”),如果没有递归出口,则递归会无限次循环下去,不仅可以使程序崩溃,严重的更有可能造成系统崩溃。因此初学者在使用递归的时候一定要谨慎。如果程序数据计算量不大而运行时间又过长,则我们可认为出现了死递归,请使用ctrl+c来终止程序。 练习1:用递归法求n!,n由键盘输入。 答案: int jiecheng(int n) { int f; if(n<0) { printf("输入数据错误!\n"); return 0; } else if(n==0||n==1) f = 1; else f = n*jiecheng(n-1); return f; } int main() { int n; printf("请输入n的值:"); scanf("%d",&n); if(jiecheng(n)>0) { printf("%d的阶乘是%d\n",n,jiecheng(n)); } else { printf("输入错误!\n"); } return 0; } 练习2:用递归方法求n阶勒让德多项式的值。递归公式为: Pn(x)=1;(n=0) Pn(x)=x;(n=1) Pn(x)=((2n-1) * x - Pn-1(x) - (n-1) * Pn-2(x))/n;(n>1) 答案: #include float Legendre(float x,int n) { if(n==0) return 1; else if(n==1) return x; else return ((2*n-1)*x - Legendre(x,n-1) - (n-1)*Legendre(x,n-2))/n; } int main() { float x; int n; printf("请输入x的值:"); scanf("%f",&x); printf("请输入n的值:"); scanf("%d",&n); printf("勒让德多项式的值是%f\n",Legendre(x,n)); return 0; } 练习3:Hanoi(汉诺塔)问题: 古代经典数学问题。有一个梵塔,塔内有3个塔座A、B、C。开始时A塔座上有64个盘子,大小不一,大盘在下,小盘在上。有一个老和尚想将A塔座的64个盘子全部移动到C塔座上,规定: ⒈一次只能移动一个圆盘 ⒉必须始终保持大盘在下,小盘在上 ⒊圆盘只能在3个塔座间移动(不允许放在地面上等其他地方) 编程输出n个圆盘的Hanoi塔问题的移动步骤。 注意:n个圆盘的Hanoi塔需要(2^n)-1步移动步骤,因此我们输入的圆盘数n不应过大。 提示:先动手模拟数量较少的圆盘(如3个圆盘)的移动步骤,再思考其中的算法 分析:Hanoi塔问题使用的递归解法: ⒈将A塔座上的n-1个圆盘借助C塔座先移动到B塔座上 ⒉将A塔座上的第n个圆盘移动到C塔座上 ⒊将B塔座上的n-1个圆盘借助A塔座移动到C塔座上 而第一步跟第三步都是将n-1个圆盘从"one"塔座借助"two"塔座移动到"three"塔座上,不同的是第一步"one"对应A,"two"对应C,"three"对应B;而第三步"one"对应B,"two"对应A,"three"对应C。因此可以将以上三步分成2种操作: ⒈将n-1个盘子从一个塔座移动到另一个塔座(n>1),这是一个递归的过程 ⒉将第n个盘子从一个塔座移动到另一个塔座。 因此我们应编写2个函数,一个函数hanoi(int n,char one,char two,char three)表示将n个盘子从one借助two移动到three上。另一个函数move(char x,char y)表示将x移动到y,x和y分别代表A、B、C其中之一,根据不同情况每次选取不同的塔座带入move()函数中。 答案: #include void move(char x,char y) { printf("%c--->%c\n",x,y); } void hanoi(int n,char one,char two,char three) { if(n==1)//递归出口 { move(one,three); } else//递归过程 { hanoi(n-1,one,three,two); move(one,three); hanoi(n-1,two,one,three); } } int main() { int m; printf("请输入圆盘的个数:"); scanf("%d",&m); hanoi(m,'A','B','C'); return 0; } 练习4:使用递归解决“棋子移动”问题 有2n(n>=4)个棋子排成一行,其中黑棋B有n个,白棋W有n个,并留有两个空格。例如,当n=4时排列如下所示:(W为白棋,B为黑棋,0为空格) W W W W B B B B 0 0 当n=5时排列如下所示:(W为白棋,B为黑棋,0为空格) W W W W W B B B B B 0 0 现在需要移动棋子,移动规则如下: ⒈每次必须同时移动相邻的两个棋子 ⒉每次移动必须跳过若干棋子 ⒊不能随意调换任意两个棋子的位置 目标:将所有的棋子移动为黑白棋相间的形式,中间不能有空格。 例如:当n=4时移动步骤如下: 起始: W W W W B B B B 0 0 第一步:W W W 0 0 B B B W B 第二步:W W W B W B B 0 0 B 第三步:W 0 0 B W B B W W B 第四步:W B W B W B 0 0 W B 第五步:0 0 W B W B W B W B(完成) 编程实现:从键盘输入n(n>=4),求每一步的棋子移动 答案: //递归出口:当n=4的时候 //递归逻辑:当n>4时,move(n,n+1)→(2n+1,2n+2); move(2n-1,2n)→(n,n+1); 递归n-1 #include #include #include #define SPACE 0 #define WHITE -1 #define BLACK 1 void print_array(int a[],int n)//打印棋子 { int i; for(i=0;i void f() { int a=1; static int b=1; a++; b++; printf("a是%d\nb是%d\n",a,b); } int main() { f(); f(); f(); f(); f();//调用5次f()函数 return 0; } 在示例程序中,尽管a和b都是局部变量,但变量b是静态局部变量,它会保存上一次函数运行后的值。因此每次运行f()函数,变量a的值都会初始化一次,而变量b则会使用上一次f()结束后的值。 练习:使用static变量,编程分别输出从1到6的阶乘值。 答案: #include int jiecheng(int n) { static int f=1; f=f*n; return f; } int main() { int i; for(i=1;i<=6;i++) { printf("%d!=%d\n",i,jiecheng(i)); } return 0; } 2)将外部变量的作用域限制在本文件中(静态外部变量) //请对比extern关键字的第二条 有时程序设计需要,某些外部变量只能在本文件中使用而不允许在其他文件中使用。这时可以在定义外部变量时加static声明。 示例: //文件file1.c static int A; …… //文件file2.c void fun() { A++;//出错 } 这种加上了static声明,只能在本文件中使用的外部变量称为“静态外部变量”。静态外部变量的作用是可以将某些变量“屏蔽”起来,从其他文件角度看这个变量“不可见、不可使用”,这样就保护了这个外部变量,防止程序运行出错。 3)定义一个内部函数 //请对比extern关键字的第三条 如果一个函数只能被本文件的其他函数调用而不允许被其他文件的函数调用,这样的函数称为“内部函数”。定义内部函数时,在函数名和类型名前加static关键字。 static int fun(int a,int b) 则该函数fun(int a,int b)只能被这个文件中的其他函数所调用,不能被其他文件的函数所调用。 7、extern关键字 1)在一个文件内扩展外部变量的作用域 如果一个外部变量不在文件开头定义,则其有效范围是从这个变量定义处到文件结束。在定义开始前的函数不能使用这个外部变量。如果需要使用,则可在引用这个变量前加extern关键字进行“外部变量声明”。例如: 示例: int f() { extern int a;//外部变量声明 …… } int a=10;//外部变量定义 这样就相当于扩展了外部变量a的使用范围,在函数f()中也可以使用变量a了。 关键字extern:对外部变量进行“外部变量声明”,表示把该外部变量的作用域扩展至此位置。 注意: ⒈extern仅仅起到变量的声明的作用,而不是变量的定义。在示例中,如果没有int a=10;(即没有定义一个外部变量),则不能够使用extern int a;变量。extern声明外部变量可以有很多,但外部变量定义只允许有一个(否则算重复定义变量,编译报错)。 ⒉我们提倡将外部变量写在所有需要引用它的函数之前(或直接写在文件开头,预处理指令(文件包含(#include)和宏定义(#define)和条件编译(#ifdef/#ifndef等))下面)。 ⒊变量的声明不需要建立存储空间,而变量定义则需要建立存储空间。 2)将外部变量的作用域扩展到其他文件中 //请对比static关键字的第二条 一个C程序可以由多个文件组成。如果程序由多个文件组成,在其中一个文件中需要引用另一个文件中已知的外部变量,可以使用extern关键字将这个外部变量的使用范围扩展到需要使用的文件中。 示例: //文件file1.c int A; …… //文件file2.c extern A; …… 这样就可以在文件file2.c中使用文件file1.c的外部变量A 3)定义一个外部函数 //请对比static关键字的第三条 如果一个函数不仅可以被本文件的其他函数调用,而且可以被其他文件的函数调用,这样的函数称为“外部函数”。定义外部函数时,在函数名和类型名前加extern关键字。 extern int fun(int a,int b) 则该函数fun(int a,int b)可以被其他文件调用 综合练习1:买彩票 编写一个“买彩票”的程序。彩票程序在后台随机生成1~35内的7个各不相同的数字。用户会输入一组7个数字,中奖规则: 猜中 7个500万 6个100万 5个1万 4个5000 3个500 0,1,2个没中奖 输出是否中奖及奖金。 综合练习2:五子棋 编写一个“双人五子棋”的程序。棋盘为9*9的矩阵,输入横纵坐标落子,黑白双方轮流落子,直至一人获胜为止。