760 likes | 956 Views
C 语言程序设计. 第 7 章 函数与预处理命令. 北京科技大学 计算机系. 第 7 章 函数与预处理命令. 7.1 概述 7.2 函数的定义与调用 7.3 数组作函数参数 7.4 函数的嵌套调用和递归调用 7.5 局部变量和全局变量及其作用域 7.6 变量的存储类别及变量的生存期 7.7 函数的存储分类 7.8 编译预处理 7.9 多文件程序的调试方法. 7.1 概述. C 程序. … …. 源程序文件 1. 源程序文件 n. 函数 1. 函数 n. …. …. 函数 1. 函数 m. ① 程序结构清晰,可读性好。
E N D
C语言程序设计 第7章 函数与预处理命令 北京科技大学 计算机系
第7章 函数与预处理命令 7.1 概述 7.2 函数的定义与调用 7.3 数组作函数参数 7.4 函数的嵌套调用和递归调用 7.5 局部变量和全局变量及其作用域 7.6 变量的存储类别及变量的生存期 7.7 函数的存储分类 7.8 编译预处理 7.9 多文件程序的调试方法
7.1 概述 C程序 … … 源程序文件1 源程序文件n 函数1 函数n … … 函数1 函数m ① 程序结构清晰,可读性好。 ② 减少重复编码的工作量。 ③ 可多人共同编制一个大程序,缩短程序设计周期,提高程序设计和调试的效率。 使用函数的 好处
【例7.1】求一个整数的立方。 int cube (int x)/* 函数定义 */ { return (x * x * x); } main( ) { int f, a; printf("\nEnter an integer number:"); scanf("%d", &a); f = cube (a); printf("%d * %d * %d = %d\n", a, a, a, f); } 程序的执行总是 从main函数开始 函数调用 程序运行情况如下: Enter an integer number:2 2 * 2 * 2 = 8
说明 ⑴ 一个C源程序可以由一个或多个源程序文件组成。C编译系统在对C源程序进行编译时是以文件为单位进行的。 ⑵ 一个C源程序文件可以由一个或多个函数组成。所有函数都是独立的。主函数可以调用其它函数,其它函数可以相互调用。 ⑶ 在一个C程序中,有且仅有一个主函数main。C程序的执行总是从main函数开始,调用其它函数后最终回到main函数,在main函数中结束整个程序的运行。
⑷ 函数的种类 从函数定义形式分: ① 有参函数: 在主调(用)函数和被调(用)函数之间通过参数进行数据传递, 如: int cube (int x) { … } ② 无参函数: 如:getchar( ) 在调用无参函数时,主调函数不需要将数据传递给无参函数。 从使用的角度看: ① 标准函数(库函数) 库函数是由系统提供的。如:getchar( )、sin(x)等。在程序中可以直接调用它们。附录A列出了C的部分库函数。 ② 用户自定义函数。 如:例7.1中的cube函数。
【例7.2】无参函数的定义与调用。 void welcome ( ) { printf("*********************************\n"); printf(" Welcome to China \n"); printf("*********************************\n"); } main( ) { welcome( );} 程序的输出结果如下: ********************************* Welcome to China *********************************
7.2.1 函数的定义 没有形式参数为无参函数 函数定义的一般形式 • 函数类型 函数名(类型名 形式参数1,…) • { 说明语句 • 执行语句 • } 例如:求两个数的最大值。 intmax(int x,int y) { int z; z = x > y ? x : y; return( z ); } 类型省略时默认为int类型
如下定义都是错误的 形参也可以这样定义 int max(x,y) int x,y; { int z; z = x > y ? x : y; return( z ); } int max(x,y) { int x,y; …… } 或 int max(int x,y) { …… } 或 int max(x,y) int x,y,z; { z = x > y ? x : y; return( z ); } √ 花括号中也可以为空,这种函数叫空函数 。 不能在函数体内定义其他函数,即函数不能嵌套定义。
7.2.2 函数的调用 函数调用的一般形式: 函数名(实参表列) 在C语言中,把函数调用也作为一个表达式。因此凡是表达式可以出现的地方都可以出现函数调用。例如: ① welcome( ); ② if (iabs (a)>max) max=iabs(a); ③ m=max(c,max(a,b));
【例7.3】求1~100的累加和。 思考:两个程序有何不同 int sum ( int x ) { int i,t=0; for (i=1; i<=x; i++) t+=i; return (t); } main( ) { int s; s=sum (100); printf("%d\n", s); } int sum100( ) { int i,t=0; for (i=1; i<=100; i++) t+=i; return (t);} main( ) { int s; s=sum100( ); printf("%d\n", s); } ? 程序输出结果: 5050 程序输出结果: 5050
7.2.3 函数参数与函数的返回值 1.函数的形式参数与实际参数 【例7.4】编一程序,将主函数中的两个变量的值传递给swap函数中的两个形参,交换两个形参的值。 void swap(int x, int y) { int z; z=x; x=y; y=z; printf("\nx=%d,y=%d",x ,y); } main( ) { int a= 10,b=20; swap(a,b); printf("\na=%d,b=%d\n",a,b); } 形式参数(形参) 单向值传递 实际参数(实参) 程序输出结果: x=20,y=10 a=10,b=20
有关形参和实参的说明: ① 当函数被调用时才给形参分配内存单元。调用结束,所占内存被释放。 ② 实参可以是常量、变量或表达式,但要求它们有确定的值。 ③ 实参与形参类型要一致,字符型与整型可以兼容。 ④ 实参与形参的个数必须相等。在函数调用时,实参的值赋给与之相对应的形参。“单向值传递”。 注意:在TC中,实参的求值顺序是从右到左。
【例7.5】函数调用中实参的求值顺序。 void fun(int a,int b) { printf("a=%d,b=%d\n",a,b); } main( ) { int m=5; fun(3+m, m++); } 程序输出结果: a=9,b=5
2.函数的类型与函数的返回值 ⑴ 函数的类型 【例7.6】输出两个数中的大数。 max(int x,int y) { int z; z=x>y?x:y; return (z); /* 返回z的值 */ } main( ) { int a,b,c; scanf("%d,%d",&a,&b); c=max(a,b); printf("max is %d\n",c); } 说明: ①函数的类型决定了函数返回值的类型。若省略函数的类型,系统默认其为int型。 ②无返回值的函数应将其类型定义为void (空)类型。
⑵ 函数的返回值 函数的返回值是通过return语句带回到主调函数的 功能:终止函数的运行,返回主调函数,若有返回值,将返回值带回主调函数。 return 语句格式: return (表达式);或 return 表达式 ; 或 return; 说明: ① 若函数没有返回值,return语句可以省略。 ② return语句中的表达式类型一般应和函数的类型一致,如果不一致,系统自动将表达式类型转换为函数类型。
【例7.8】计算并输出圆的面积。 s(int r) { return 3.14*r*r;} main( ) { int r,area; scanf("%d",&r); printf("%d\n",s(r)); } 自动转换为int型 程序运行情况如下: 2 12 ? 思考: 若要得到单精度实型的圆面积,程序应如何修改
7.2.4 对被调函数的声明和函数原型 变量要先定义后使用,函数也如此。即被调函数的定义要出现在主调函数的定义之前。如swap函数: 允许整型函数(且参数也是整型)的定义出现在主调函数之后。如max函数: 如果非整型函数在主调函数之后定义,则应在主调函数中或主调函数之前对被调函数进行声明。 void swap(int x, int y) { …} main( ) { … swap(a,b); } main( ) { … c=max(a,b); } max(int x,int y) { …}
对被调函数进行声明的一般形式 函数类型 函数名(参数类型1参数名1,…); 或 函数类型 函数名(参数类型1,参数类型2,…); 第二种形式省略了参数名,此种形式也称为函数的原型。 思考: 以下哪种情况需要在主调函数中对被调函数声明 • 被调函数定义在前,主调函数定义在后。 • 主调函数定义在前,被调函数定义在后,且被调函数的类型不是整型的。 • 被调函数定义在后,但被调函数的类型是整型。 ?
【例7.9】计算并输出两个数的和、差、积、商。【例7.9】计算并输出两个数的和、差、积、商。 main( ) { void calc(float x,float y,char opr); float a,b; char opr; printf("\nInput expression:"); scanf("%f%c%f",&a,&opr,&b); calc(a,b,opr); } void calc(float x,float y,char opr) { switch(opr) { case '+':printf("%5.2f%c%5.2f=%6.2f\n",x,opr,y,x+y);return; case '-':printf("%5.2f%c%5.2f=%6.2f\n",x,opr,y,x-y);return; case '*':printf("%5.2f%c%5.2f=%6.2f\n",x,opr,y,x*y);return; case '/':printf("%5.2f%c%5.2f=%6.2f\n",x,opr,y,x/y);return; default :printf("Operator err! \n"); } } 对被调函数的声明
【例7.10】 哥德巴赫猜想之一是任何一个大于5的偶数都可以表示为两个素数之和。验证这一论断。 • 输入n的值 • for (a=6; a<=n; a+=2) • for(b=3;b<=a/2;b+=2) • b是素数 ? • T F • c=a-b • c是素数? • T F • 输出: • a、b、c的值 • break; 图 7.2 验证哥德巴赫猜想
程序如下: 可以在main函数的前面对prime函数进行声明。实际上,该声明可以省略,为什么? #include "math.h" int prime(int n); main( ) { int a,b,c,n; scanf("%d",&n); for (a=6; a<=n; a+=2) for (b=3; b<=a/2; b+=2) if (prime(b)) { c=a-b; if (prime(c)) { printf("%d=%d+%d\n",a,b,c); break; } } } /* 穷举法判断素数 */ int prime(int n) { int i; for (i=2; i<=sqrt(n); i++) if (n%i==0) return 0; return 1; }
7.3 数组作函数参数 7.3.1 一维数组元素作函数参数 【例7.11】求5个数中的最小值。 int min(int x, int y) { return (x<y?x:y); } main( ) { int a[5],i,m ; for (i=0; i<5; i++) scanf("%d",&a[i]); m=a[0]; for (i=1; i<5; i++) m=min(m,a[i]); printf("%d\n", m); } 用打擂台方法求最小值。m相当于擂主
数组名表示数组在内存中的起始地址。 例如:数组a在内存中从2000地址开始存放,则a的值为2000。2000是地址值,是指针类型的数据(第8中将介绍指针类型),不能把它看成是整型或其他类型数据。 实参是数组名,形参也应定义为数组形式,形参数组的长度可以省略,但[ ]不能省,否则就不是数组形式了。 【例7.12】用冒泡法将10个整数排序。 7.3.2 一维数组名作函数参数
void printarr(int b[10]) { int i; for (i=0; i<10; i++) printf("%5d",b[i]); printf("\n");} void sort(int b[ ],int n); void printarr(int b[ ]); main( ) { int a[10] = {11,22,63,97,58,80,45, 32,73,36}; printf("Before sort:\n"); printarr(a); sort(a,10); printf("After sort:\n"); printarr(a); } void sort(int b[ ], int n) { int i,j,t; for (i=1; i<n; i++) for (j=0; j<n-i; j++ ) if (b[j]>b[j+1]) { t=b[j];b[j]=b[j+1];b[j+1]=t; } }
a[0] a[1] a[2] a[3] a[4 ] a[5] a[6] a[7] a[8] a[9] b[0] b[1] b[2] b[3] b[4] b[5] b[6] b[7] b[8] b[9] 首地址: 2000 首地址: 2000 (a) 排序前 a[0] a[1] a[2] a[3] a[4 ] a[5] a[6] a[7] a[8] a[9] b[0] b[1] b[2] b[3] b[4] b[5] b[6] b[7] b[8] b[9] (b) 排序后 11 22 63 97 58 80 45 32 73 36 11 22 32 36 45 58 63 73 80 97 实参赋给形参 形参 b 实际是一个 可以存放地址的变量 2000 a:2000 b 图7.3 调用sort函数
【例7.13】编程序,实现字符串连接。 #include "stdio.h" main( ) { void scat(char str1[ ],char str2[ ]); char s1[50],s2[50]; int i,k; printf("Input s1:"); gets(s1); printf("Input s2:"); gets(s2); scat(s1,s2); printf("Output s1:%s\n",s1); printf("Output s2:%s\n",s2); } void scat(char str1[ ],char str2[ ]) { int i=0,k=0; while (str1[i]!='\0') i++; while (str2[k]!='\0') { str1[i]=str2[k]; i++; k++; } str1[i]='\0'; } scat函数还可简化为: void scat(char str1[ ],char str2[ ]) { int i=0,k=0; while (str1[i]) i++; while (str1[i++]=str2[k++]); }
7.3.3 多维数组作函数参数 1 59 2 610 3 711 4 812 1 2 3 4 5 6 7 8 9 10 11 12 a= b= 以二维数组为例。二维数组名作实参时,对应的形参也应该定义为一个二维数组形式。对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小说明。 【例7.14】编程序,将矩阵转置。设转置前为a矩阵,转置后为b矩阵,如下所示: 思路:将a[0][0]b[0][0],a[0][1] b[1][0],a[0][2]b[2][0], a[1][0]b[0][1],…,a[i][j]b[j][i],…。
void turn( ); main( ) { int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}; int i,j,b[4][3]; … turn(a,b); printf("array b:\n"); for (i=0; i<4; i++) { for (j=0; j<3; j++) printf("%5d",b[i][j]); printf("\n"); } } /* 矩阵转置函数 */ void turn(int arra[ ][4],int arrb[ ][3]) { int r, c; for (r=0; r<3;r++) for (c=0; c<4; c++) arrb[c][r]=arra[r][c]; }
7.4.1 函数的嵌套调用 7.4 函数的嵌套调用和递归调用 main函数 {…… 调用函数 A; …… } 函数 B {…… …… …… } 函数 A { …… 调用函数 B; …… }
【例7.15】函数的嵌套调用 main( ) { int n=3; printf ("%d\n",sub1(n)); } sub1(int n) { int i,a=0; for (i=n; i>0; i--) a+=sub2(i); return a ; } sub2(int n) { return n+1; } 程序输出结果: 9
7.4.2 函数的递归调用 直接调用sub 函数本身 1.递归的基本概念 递归调用:一个函数直接或间接地调用了它本身,就称为函数的递归调用。 递归函数:在函数体内调用该函数本身。 例如: int sub(int x) { int y,z; …… if( …… ) z=sub(y); else { …… } return ; }
2.递归函数的执行过程 1 (n=0, 1) n×(n-1)! (n>1) n! = 【例7.16】编一递归函数求n!。 思路:以求4的阶乘为例: 4!=4*3!,3!=3*2!,2!=2*1!,1!=1,0!=1。 递归结束条件:当n=1或n=0时,n!=1。 递归公式:
程序如下: float fact (int n) { float f=0; if(n<0) printf("n<0,error!"); else if (n==0 || n==1) f=1; else f=fact(n-1)*n; return (f); } main( ) { int n; float y; printf("\nInput n:"); scanf("%d",&n); y=fact(n); printf("%d!=%-10.0f\n",n,y); } 运行情况如下: Input a integer number:4 4!=24
递归调用过程 回 推 main( ) fact(4) fact(3) fact(2) fact(1) { { { { { … … … … … y=fact(4); f=4*fact(3); f=3*fact(2); f=2*fact(1); f=1; … … … … … return 24 return 6 return 2 return 1 } } } } } 递 推
3.编制递归函数的方法 1 (n=0) x × xn - 1(n>0) xn = ⑴ 数值型问题递归函数的编程方法 对于数值型问题,首先要找出解题的数学公式,这个公式必须是递归定义的,且所处理的对象要有规律地递增或递减,然后确定递归结束条件。 【例7.17】编一递归函数求xn。 思路:首先把xn转化成递归定义的公式 再找出递归结束条件:当n=0时,xn=1。
程序如下: long xn(int x,int n) { long f=0; if (n<0) printf("n<0,data error!\n"); else if (n==0) f=1; else f=x*xn(x,n-1); return (f); } main( ) { int n,x; long y; scanf("%d,%d",&x,&n); y=xn(x,n); printf("%ld\n",y); } 程序运行情况如下: 2,10 1024
⑵ 非数值型问题递归函数的编程方法 有些问题不能直接用数学公式求解。非数值型问题比数值型问题更难找出递归的算法。它不能用一个递归公式表示。解决这类问题首先要把问题将大化小,将繁化简。将一个复杂的问题化解成若干个相对简单的小问题,而某个小问题的解法与原问题解法相同,并且越来越简单直至有确定的解。 【例7.18】编制一递归函数,将一个十进制正整数(如:15613)转换成八进制数形式输出。
⑵ 非数值型问题递归函数的编程方法(续) 余数: 商: 15613%8=5 15613/8=1951 1951%8=7 1951/8=243 243%8=3 243/8=30 30%8=6 30/8=3 3%8=3 3/8=0 结果:36375 图 7.5 十进制转换成八进制 思路:十进制整数转换成八进制整数的方法是除8逆向取余。如图7.5所示。
⑵ 非数值型问题递归函数的编程方法(续) • 该题实际上是要把一个十进制数除以8得到的余数逆向输出。就是先得到的余数后输出,最后得到的余数最先输出。 • 我们先由大化小:求八进制数变成求一系列余数的问题。求第一个余数是将15613除以8取余,因为先得到的余数后输出,所以把这个余数存在一个变量m中,接下去求下一个余数。和求第一个余数的方法相同,只是被除数变成了15613除以8的整数商1951。因此,这是一个递归调用的问题。定义变量m存放余数,x存放被除数。递归算法描述如下:
⑵ 非数值型问题递归函数的编程方法(续) ① 先求出余数m:m=x%8; ② 求x除以8取余后的整数商:x=x/8; ③ 如果x不等于0,递归调用该函数,否则执行④。 ④ 输出余数m。 ⑤ 返回调用点。
程序如下: #include "stdio.h" void dtoo(int x) { int m; m=x%8; x=x/8; if (x!=0) dtoo(x); printf("%d",m); } main( ) { int n; scanf("%d",&n); printf("%d=(",n); dtoo(n); printf(")8\n"); } 程序运行情况如下: 15613 15613=(36375)8
7.5 局部变量和全局变量及其作用域 问题:一个变量在程序的哪个函数中都能使用吗? 7.5.1 变量的作用域 变量的作用域:变量在程序中可以被使用的范围。 根据变量的作用域可以将变量分为局部变量和全局变量。 7.5.2 局部变量及其作用域 局部变量(内部变量):在函数内或复合语句内定义的变量以及形参。 作用域:函数内或复合语句内。 【例7.19】分析下面程序的运行结果及变量的作用域。
程序输出结果: main: a=1 b= 1 c= 1 sub: a=2 b= 3 c= 1 main: a=1 b= 1 c= 1 comp: a=2 b= 2 c= 1 main: a=1 b= 1 c= 1 局部变量 局部变量 局部变量 void sub(int a,int b) { int c; a=a+b; b=b+a; c=b-a; printf("sub:\ta=%d b= %d c= %d\n",a,b,c); } main( ) { int a=1,b=1,c=1; printf("main:\ta=%d b= %d c= %d\n",a,b,c); sub(a,b); printf("main:\ta=%d b= %d c= %d\n",a,b,c); { int a=2,b=2; printf("comp:\ta=%d b= %d c= %d\n",a,b,c); } printf("main:\ta=%d b= %d c= %d\n",a,b,c); } “分程序”或“程序块”
7.5.3 全局变量及其作用域 全局变量(外部变量):在函数外部定义的变量。 作用域:从定义变量的位置开始到本源文件结束。如在其作用域内的函数或分程序中定义了同名局部变量,则在局部变量的作用域内,同名全局变量暂时不起作用。 【例7.20】全局变量和局部变量的作用域。
main( ) { int b=6,c=7; f(b,c); printf("%d\t%d\t%d\n",a,b,c); { int a=9,b=8; printf("%d\t%d\t%d\n",a,b,c); {c=10; printf("%d\t%d\t%d\n", a,b,c); } printf("%d\t%d\t%d\n",a,b,c); } printf("%d\t%d\t%d\n",a,b,c); } 全局变量 局部变量 局部变量 局部变量 int a = 5; void f(int x, int y) { int b,c; b=a+x; c=a-y; printf("%d\t%d\t%d\n",a,b,c); } 程序输出结果: 5 11 -2 5 6 7 9 8 7 9 8 10 9 8 10 5 6 10
7.6 变量的存储类别及变量的生存期 程序代码区 静态存储区 动态存储区 存储分配 思考:1. 何时为变量分配内存单元? 2. 将变量分配在内存的什么区域? 3. 变量占据内存的时间(生存期)? 7.6.1 变量的生存期与变量的存储分类 变量的生存期:变量在内存中占据存储空间的时间。 静态存储变量 动态存储变量
7.6.2 变量的存储类别 数据类型: 决定为变量分配内存单元的长度, 数据的存放形式, 数的范围。 变量的属性 存储类别: 决定了变量的生存期, 给它分配在哪个存储区。
变量定义语句的一般形式 可省 main(){int x,y; … } main(){auto int x,y; … } 等价 自动变量 存储类别 数据类型 变量名1, … , 变量名n ; auto(自动的)register(寄存器的)static(静态的)extern(外部的) 1.自动变量(auto类别) 局部变量可以定义为自动变量。
自动变量 ⑴ 内存分配 调用函数或执行分程序时在动态存储区为其分配存储单元,函数或分程序执行结束,所占内存空间即刻释放。 ⑵ 变量的初值 定义变量时若没赋初值,变量的初值不确定;如果赋初值则每次函数被调用时执行一次赋值操作。 ⑶ 生存期 在函数或分程序执行期间。 ⑷ 作用域 自动变量所在的函数内或分程序内。