第八章 函数
8.1 概述
程序的组成:程序模块即子程序
在C语言中, 子程序的作用是由函数采完成的 。
一个 C程序可由一个主函数和若干个函数构成 。 由主函
数调用其函数, 其它函数也可以互相调用 。 同一个函数
可以被一个或多个函数调用任意多次 。
函数的作用,优点?
? main
? a b c
? d e f g h h I
? e g
图 8.1
一个程序中函数调用的示意图。
[例 8.1]
main()
{ printstar(); / *调用 printstaar函数 */
print_message();/ *调用 print_message函数 */
printstar(); / *调用 printstar函数 */
}
printstar() / *printstar函数 */
{ printf(“**************\n”);}
print_message() / *print_message函数 */
{ printf(“how do you do\n”);
}
运行情况如下,********************
how do you do
********************
说明,
1,以源文件为单位进行 编译,而不是以函数
为单位进行编译。
2,对于一个较大的源程序,可分别放到若干
个源文件中,再由若干源文件组成一个 C程序。
这样分别编写,分别编译,可提高调试效率。
一个源文件可以为多个 C程序公用。
3,C程序的执行从 main函数开始,调用其它
函数后流程回到 main函数,在 main函数中结束
整个程序的运行。 main函数是系统定义的。
4,所有函数都是平行的,即在定义函数时是
互相独立的,一个函数并不从属于另一函数,
即函数不能嵌套定义,但可以互相调用,
其他函数不能调用 main函数。
5,函数的分类,
? 从用户使用的角度,
? 标准函数、用户自定义函数
? 从函数的形式分,
? 无参函数、有参函数
8.2 函数定义的一般形式
1,无参函数的定义形式
类型标识符 函数名 ( )
{ 声明部分
语句
}
void print()
{ printf(“Hello”);
}
2.有参函数定义的一般形式
类型标识符 函数名(形式参数表列)
形式参数说明
{说明部分
语句 }
例如,
int max( int x,int y) ---没有分号
{ int z; /*函数体中的说明部分 */
z=x>y?x:y;
return( z) ;
}
?
3.可以有, 空函数,,它的形式为,
类型说明符 函数名()
{ … }
例如,
dummy()
{ }
8,3 函数参数和函数的值
8.3.1 形式参数和实际参数
在调用函数时,主调函数和被调用函数之间有数据传
递关系。
在定义函数时函数名后面括弧中的变量名称 形参 ;
在调用函数时,函数名后面括弧中的表达式称为, 实际参
数,简称 实参 。
[例 8.2] main()
{ int a,b,c;
scanf(, %d,%d”,&a,&b);
c= max(a,b);
printf(, max is %d”,c) ; }
a,b 为实参,x,y为形参( max函数中的参数)
关于形参与实参的说明,
1,形参变量,在未出现函数调用时,它们不占内
存中的存储单元。只有在发生函数调用时形参才被分
配内存单元。在调用结束后,所占的内存单元也被释
放。
2,实参可以是常量、变量或表达式,如,
max(a+b,a-b);
但要求有确定的值。在调用时将实参的值赋给形参变量
(如果形参是数组名,则传递的是数组首地址,而不
是变量的值 )。
3,在被定义的函数中,必须指定形参的类型。
4,实参与形参的类型应一致,否则发生, 类型不
匹配, 的错误。字符型与整型可以互相通用。
5,C语言规定,实参变量对形参变量的传递是,值传递
“,即单向传递。在内存中,实参单元对形参单元是不
同的 单元。
在调用函数时,给形参分配存储单元,并将实参对应
的值传递给形参,调用结束后,形参单元被释放,实参
单元仍保留并维持原值。
在执行一个被调用函数时,形参的值如果发生改变,
并不会改变主调函数的实参的值。
例,p158
8.3.2 函数的返回值
1.函数的返回值 是通过函数中的 return语句获得的。
被调用函数中只有通过 return语句才能返回一个 确定
的 函数值。不需要返回函数值可以不要 return语句。
函数中可有多个 return语句。
return语句后面的括弧也可以不要,
return z 等价于 return( z)
return后面的值可以是一个表达式。
例如, max( int x,int y)
{ return( x>y? x,y); }
2,函数值的类型 。函数的返回值属于某一个确定的
类型,应在定义函数时指定函数值的类型。
例如,int max( x,y) 函数值为整型
char letter( ci,c2)函数值为字符型
double min( x,y) 函数值为双精度型
C语言规定,凡不加类型说明的函数,一律自动按整型处理。
3.如果函数值的类型和 return语句中表达式的值
不一致,则以函数类型为准。对数值型数据,可以自
动进行类型转换。即函数类型决定返回值的类型。
例8,3
4·如果被调用函数中没有 return语句,函数并不是不带回
值,而只是不带回有用的值,带回的是一个不确定的、
用户不需要的值。
a=printstar();
5· 对于, 不带回值,,可以用, void”定义, 无类型,
(或称, 空类型, )
void printstar()
{………}
void print_message( )
{…………}
8.4 函数的调用
8.4.1 函数调用的一般形式
函数名(实参表列);
注意:调用无参函数则实参表列可以没有,但括弧不能省略。
如,clrscr(); getch()
? 实参表列包含多个实参则各参数间用逗号隔开。
? 实参与形参的个数相等,类型一致,顺序对应,一一
传递数据。
? 对实参表求值的顺序并不是确定的,TurboC是按自
右而左的顺序求值;
例:8,4
8.4.2 函数调用的方式
1.函数语句,一般不要求函数带回值,只要
求函数完成一定的操作。
printstar();
2·函数表达式 。要求函数带回一个确定的值以
参加表达式的运算。
c=2*max(a,b);
3,函数参数 。函数调用作为一个函数的实参。
m=max( a,max( b,c)) ;
8.4.3 函数声明和函数原型
调用函数时, 应具备以下条件,
1·首先被调用的函数必须是已经存在的函数
( 是库函数或用户自己定义的函数 ) 。
2·如果使用库函数,应使用文件包含语句,
# include,stdio.h”
# include,math,H”
# include,string.h”
3,如果使用同一个文件中用户自定义的函数,一般还应该
在主调函数中对被调用函数进行声明,声明形式为,
类型标识符 被调用函数名(参数说明);
以上声明又称为 函数原型
[例 8.5] main()
{ float add( float x,float x) ; /*内部声明 */
float a,b,c;
scanf(, %f,%f”,&a,&b) ;
c=add( a,b),
printf(, sum is %f”,c) ; }
float add( float x,float y) / *定义 add函数 */
{ f1oat z;
z=x+y;
return( z) ;}
C语言在 从上而下逐行编译 检查程序时,有以下规定,
( 1)如果函数的值(返回值)是整型或字符型,可
以不必进行说明,系统对它们自动按整型说明。
( 2)如果被调用函数的定义出现在主调函数之前,
在主调函数中可以不必加以说明。
如果在函数调用之前,没有声明被调函数,则
将第一次遇到的被调函数作为对函数的声明,并默认为
整型。
(3) 函数的声明与定义不同。
声明的作用是把函数的名字、类型、以及形参的类型、个数、
顺序告诉编译系统,以便系统进行检查。
( 4)函数声明时可不指定形参的名字,但必须指定
类型。
例 8,5改写如下(即把 main函数放在 add函数
的下面),就不必在 max函数中对 add进行说明。
float add( float x,float y)
{ float z;
z=x+y;
return( z); }
main()
{ float a,b,c;
scanf( "%f,%f",&a,&b) ;
c= add( a,b) ;
printf( "%f",c);
}
( 3) 如果已在所有函数定义之前,在文件的开头,在函
数的外部已说明了函数类型,则在各个主调函数中不
必对所调用的函数再作类型说明。
例如,
float f(floatx,floaty); /*外部声明 */
main( )
{...} / *不必说明函数 f的类型 */
float f( float x,float y)/ *f函数定义 */
{··}
例 8.5
8.5 函数的嵌套调用
C语言的函数定义都是互相平行、独立的,在定义
函数时,一个函数内不能包含另一个函数(在定义一个
函数时,其函数体内又包含另一个函数的完整定义,这
就是嵌套定义,这个内嵌的函数只能被包含它的函数所
调用)
C语言允许嵌套调用函数,
Main函数 a函数 b函数
调用 a函数 调用 b函数
结束
F(x2)
x2
[例 8.6] 用弦截法求方程的根。
x^3-5*x^2+16*x-80= 0
方法如下,
( 1)取两个不同点调 x1,x2,如果 f(x1)和 f(x2)符
号相反,则 (x1,x2)区间内必有一个根。如果 f(x1)与
f(x2)同符号,则应改变 x1,x2,直到 f(x1),f(x2)异号
为止,注意 x1,x2的值不应差
太大,以保证 (x1,x2)区间内只有一个根。
( 2)连接 f( x1)和 f( x2)两点,此线(即弦)交
x轴于 x,见图 7·6。
点 x坐标可用下式求出,
x1*f(x2)-x2*f(x1)
x=
f(x2)-f(x1)
再从 x求出 f( x)。
图 8.6
x1 x
f(x1)
f(x)
(3)若 f(x)f(x1)同符号,则根必在( x,x2)区间内,此时将 x作
为新的 x1。如果 f(x)与 f( x2)同符号,则表示根在 (x1,x)_区间
内,将 x作为新的 x2。
( 4) 重复步骤 ( 2) 和 ( 3), 直到 |f( x) |为一个很小的数, 例
如 10-6。 此时认为 f( x) ~~ 0。
根据上述思路画出 N-s流程图, 见图 8,7。
输入 x1,x2,求 f(x1),f(x2)
直到 f(x1) f(x2) 异号
求 f(x1)与 f(x2)连线与
x轴交点
y=f(x) y1=f(x1)
y与 y1同号
t f
x1=x y1=y x2=x y2=y
直到 |y|< 0.000001
root=x 输出 root
图 8.7
分别用几个函数来实现各部分功能,
( 1)用函数 f( x)来求 x的函数,x^3一 5x^2+ 16x
一 80。
( 2)用函数 xpoint( x1,x2)来求 f( xl)和 f( x2)
的连线与 x轴的交点 x的坐标。
( 3)用函数 root(x1,x2)来求( x1,x2)区间的那
个实根。显然,执行 root函数过程中要用到函数
xpoint,而执行 xpoint函数过程中要用到 f函数。
# include,math,h,
float f(float x)
/ *定义,f(x)=x^3一 5x^2 +16x一 80*/
{float y;
y=((x-0.5)*x+16.0)*x-80.0;
return (y); }
float xpoint( float x1,float x2)
/ *定义 xpoint函数,求出弦与 x轴交点。 */
{float y;
y=( xl*f(x2)-x2*f(x1))/( f( x2)一 f( xl));
return( y);
}
float root(float x1,float x2)
/ *定义 root函数,求近似根 */
{ float x,y,yl;
y1= f(x1);
do
{ x=xpoint(x1,x2);
y= f(x);
if( y*yl> 0) / *f( x)与 f(x1)同符号。 */
{yl=y;
x1=x;}
else
x2=x;
} whi1e ( fabs(y)>=0.0001);
return(x); }
main ( ) / *主函数 */
{float x1,x2,f1,f2,x;
do
{
printf( "input x1, x2,\n”) ;
scanf( "%f,%f",&x1,&x2) ;
fl=f(x1); f2=f(x2);}
while ( fl*f2) >=0);
x= root( x1,x2);
printf( "A root of equation is% 8.4f",x);
}
运行情况如下,
input x1,X2,
2,6
A root of equation is 5.0000
从程序可以看到,
( 1) 在定义函数时, 函数名为 f,xpoint,root的
三个函数是互相独立的, 并不互相从属 。
( 2) 3个函数的定义均出现在 main函数之前, 因此在
main函数中不必对这三个函数作类型说明 。
( 3)程序从 main函数开始执行。
先执行一个 do一 while循环,作用是:输入 x1和 x2,
判别 f(x1)和 f(x2)是否异号,如果不是异号则重新输
入 x1和 x2,直到满足 f( x1)与 f( x2)异号为止。然
后用函数调用 root( x1,x2)求根 x。调用 root函数过
程中,要调用 xpoint函数来求 f(x1)与 f(x2)连线的交
点 x。在调用 xpoint函数过程中要用到函数 f来求 x1和
x2的相应的函数值 f( xl)和 f( x2)。这就是函数的
嵌套调用。见图 8·8。
main函数 root函数 xpoint函数 f函数
调用 root函数 调用 xpoint函数 调用 f函数
输出根 x
结束
图 8.8
8.6 函数的递归调用
在调用一个函数的过程中又出现直接或间
接地调用该函数本身,称为 函数的递归调用 。
C语言允许函数的递归调用。例如,
int f( int x)
{ int y,z;
z=f(y);
return (2*z); }
递归调用不应出现无终止的递归调用,而只应
出现有限次数的、有终止的递归调用
例 8.7
age( 5) =age( 4)+ 2
age( 4) =age( 3)+ 2
age( 3) =age (2) +2
age( 2)= age(1)+ 2
age( 1)= 10
可以用式子表述如下,
10 ( n=1)
age( n) =
age( n-1)+2 ( n> 1)
可以看到,当 n>= 1时,求第 n个人的年龄的公式是相
同的。因此可以用一个函数来表示上述关系,图 8.11
表示求第 5个人年龄的过程。
age(5) age(5)
=age(4)+2 =18
age(4) age(4)
=age(3)+2 =16
age(3) age(3)
=age(2)+2 =14
age(2) age(2)
=age(1)+2 = 12
age(1)
=10
从图可知,求解可分成两个阶段:第一阶段是, 回推,,
即将第 n个人的年龄表示为第( n-1)个人年龄的函数,
此时 age(1)已知,回推结束。
图 8.11
第二阶段,采用递推方法,从第 1个人的已知年龄推算
出第 2个人的年龄( 12岁),从第 2个人的年龄推算出 3
个人的年龄( 14岁) ……,一直推算出第 5个人的年龄
( 18岁)为止。
一个递归的过程可分为“回推”和“递推”两
个阶段。
一个递归的过程必须具有一个结束递归过程的
条件。
例如,age( 1)= 10,是使递归结束的条件。
可以用一个函数来描述上述递归过程,
int age( int n) /*求年龄的递归函数 */
{ int c;/ * c用作存放函数的返回值的变 */
if( n==1) c=10;
else c=age( n一 1) 十 2;
return( c); }
main( )
{printf( "%d",age( 5)) ; }
运行结果如下,18
main函数中只有一个语句 。
函数调用过程如图 8.12所示 。
Main age函数 age函数 age函数 age函数 age函数
n= 5 n=4 n=3 n= 2 n=1
age(5)=18 age(4)=16 age(3)=14 age(2)=12 age(1)=10
age(5)
并输出 C=age(4)+2 C=age(3)+2 C=age(2)+2 C=age(1)+2 C=10
图 8.12
[例 8,8]用递归方法求 n!
求 n!也可以用递归方法, 即 5! 等于 4! *5,
而 4! =3! *4……,1! =1。
可用下面的递归公式表示,
1 ( n=0,1)
n!= (n-1)!*n (n> 1)
1 ( n=0,1)
n!= (n-1)!*n (n> 1)
程序,float fac( int n)
{ float f;
if( n< 0) printf(, n< 0,data
error\n”);
else if( n==0||n==1) f=1;
else f= fac( n——1) *n;
return( f); }
main()
{ int n;
float y;
printf(, input a integer
number,, );
scanf(, %d”,&n);
y= fac( n);
printf(, %d!=%15.0f”,n,y);}
[例 8,9] Hanoi 汉诺 塔问题。
p163
8.7 数组作为函数参数
变量、数组元素可以做函数实参,其用法相
同。 数组名也可以做实参和形参,传递的是整
个数组。
一,数组元素 做函数实参
数组元素作为函数的实参,与用变量做
实参一样,是单向传递,即, 值传送, 方式。
[例 8.10] p178
二, 可以用数组名作函数参数, 此时实参与形参
都应用数组名 ( 或用数组指针, 见第 9章 。 )
[例 8.11] p179
f1oat average( float array[])
{ int i;
f1oat aver,sum=array[0];
for( i=1; j< 10; i++) sum=sum+ array[0];
aver=sum/ 10;
return(aver);}
main()
{f1oat score[10];
int i;
printf( "input l0 scores,\ n”);
for( i=0; i<=10; i++) scanf ( "%f",&score[i]);
printf( "\n");
aver = average( score);
printf( "average score is % 5·2f”,aver); }
说明,
1。用数组名作函数参数,实参与形参类型必须
一致;
2。 形参数组可以不指定大小;
3。数组名作参数时,不是“值传送”,不是单
向传递,而是把实参数组的起始地址传递给形
参数组,这样 两个数组共占同一段内存。
( 此时形参数组的变化事实上将会引起实参数组的变
化。)
求平均值,
/* example 8.12 on page 180 */
float average(float array[],int n)
{ int i;
float aver,sum=array[0];
for(i=1;i<n;i++) sum=sum+array[i];
aver=sum/n;
return(aver);
}
main()
{
float s1[5]={98.5,97,91.5,60,55};
Float s2[10]={67,89,99,69,77,89.5,76,54,60,99};
printf("the average of class A is
%5.2f\n",average(scor1,5));
printf("the average of class B is
%5.2f\n",average(scor2,10));
}
[例 8.13] p181
用选择法对数组中 10个整数按由小到大排序。所谓选
择法就是:先将 10个数中最小的数与 a[0]对换;再将
a[1]到 a[9]中最小的数与 a[1]对换; ……,每比较一轮,
找出一个未经排序的数中最小的一个。共应比较 9轮。
程序如下,
void sort( int array[],int n)
{ int i,j,k,t;
for( i= 0; i<=n-1;i++)
{ k=i;
for( j=i+1;j<=n;j++)
if (array[j]< array[k] ) k= j;
if (k!=i){t=array[k];array[k]=array[i];array[i]=t;} }
main()
{ int a[10],i;
printf(, enter the array\n”) ;
for( I=0; i< 10; i十十 ) scanf(, %d”,&a[i]) ;
sort( a,10);
printf(, the sorted array,\n”);
for( i= 0;i< 10;j++) printf(, % d”,a[i]);
printf(, \n”); }
三,用多维数组作函数参数
多维数组元素可以作为实参 ;
多维数组名作为实参和形参;
对形参数组定义时可以指定每一维的大小,也可省
略第一维的大小说明。
int array[ 3] [10];
或 int array[] [10]; 二者都合法而且等价。
不能把第二维以及其它高维的大小说明省略。
int array[ 3] []; --不合法
int array[ ][ ] ; --不合法
原因,从实参传送来的是数组起始地址,在内存中按
数组排列规则存放(按行存放),而并不区分行和列,
如果在形参中不说明列数,则系统无法决定应为多少
行多少列。
实参数组的长度可以与形参数组不同
例如,实参数组定义为
int score[ 5] [10];
而形参数组定义为
int array[ 3] [10];
这时形参数组只取实参数组的一部分,其余部分不起作
用。
[例 8.14] 有一个 3X4的矩阵,求其中的最大元素。
程序如下,
max_value( int array[][4])
{ int i,j,k,max;
max= array[0][0];
for (i= 0; i< 3; i十十 )
for( j= 0; j< 4; j十十 )
if( array[ I] [j]> max) max=array[i][j];
return ( max) ; }
main()
{ int a[3][4]= {{1,9,7,3},{2,4,6,8},{15,17,
34,12}};
printf(, max value is % d \n”,max_value(a)) ;}
8.8 局部变量和全局变量
8.8.1 局部变量
定义:在一个函数内部定义的变量是内部变量,它只
在本函数范围内有效,在此函数以外是不能使用这些
变量的。
? 包括主函数在内的各函数内部定义的变量都只在各函
数内有效,且不能相互调用;
? 不同函数中可以使用相同名字的变量;
? 形参是局部变量;
? 可在复合语句中定义变量;
float fl( int a) / *函数 f1*/
{int b,c; a,b,c有效
……
}
char f2( int x,int y)
{int i,j;
…… x,y,i,j有效
}
main()
{int m,n;
…… m,n有效
}
8.8.2 全局变量
在函数之外定义的变量称为 外部变量, 外部
变量是全局变量 。
全局变量可以为本文件中其它函数所共用 。
它的 有效范围 为:从定义变量的位置开始到
本源文件结束 。
int p=1,q=5; / *外部变量 */
float f1( int a); / *定义函数 fl*/
{ int b,c; 全局变量
………} p,q的作用
char c1,c2; / *外部变量 */ 范围
char f2(int x,int y); /*定义函数 f2*/
{int i,j;
· ……} 全局变量 c1 c2的
main ( ) / *主函数 */ 作用范围
{ int m,n;
……… }
全局变量的作用说明,
1,增加了函数间数据联系的渠道,同时可以从函数
得到一个以上的返回值。
2.全局变量以减少函数实参与形参的个数,从而减少
内存空间以及传递数据时的时间消耗。
P185 例 8.15
思考:使用全局变量的缺点
3.如果在同一个源文件中,外部变量与局部变量同名,
则在局部变量的作用范围内,外部变量不起作用。如,
[例 8.16]
int a=3,b=5; / *a,b为外部变量 */ a,b作用范围
Max(a,b);
int a,b; / *a,b为局部变量 */
{int c; 形参 a,b
c=a>b?a:b; 作用范围
return ( c) ;
}
main() 局部变量 a作用范围
{ int a=8;/ *a为局部变量 */
printf(, %d”,max( a,b));
} 全局变量 b的作用范围
8.9 变量的存储类别
8.9.1 动态存储方式与静态存储方式
变量从作用域 ( 空间 ) 角度可分为 全局变量和局部变量 。
变量从变量值存在的时间(生存期)角度可分为 静态存
储变量和动态存储变量。
静态存储方式是指在程序运行期间分配固定的存储空间
的方式。
动态存储方式则是在程序运行期间根据需要进行动态的
分配存储空间的方式。
内存中的供用户使用的存储空间的情况。这个存
储空间可以分为三部分,见图 8.16。
1.程序区
2.静态存储区
3.动态存储区
数据分别存放在静态存储区和动态存储区中。
静态存储区中,全局变量
全局变量存放在静态存储区中,在程序开始执行时给全局
变量分配存储区,程序执行完毕就释放。在程序执行过程
中它们占据固定的存储单元,而不是动态地分配和释放的。
程序区
静态存储区
动态存储区
动态存储区 中存放以下数据,
①函数形参变量。在调用函数时给形参变量分配
存储空间。
②局部变量(未加 static说明的局部变量,即自
动变量)。
③函数调用时的现场保护和返回地址等。
动态的含义:动态分配和释放存储空间
如果在一个程序中两次调用同一函数,分配给此函
数中局部变量的存储空间地址可能是不相同的。
在 C语言中每一个变量和函数有两个属性,
? 数据类型;
? 数据的存储类别 。
存储类别指的是数据在内存中存储的方法。
存储方法分为两大类:静态存储类和动态存储类。
具体包含四种,
? 自动的( auto),
? 静态的( static),
? 寄存器的( register),
? 外部的( extern)。
8.9.2 auto变量
自动变量用关键字 auto作存储类型的说明。
函数中的局部变量,如不做专门的说明,都是动态分
配存储空间的,存储在动态存储区中,这类局部变量称
为自动变量。
, auto”也可以省略,auto不写则隐含确定为“自动存
储类别”,它属于动态存储类别。
auto int b,c=3;
int b,c=3; 二者等价
8.9.3 static变量
static 类型 变量名
局部静态变量,函数中的局部变量的值在函数调用结束
后不消失而保留原值 (其占用的存储单元不释放 ),在
下一次该函数调用时,该变量已有值,就是上一次函
数调用结束时的值。
[例 8,17]
对局部静态变量的说明,
1.局部静态变量属于静态存储类别,在静态存储区内
分配存储单元。在程序整个运行期间都不释放。而自
动变量(即局部动态变量)属于动态存储类别,占动
态存储区空间而不占固定空间,函数调用结束后即释
放。
2.对局部静态变量是在编译时赋初值的,即只赋初
值一次,在程序运行时它已有初值。以后每次调用函
数时不再重新赋初值而只是保留上次函数调用结束时
的值。而对自动变量赋初值,不是在编译时进行的,
而在函数调用时进行,每调用一次函数重新给一次初
值,相当于执行一次赋值语句。
3,如在定义局部变量不赋初值的话,则对静态变量来说,
编译时自动赋初值 0(对数值型变量)或空字符(对字符
变量) 。
(对自动变量来说,如果不赋初值则它的值是一个不确定的值。
这是由于每次函数函数调用结束后存储单元已释放,下次调用时又
重新另分配存储单元,而所分配的单元中的值是不确定的。 )
4.虽然局部静态变量在函数调用结束后仍然存在,但其
它函数是不能引用它的。
局部静态变量的使用示例,
1.需要保留函数上一次调用结束时的值。 例如,
[例 8.18] 打印 1——5的阶乘。
int fac( int n)
{ static int f= 1,
f=f*n;
return( f); }
main()
{int i;
for( i= 1; I<=5;i++)
printf(, % d! =% d”,i,fac( i));
}
2.如果初始化后,变量只被引用而不改变其值,则这时用局部静态
变量比较方便,以免每次调用时重新赋值。
例如,例 8.19是输出 1到 5的阶乘值 。
int fac( int n)
{ register int i,f=1; / *定义寄存器变量 */
for ( i= l; i<n; i++ )
f=f*i; return( f) ;
main( )
{ int i;
for ( i= 1; 1<= 5; i十十 )
printf(, % d! = % d\n”,fac( i)) ; }
定义局部变量 f和 i是寄存器变量, 如果 n的值大, 则
能节约许多执行时间 。
8.9.4 Register变量
说明,
1.只有局部自动变量和形式参数可以作为寄存器变量,
其它(如全局变量)不行。在调用一个函数时占用一些寄
存器以存放寄存器变量的值,函数调用结束释放寄存器。
2.一个计算机系统中的寄存器数目是有限的,不能定义
任意多个寄存器变量。
3.局部静态变量不能定义为寄存器变量。不能写成
register static int a,b,c;
不能把变量 a,b,c既放在静态存储区中,又放在寄存器
中,二者只能居其一。
8.9.5 用 extern声明外部变量
1.在一个文件内声明外部变量
外部变量在文件开头定义,在整个文件范围内都有效;否则其 作用范围只限于定义点到文件终了。
int max( int x,int y)/ *定义 max函数 */
{int z;
z= X> y? x,y;
return(z) ;}
main()
{ extern int a,b; / *外部变量说明 */
printf(, %d”,max(a,b)) ;}
int a=13;b=-8; / *外部变量定义 */
2.在多文件的程序中声明外部变量
在不同的文件中都要用到同一个外部变量 a时,
在一个文件中定义该变量;
int a;
在其他的文件中做外部变量声明,
extern a;
8.9.6 用 static 声明外部变量
Static int a;
表示外部变量 a此时仅限于本程序使用,而不能被其
他文件使用。
比较以下两种外部变量定义的区别和共同点,
Int a;
Static int a;
8.9.7 变量的声明与定义
定义性声明
引用性声明
8.9.8 存储类别小结
对一个数据的定义, 需要指定两种属性,
数据类型和存储类别,
分别用两个关键字进行定义 。 如,
static int a; ( 静态内部变量或静态外部变量 )
auto char c; ( 自动变量, 在函数内定义 )
register int d; ( 寄存器变量, 在函数内定义 )
extern int b;( 说明 b是一个已被定义的外部变量 )
1.从 作用域角度 分,有局部变量和全局变量。它们
采取的存储类别如下,
自动变量,即动态局部变量(离开函数,值
就消失)
局部变量 静态局部变量(离开函数,值仍保留)
寄存器变量(离开函数,值就消失)
形式参数可以定义为自动变量或寄存器变量)
静态外部变量(只限本文件用)
全局变量
外部变量(允许其他文件用)
2.从 变量存在的时间 来区分,有动态存储和静态存
储两种类型。静态存储是程序整个运行时间都存在,
而动态存储则是在调用函数时临时分配单元。
(自动变量(本函数内有效)
动态存储) 寄存器变量(本函数内有效)
(形式参数
(静态局部变量(函数内有效)
静态存储,静态外部变量(本文件内有效)
外部变量(其它文件可引用)
3.从 变量值存放的位置 来区分,可分为,
(静态局部变量)
内存中静,静态外部变量(函数外部静态变量)
态存储区 外部变量(可为其它文件引用)
内存中动态存储区:自动变量和形式参数
CPU中的寄存器:寄存器变量
4.关于 作用域和生存期 的概念。从前面叙述可以知道,
对一个变量的性质可以从两个方面分析,一是从变量的作
用域,一是从变量值存在时间的长短,即生存期。前者是
从空间的角度,后者是从时间的角度。二者有联系但不是
同一回事。图 8.19是作用域的示意图。图 8.20是生存期的
示意图。如果一个变量在某个文件或函数范围内是有效的,
则称该文件或函数为该变量的作用域,在此作用域内可以
引用该变量,所以又称变量在此作用域内, 可见,,这种
性质又称为变量的, 可见性,,例如变量 a,b在函数 fl中
,可见, 。如果一个变量值 i在某一时刻是存在的,则认为
这一时刻属于该变量的, 生存期,,或称该变量在此时刻
,存在, 。表 7.2示各种类型变量的作用域和存在性的情况。
文件 file1,C
定义外部变量 a
main函数
f2;
f1;}
f1
{auto int b ; a的作用域
……,F2;……;} b的作用域
f2函数
( static int c;
……….;}
f1函数所用函数是否会与其它文件中函数同名,通常把只由
同一文件使用的函数和外部变量放在一个文件中,冠以
static使之局部化,其它文件不能引用。
8.10 内部函数和外部函数
8.10.1 内部函数
8.10,2 外部函数
在定义函数时, 如果冠以关键字 extern,表示此函数是
外部函数 。 如
extern int fun( a,b)
函数 fun可以为其它文件调用, 如果在定义函数时省略
extern,则隐含为外部函数 。 本书前面所用的函数都作为
外部函数 。
在需用调用此函数的文件中, 一般要用 extern说明所用
的函数是外部函数 。
[例 8,22]有一个字符串, 内有若干个字符, 今输入一个
字符, 程序将字符串中该字符删去 。 用外部函数实现 。
file1,C ( 文件 l)
main( )
extern enter_string(),delete_string(),
print_string();/ *说明本文件要用到其它文件中的函
数 */
char c;
static char str[80];
enter_string(str);
scanf(, %c”,&c);
delete_string(str,c);
print_string(str); }
file2,c(文件 2)
# indude,stdio.h”
extern enter_string( str)/ *定义外部函数
enter_string*/
char str[80];
{ gets ( str);} / *读入字符串 str*/
file3,c(文件 3)
extern delete_string( str,ch) ;/ *定义外部函数
delete_string*/
char str[],ch;
{ int i,j;
for ( i= j= 0; sir[I]! =’\0’; i++)
if( str[i]! =ch) str[j++]=str[i];
str[j]=’\0’;}
file4,c(文件 4)
extern print_string (str) / *定义外部函数
print_string*/
char str[];
{ printf(, %s”,str) ;
运行情况如下,
abcdefgc (输入 str)
c (输入要删去的字符
abdefg (输出已删去指走字符的字符串)
整个程序由四个文件组成,每个文件包含一个函数。主函
数是主控函数,由 4个函数调用语句组成。其中删 nf是库函
数。另外 3个是用户自己定义的函数,它们都定义为外部函
数。当然,定义时 extern不写也可以,系统隐含它们为外部
函数。在 main函数中用 extern说明在 main函数中用到的
enter一 string,delete一 string,print一 string是外部
函数。在有的系统中,也可以不在调用函数中对被调用的
函数作, 外部说明, 。
函数 de1ete_string的作用是根据给定的字符串 str和要
删除的字符, 对 str作删除处理 。 算法是这样的:对 str数
组的字符逐个检查, 如果不是被删除的字符就将它存放在
数组中, 见图 7,21( 设删除空格 ) 。
T h i s i s a c p r o g r a m \0
T h i s i s a c p r o g r a m \0
图 7.21
从 str[0]开始逐个检查数组元素值是否等于指定要删除
的字符,是就留在数组中,若是就不保留。从图中可以
看到,应该使 str[0]赋给 str[0],str[1]赋给 str[1],
str[2] 赋给 str[2],str[ 3]赋给 str[ 3],然后
str[5] 赋给。 str[5]……·请读者注意分析如何控制 i
和 j的变化,以便使被删除的字符不保留在原数组中。
这个题目当然可以设两个数组,把不删除的字符一一赋
给新数组,但我们只用一个数组,只把不被删除的字符
保留下来。由于 i总是大于或等于入因此最后保留下来
的字符不会覆盖未被检测处理的字符,最后将结束符
‘ \0’也复制到被保留的字符后面。
在用一般的方法进行编译连接时,先分别对 4个文件
进行编译,得到 4个。 oBJ文件。然后用 link把 4个目标
文件(,oBJ文件)连接起来。在 MS C上用以下命令,
link filel + file2+ file3十 file4/
得到一个可执行的文件。当然也可以用
#tinc1ude命令将 file2,C,file3,c和 file4,c包含
到 file1中,在 file1,c中的开头加 3行,
# include,file2,c”
# inciude,file3,C”
# include,11e4,c”
这时,在编译时,系统自动将这 3个文件放到 main函数
的前头,作为一个整体编译,而不是分 4个文件编译。这时,
这些函数被认为是在同一文件中,不再是作为外部函数被
其它文件调用了。 main函数中原有的 extern说明可以不要。
8.1 概述
程序的组成:程序模块即子程序
在C语言中, 子程序的作用是由函数采完成的 。
一个 C程序可由一个主函数和若干个函数构成 。 由主函
数调用其函数, 其它函数也可以互相调用 。 同一个函数
可以被一个或多个函数调用任意多次 。
函数的作用,优点?
? main
? a b c
? d e f g h h I
? e g
图 8.1
一个程序中函数调用的示意图。
[例 8.1]
main()
{ printstar(); / *调用 printstaar函数 */
print_message();/ *调用 print_message函数 */
printstar(); / *调用 printstar函数 */
}
printstar() / *printstar函数 */
{ printf(“**************\n”);}
print_message() / *print_message函数 */
{ printf(“how do you do\n”);
}
运行情况如下,********************
how do you do
********************
说明,
1,以源文件为单位进行 编译,而不是以函数
为单位进行编译。
2,对于一个较大的源程序,可分别放到若干
个源文件中,再由若干源文件组成一个 C程序。
这样分别编写,分别编译,可提高调试效率。
一个源文件可以为多个 C程序公用。
3,C程序的执行从 main函数开始,调用其它
函数后流程回到 main函数,在 main函数中结束
整个程序的运行。 main函数是系统定义的。
4,所有函数都是平行的,即在定义函数时是
互相独立的,一个函数并不从属于另一函数,
即函数不能嵌套定义,但可以互相调用,
其他函数不能调用 main函数。
5,函数的分类,
? 从用户使用的角度,
? 标准函数、用户自定义函数
? 从函数的形式分,
? 无参函数、有参函数
8.2 函数定义的一般形式
1,无参函数的定义形式
类型标识符 函数名 ( )
{ 声明部分
语句
}
void print()
{ printf(“Hello”);
}
2.有参函数定义的一般形式
类型标识符 函数名(形式参数表列)
形式参数说明
{说明部分
语句 }
例如,
int max( int x,int y) ---没有分号
{ int z; /*函数体中的说明部分 */
z=x>y?x:y;
return( z) ;
}
?
3.可以有, 空函数,,它的形式为,
类型说明符 函数名()
{ … }
例如,
dummy()
{ }
8,3 函数参数和函数的值
8.3.1 形式参数和实际参数
在调用函数时,主调函数和被调用函数之间有数据传
递关系。
在定义函数时函数名后面括弧中的变量名称 形参 ;
在调用函数时,函数名后面括弧中的表达式称为, 实际参
数,简称 实参 。
[例 8.2] main()
{ int a,b,c;
scanf(, %d,%d”,&a,&b);
c= max(a,b);
printf(, max is %d”,c) ; }
a,b 为实参,x,y为形参( max函数中的参数)
关于形参与实参的说明,
1,形参变量,在未出现函数调用时,它们不占内
存中的存储单元。只有在发生函数调用时形参才被分
配内存单元。在调用结束后,所占的内存单元也被释
放。
2,实参可以是常量、变量或表达式,如,
max(a+b,a-b);
但要求有确定的值。在调用时将实参的值赋给形参变量
(如果形参是数组名,则传递的是数组首地址,而不
是变量的值 )。
3,在被定义的函数中,必须指定形参的类型。
4,实参与形参的类型应一致,否则发生, 类型不
匹配, 的错误。字符型与整型可以互相通用。
5,C语言规定,实参变量对形参变量的传递是,值传递
“,即单向传递。在内存中,实参单元对形参单元是不
同的 单元。
在调用函数时,给形参分配存储单元,并将实参对应
的值传递给形参,调用结束后,形参单元被释放,实参
单元仍保留并维持原值。
在执行一个被调用函数时,形参的值如果发生改变,
并不会改变主调函数的实参的值。
例,p158
8.3.2 函数的返回值
1.函数的返回值 是通过函数中的 return语句获得的。
被调用函数中只有通过 return语句才能返回一个 确定
的 函数值。不需要返回函数值可以不要 return语句。
函数中可有多个 return语句。
return语句后面的括弧也可以不要,
return z 等价于 return( z)
return后面的值可以是一个表达式。
例如, max( int x,int y)
{ return( x>y? x,y); }
2,函数值的类型 。函数的返回值属于某一个确定的
类型,应在定义函数时指定函数值的类型。
例如,int max( x,y) 函数值为整型
char letter( ci,c2)函数值为字符型
double min( x,y) 函数值为双精度型
C语言规定,凡不加类型说明的函数,一律自动按整型处理。
3.如果函数值的类型和 return语句中表达式的值
不一致,则以函数类型为准。对数值型数据,可以自
动进行类型转换。即函数类型决定返回值的类型。
例8,3
4·如果被调用函数中没有 return语句,函数并不是不带回
值,而只是不带回有用的值,带回的是一个不确定的、
用户不需要的值。
a=printstar();
5· 对于, 不带回值,,可以用, void”定义, 无类型,
(或称, 空类型, )
void printstar()
{………}
void print_message( )
{…………}
8.4 函数的调用
8.4.1 函数调用的一般形式
函数名(实参表列);
注意:调用无参函数则实参表列可以没有,但括弧不能省略。
如,clrscr(); getch()
? 实参表列包含多个实参则各参数间用逗号隔开。
? 实参与形参的个数相等,类型一致,顺序对应,一一
传递数据。
? 对实参表求值的顺序并不是确定的,TurboC是按自
右而左的顺序求值;
例:8,4
8.4.2 函数调用的方式
1.函数语句,一般不要求函数带回值,只要
求函数完成一定的操作。
printstar();
2·函数表达式 。要求函数带回一个确定的值以
参加表达式的运算。
c=2*max(a,b);
3,函数参数 。函数调用作为一个函数的实参。
m=max( a,max( b,c)) ;
8.4.3 函数声明和函数原型
调用函数时, 应具备以下条件,
1·首先被调用的函数必须是已经存在的函数
( 是库函数或用户自己定义的函数 ) 。
2·如果使用库函数,应使用文件包含语句,
# include,stdio.h”
# include,math,H”
# include,string.h”
3,如果使用同一个文件中用户自定义的函数,一般还应该
在主调函数中对被调用函数进行声明,声明形式为,
类型标识符 被调用函数名(参数说明);
以上声明又称为 函数原型
[例 8.5] main()
{ float add( float x,float x) ; /*内部声明 */
float a,b,c;
scanf(, %f,%f”,&a,&b) ;
c=add( a,b),
printf(, sum is %f”,c) ; }
float add( float x,float y) / *定义 add函数 */
{ f1oat z;
z=x+y;
return( z) ;}
C语言在 从上而下逐行编译 检查程序时,有以下规定,
( 1)如果函数的值(返回值)是整型或字符型,可
以不必进行说明,系统对它们自动按整型说明。
( 2)如果被调用函数的定义出现在主调函数之前,
在主调函数中可以不必加以说明。
如果在函数调用之前,没有声明被调函数,则
将第一次遇到的被调函数作为对函数的声明,并默认为
整型。
(3) 函数的声明与定义不同。
声明的作用是把函数的名字、类型、以及形参的类型、个数、
顺序告诉编译系统,以便系统进行检查。
( 4)函数声明时可不指定形参的名字,但必须指定
类型。
例 8,5改写如下(即把 main函数放在 add函数
的下面),就不必在 max函数中对 add进行说明。
float add( float x,float y)
{ float z;
z=x+y;
return( z); }
main()
{ float a,b,c;
scanf( "%f,%f",&a,&b) ;
c= add( a,b) ;
printf( "%f",c);
}
( 3) 如果已在所有函数定义之前,在文件的开头,在函
数的外部已说明了函数类型,则在各个主调函数中不
必对所调用的函数再作类型说明。
例如,
float f(floatx,floaty); /*外部声明 */
main( )
{...} / *不必说明函数 f的类型 */
float f( float x,float y)/ *f函数定义 */
{··}
例 8.5
8.5 函数的嵌套调用
C语言的函数定义都是互相平行、独立的,在定义
函数时,一个函数内不能包含另一个函数(在定义一个
函数时,其函数体内又包含另一个函数的完整定义,这
就是嵌套定义,这个内嵌的函数只能被包含它的函数所
调用)
C语言允许嵌套调用函数,
Main函数 a函数 b函数
调用 a函数 调用 b函数
结束
F(x2)
x2
[例 8.6] 用弦截法求方程的根。
x^3-5*x^2+16*x-80= 0
方法如下,
( 1)取两个不同点调 x1,x2,如果 f(x1)和 f(x2)符
号相反,则 (x1,x2)区间内必有一个根。如果 f(x1)与
f(x2)同符号,则应改变 x1,x2,直到 f(x1),f(x2)异号
为止,注意 x1,x2的值不应差
太大,以保证 (x1,x2)区间内只有一个根。
( 2)连接 f( x1)和 f( x2)两点,此线(即弦)交
x轴于 x,见图 7·6。
点 x坐标可用下式求出,
x1*f(x2)-x2*f(x1)
x=
f(x2)-f(x1)
再从 x求出 f( x)。
图 8.6
x1 x
f(x1)
f(x)
(3)若 f(x)f(x1)同符号,则根必在( x,x2)区间内,此时将 x作
为新的 x1。如果 f(x)与 f( x2)同符号,则表示根在 (x1,x)_区间
内,将 x作为新的 x2。
( 4) 重复步骤 ( 2) 和 ( 3), 直到 |f( x) |为一个很小的数, 例
如 10-6。 此时认为 f( x) ~~ 0。
根据上述思路画出 N-s流程图, 见图 8,7。
输入 x1,x2,求 f(x1),f(x2)
直到 f(x1) f(x2) 异号
求 f(x1)与 f(x2)连线与
x轴交点
y=f(x) y1=f(x1)
y与 y1同号
t f
x1=x y1=y x2=x y2=y
直到 |y|< 0.000001
root=x 输出 root
图 8.7
分别用几个函数来实现各部分功能,
( 1)用函数 f( x)来求 x的函数,x^3一 5x^2+ 16x
一 80。
( 2)用函数 xpoint( x1,x2)来求 f( xl)和 f( x2)
的连线与 x轴的交点 x的坐标。
( 3)用函数 root(x1,x2)来求( x1,x2)区间的那
个实根。显然,执行 root函数过程中要用到函数
xpoint,而执行 xpoint函数过程中要用到 f函数。
# include,math,h,
float f(float x)
/ *定义,f(x)=x^3一 5x^2 +16x一 80*/
{float y;
y=((x-0.5)*x+16.0)*x-80.0;
return (y); }
float xpoint( float x1,float x2)
/ *定义 xpoint函数,求出弦与 x轴交点。 */
{float y;
y=( xl*f(x2)-x2*f(x1))/( f( x2)一 f( xl));
return( y);
}
float root(float x1,float x2)
/ *定义 root函数,求近似根 */
{ float x,y,yl;
y1= f(x1);
do
{ x=xpoint(x1,x2);
y= f(x);
if( y*yl> 0) / *f( x)与 f(x1)同符号。 */
{yl=y;
x1=x;}
else
x2=x;
} whi1e ( fabs(y)>=0.0001);
return(x); }
main ( ) / *主函数 */
{float x1,x2,f1,f2,x;
do
{
printf( "input x1, x2,\n”) ;
scanf( "%f,%f",&x1,&x2) ;
fl=f(x1); f2=f(x2);}
while ( fl*f2) >=0);
x= root( x1,x2);
printf( "A root of equation is% 8.4f",x);
}
运行情况如下,
input x1,X2,
2,6
A root of equation is 5.0000
从程序可以看到,
( 1) 在定义函数时, 函数名为 f,xpoint,root的
三个函数是互相独立的, 并不互相从属 。
( 2) 3个函数的定义均出现在 main函数之前, 因此在
main函数中不必对这三个函数作类型说明 。
( 3)程序从 main函数开始执行。
先执行一个 do一 while循环,作用是:输入 x1和 x2,
判别 f(x1)和 f(x2)是否异号,如果不是异号则重新输
入 x1和 x2,直到满足 f( x1)与 f( x2)异号为止。然
后用函数调用 root( x1,x2)求根 x。调用 root函数过
程中,要调用 xpoint函数来求 f(x1)与 f(x2)连线的交
点 x。在调用 xpoint函数过程中要用到函数 f来求 x1和
x2的相应的函数值 f( xl)和 f( x2)。这就是函数的
嵌套调用。见图 8·8。
main函数 root函数 xpoint函数 f函数
调用 root函数 调用 xpoint函数 调用 f函数
输出根 x
结束
图 8.8
8.6 函数的递归调用
在调用一个函数的过程中又出现直接或间
接地调用该函数本身,称为 函数的递归调用 。
C语言允许函数的递归调用。例如,
int f( int x)
{ int y,z;
z=f(y);
return (2*z); }
递归调用不应出现无终止的递归调用,而只应
出现有限次数的、有终止的递归调用
例 8.7
age( 5) =age( 4)+ 2
age( 4) =age( 3)+ 2
age( 3) =age (2) +2
age( 2)= age(1)+ 2
age( 1)= 10
可以用式子表述如下,
10 ( n=1)
age( n) =
age( n-1)+2 ( n> 1)
可以看到,当 n>= 1时,求第 n个人的年龄的公式是相
同的。因此可以用一个函数来表示上述关系,图 8.11
表示求第 5个人年龄的过程。
age(5) age(5)
=age(4)+2 =18
age(4) age(4)
=age(3)+2 =16
age(3) age(3)
=age(2)+2 =14
age(2) age(2)
=age(1)+2 = 12
age(1)
=10
从图可知,求解可分成两个阶段:第一阶段是, 回推,,
即将第 n个人的年龄表示为第( n-1)个人年龄的函数,
此时 age(1)已知,回推结束。
图 8.11
第二阶段,采用递推方法,从第 1个人的已知年龄推算
出第 2个人的年龄( 12岁),从第 2个人的年龄推算出 3
个人的年龄( 14岁) ……,一直推算出第 5个人的年龄
( 18岁)为止。
一个递归的过程可分为“回推”和“递推”两
个阶段。
一个递归的过程必须具有一个结束递归过程的
条件。
例如,age( 1)= 10,是使递归结束的条件。
可以用一个函数来描述上述递归过程,
int age( int n) /*求年龄的递归函数 */
{ int c;/ * c用作存放函数的返回值的变 */
if( n==1) c=10;
else c=age( n一 1) 十 2;
return( c); }
main( )
{printf( "%d",age( 5)) ; }
运行结果如下,18
main函数中只有一个语句 。
函数调用过程如图 8.12所示 。
Main age函数 age函数 age函数 age函数 age函数
n= 5 n=4 n=3 n= 2 n=1
age(5)=18 age(4)=16 age(3)=14 age(2)=12 age(1)=10
age(5)
并输出 C=age(4)+2 C=age(3)+2 C=age(2)+2 C=age(1)+2 C=10
图 8.12
[例 8,8]用递归方法求 n!
求 n!也可以用递归方法, 即 5! 等于 4! *5,
而 4! =3! *4……,1! =1。
可用下面的递归公式表示,
1 ( n=0,1)
n!= (n-1)!*n (n> 1)
1 ( n=0,1)
n!= (n-1)!*n (n> 1)
程序,float fac( int n)
{ float f;
if( n< 0) printf(, n< 0,data
error\n”);
else if( n==0||n==1) f=1;
else f= fac( n——1) *n;
return( f); }
main()
{ int n;
float y;
printf(, input a integer
number,, );
scanf(, %d”,&n);
y= fac( n);
printf(, %d!=%15.0f”,n,y);}
[例 8,9] Hanoi 汉诺 塔问题。
p163
8.7 数组作为函数参数
变量、数组元素可以做函数实参,其用法相
同。 数组名也可以做实参和形参,传递的是整
个数组。
一,数组元素 做函数实参
数组元素作为函数的实参,与用变量做
实参一样,是单向传递,即, 值传送, 方式。
[例 8.10] p178
二, 可以用数组名作函数参数, 此时实参与形参
都应用数组名 ( 或用数组指针, 见第 9章 。 )
[例 8.11] p179
f1oat average( float array[])
{ int i;
f1oat aver,sum=array[0];
for( i=1; j< 10; i++) sum=sum+ array[0];
aver=sum/ 10;
return(aver);}
main()
{f1oat score[10];
int i;
printf( "input l0 scores,\ n”);
for( i=0; i<=10; i++) scanf ( "%f",&score[i]);
printf( "\n");
aver = average( score);
printf( "average score is % 5·2f”,aver); }
说明,
1。用数组名作函数参数,实参与形参类型必须
一致;
2。 形参数组可以不指定大小;
3。数组名作参数时,不是“值传送”,不是单
向传递,而是把实参数组的起始地址传递给形
参数组,这样 两个数组共占同一段内存。
( 此时形参数组的变化事实上将会引起实参数组的变
化。)
求平均值,
/* example 8.12 on page 180 */
float average(float array[],int n)
{ int i;
float aver,sum=array[0];
for(i=1;i<n;i++) sum=sum+array[i];
aver=sum/n;
return(aver);
}
main()
{
float s1[5]={98.5,97,91.5,60,55};
Float s2[10]={67,89,99,69,77,89.5,76,54,60,99};
printf("the average of class A is
%5.2f\n",average(scor1,5));
printf("the average of class B is
%5.2f\n",average(scor2,10));
}
[例 8.13] p181
用选择法对数组中 10个整数按由小到大排序。所谓选
择法就是:先将 10个数中最小的数与 a[0]对换;再将
a[1]到 a[9]中最小的数与 a[1]对换; ……,每比较一轮,
找出一个未经排序的数中最小的一个。共应比较 9轮。
程序如下,
void sort( int array[],int n)
{ int i,j,k,t;
for( i= 0; i<=n-1;i++)
{ k=i;
for( j=i+1;j<=n;j++)
if (array[j]< array[k] ) k= j;
if (k!=i){t=array[k];array[k]=array[i];array[i]=t;} }
main()
{ int a[10],i;
printf(, enter the array\n”) ;
for( I=0; i< 10; i十十 ) scanf(, %d”,&a[i]) ;
sort( a,10);
printf(, the sorted array,\n”);
for( i= 0;i< 10;j++) printf(, % d”,a[i]);
printf(, \n”); }
三,用多维数组作函数参数
多维数组元素可以作为实参 ;
多维数组名作为实参和形参;
对形参数组定义时可以指定每一维的大小,也可省
略第一维的大小说明。
int array[ 3] [10];
或 int array[] [10]; 二者都合法而且等价。
不能把第二维以及其它高维的大小说明省略。
int array[ 3] []; --不合法
int array[ ][ ] ; --不合法
原因,从实参传送来的是数组起始地址,在内存中按
数组排列规则存放(按行存放),而并不区分行和列,
如果在形参中不说明列数,则系统无法决定应为多少
行多少列。
实参数组的长度可以与形参数组不同
例如,实参数组定义为
int score[ 5] [10];
而形参数组定义为
int array[ 3] [10];
这时形参数组只取实参数组的一部分,其余部分不起作
用。
[例 8.14] 有一个 3X4的矩阵,求其中的最大元素。
程序如下,
max_value( int array[][4])
{ int i,j,k,max;
max= array[0][0];
for (i= 0; i< 3; i十十 )
for( j= 0; j< 4; j十十 )
if( array[ I] [j]> max) max=array[i][j];
return ( max) ; }
main()
{ int a[3][4]= {{1,9,7,3},{2,4,6,8},{15,17,
34,12}};
printf(, max value is % d \n”,max_value(a)) ;}
8.8 局部变量和全局变量
8.8.1 局部变量
定义:在一个函数内部定义的变量是内部变量,它只
在本函数范围内有效,在此函数以外是不能使用这些
变量的。
? 包括主函数在内的各函数内部定义的变量都只在各函
数内有效,且不能相互调用;
? 不同函数中可以使用相同名字的变量;
? 形参是局部变量;
? 可在复合语句中定义变量;
float fl( int a) / *函数 f1*/
{int b,c; a,b,c有效
……
}
char f2( int x,int y)
{int i,j;
…… x,y,i,j有效
}
main()
{int m,n;
…… m,n有效
}
8.8.2 全局变量
在函数之外定义的变量称为 外部变量, 外部
变量是全局变量 。
全局变量可以为本文件中其它函数所共用 。
它的 有效范围 为:从定义变量的位置开始到
本源文件结束 。
int p=1,q=5; / *外部变量 */
float f1( int a); / *定义函数 fl*/
{ int b,c; 全局变量
………} p,q的作用
char c1,c2; / *外部变量 */ 范围
char f2(int x,int y); /*定义函数 f2*/
{int i,j;
· ……} 全局变量 c1 c2的
main ( ) / *主函数 */ 作用范围
{ int m,n;
……… }
全局变量的作用说明,
1,增加了函数间数据联系的渠道,同时可以从函数
得到一个以上的返回值。
2.全局变量以减少函数实参与形参的个数,从而减少
内存空间以及传递数据时的时间消耗。
P185 例 8.15
思考:使用全局变量的缺点
3.如果在同一个源文件中,外部变量与局部变量同名,
则在局部变量的作用范围内,外部变量不起作用。如,
[例 8.16]
int a=3,b=5; / *a,b为外部变量 */ a,b作用范围
Max(a,b);
int a,b; / *a,b为局部变量 */
{int c; 形参 a,b
c=a>b?a:b; 作用范围
return ( c) ;
}
main() 局部变量 a作用范围
{ int a=8;/ *a为局部变量 */
printf(, %d”,max( a,b));
} 全局变量 b的作用范围
8.9 变量的存储类别
8.9.1 动态存储方式与静态存储方式
变量从作用域 ( 空间 ) 角度可分为 全局变量和局部变量 。
变量从变量值存在的时间(生存期)角度可分为 静态存
储变量和动态存储变量。
静态存储方式是指在程序运行期间分配固定的存储空间
的方式。
动态存储方式则是在程序运行期间根据需要进行动态的
分配存储空间的方式。
内存中的供用户使用的存储空间的情况。这个存
储空间可以分为三部分,见图 8.16。
1.程序区
2.静态存储区
3.动态存储区
数据分别存放在静态存储区和动态存储区中。
静态存储区中,全局变量
全局变量存放在静态存储区中,在程序开始执行时给全局
变量分配存储区,程序执行完毕就释放。在程序执行过程
中它们占据固定的存储单元,而不是动态地分配和释放的。
程序区
静态存储区
动态存储区
动态存储区 中存放以下数据,
①函数形参变量。在调用函数时给形参变量分配
存储空间。
②局部变量(未加 static说明的局部变量,即自
动变量)。
③函数调用时的现场保护和返回地址等。
动态的含义:动态分配和释放存储空间
如果在一个程序中两次调用同一函数,分配给此函
数中局部变量的存储空间地址可能是不相同的。
在 C语言中每一个变量和函数有两个属性,
? 数据类型;
? 数据的存储类别 。
存储类别指的是数据在内存中存储的方法。
存储方法分为两大类:静态存储类和动态存储类。
具体包含四种,
? 自动的( auto),
? 静态的( static),
? 寄存器的( register),
? 外部的( extern)。
8.9.2 auto变量
自动变量用关键字 auto作存储类型的说明。
函数中的局部变量,如不做专门的说明,都是动态分
配存储空间的,存储在动态存储区中,这类局部变量称
为自动变量。
, auto”也可以省略,auto不写则隐含确定为“自动存
储类别”,它属于动态存储类别。
auto int b,c=3;
int b,c=3; 二者等价
8.9.3 static变量
static 类型 变量名
局部静态变量,函数中的局部变量的值在函数调用结束
后不消失而保留原值 (其占用的存储单元不释放 ),在
下一次该函数调用时,该变量已有值,就是上一次函
数调用结束时的值。
[例 8,17]
对局部静态变量的说明,
1.局部静态变量属于静态存储类别,在静态存储区内
分配存储单元。在程序整个运行期间都不释放。而自
动变量(即局部动态变量)属于动态存储类别,占动
态存储区空间而不占固定空间,函数调用结束后即释
放。
2.对局部静态变量是在编译时赋初值的,即只赋初
值一次,在程序运行时它已有初值。以后每次调用函
数时不再重新赋初值而只是保留上次函数调用结束时
的值。而对自动变量赋初值,不是在编译时进行的,
而在函数调用时进行,每调用一次函数重新给一次初
值,相当于执行一次赋值语句。
3,如在定义局部变量不赋初值的话,则对静态变量来说,
编译时自动赋初值 0(对数值型变量)或空字符(对字符
变量) 。
(对自动变量来说,如果不赋初值则它的值是一个不确定的值。
这是由于每次函数函数调用结束后存储单元已释放,下次调用时又
重新另分配存储单元,而所分配的单元中的值是不确定的。 )
4.虽然局部静态变量在函数调用结束后仍然存在,但其
它函数是不能引用它的。
局部静态变量的使用示例,
1.需要保留函数上一次调用结束时的值。 例如,
[例 8.18] 打印 1——5的阶乘。
int fac( int n)
{ static int f= 1,
f=f*n;
return( f); }
main()
{int i;
for( i= 1; I<=5;i++)
printf(, % d! =% d”,i,fac( i));
}
2.如果初始化后,变量只被引用而不改变其值,则这时用局部静态
变量比较方便,以免每次调用时重新赋值。
例如,例 8.19是输出 1到 5的阶乘值 。
int fac( int n)
{ register int i,f=1; / *定义寄存器变量 */
for ( i= l; i<n; i++ )
f=f*i; return( f) ;
main( )
{ int i;
for ( i= 1; 1<= 5; i十十 )
printf(, % d! = % d\n”,fac( i)) ; }
定义局部变量 f和 i是寄存器变量, 如果 n的值大, 则
能节约许多执行时间 。
8.9.4 Register变量
说明,
1.只有局部自动变量和形式参数可以作为寄存器变量,
其它(如全局变量)不行。在调用一个函数时占用一些寄
存器以存放寄存器变量的值,函数调用结束释放寄存器。
2.一个计算机系统中的寄存器数目是有限的,不能定义
任意多个寄存器变量。
3.局部静态变量不能定义为寄存器变量。不能写成
register static int a,b,c;
不能把变量 a,b,c既放在静态存储区中,又放在寄存器
中,二者只能居其一。
8.9.5 用 extern声明外部变量
1.在一个文件内声明外部变量
外部变量在文件开头定义,在整个文件范围内都有效;否则其 作用范围只限于定义点到文件终了。
int max( int x,int y)/ *定义 max函数 */
{int z;
z= X> y? x,y;
return(z) ;}
main()
{ extern int a,b; / *外部变量说明 */
printf(, %d”,max(a,b)) ;}
int a=13;b=-8; / *外部变量定义 */
2.在多文件的程序中声明外部变量
在不同的文件中都要用到同一个外部变量 a时,
在一个文件中定义该变量;
int a;
在其他的文件中做外部变量声明,
extern a;
8.9.6 用 static 声明外部变量
Static int a;
表示外部变量 a此时仅限于本程序使用,而不能被其
他文件使用。
比较以下两种外部变量定义的区别和共同点,
Int a;
Static int a;
8.9.7 变量的声明与定义
定义性声明
引用性声明
8.9.8 存储类别小结
对一个数据的定义, 需要指定两种属性,
数据类型和存储类别,
分别用两个关键字进行定义 。 如,
static int a; ( 静态内部变量或静态外部变量 )
auto char c; ( 自动变量, 在函数内定义 )
register int d; ( 寄存器变量, 在函数内定义 )
extern int b;( 说明 b是一个已被定义的外部变量 )
1.从 作用域角度 分,有局部变量和全局变量。它们
采取的存储类别如下,
自动变量,即动态局部变量(离开函数,值
就消失)
局部变量 静态局部变量(离开函数,值仍保留)
寄存器变量(离开函数,值就消失)
形式参数可以定义为自动变量或寄存器变量)
静态外部变量(只限本文件用)
全局变量
外部变量(允许其他文件用)
2.从 变量存在的时间 来区分,有动态存储和静态存
储两种类型。静态存储是程序整个运行时间都存在,
而动态存储则是在调用函数时临时分配单元。
(自动变量(本函数内有效)
动态存储) 寄存器变量(本函数内有效)
(形式参数
(静态局部变量(函数内有效)
静态存储,静态外部变量(本文件内有效)
外部变量(其它文件可引用)
3.从 变量值存放的位置 来区分,可分为,
(静态局部变量)
内存中静,静态外部变量(函数外部静态变量)
态存储区 外部变量(可为其它文件引用)
内存中动态存储区:自动变量和形式参数
CPU中的寄存器:寄存器变量
4.关于 作用域和生存期 的概念。从前面叙述可以知道,
对一个变量的性质可以从两个方面分析,一是从变量的作
用域,一是从变量值存在时间的长短,即生存期。前者是
从空间的角度,后者是从时间的角度。二者有联系但不是
同一回事。图 8.19是作用域的示意图。图 8.20是生存期的
示意图。如果一个变量在某个文件或函数范围内是有效的,
则称该文件或函数为该变量的作用域,在此作用域内可以
引用该变量,所以又称变量在此作用域内, 可见,,这种
性质又称为变量的, 可见性,,例如变量 a,b在函数 fl中
,可见, 。如果一个变量值 i在某一时刻是存在的,则认为
这一时刻属于该变量的, 生存期,,或称该变量在此时刻
,存在, 。表 7.2示各种类型变量的作用域和存在性的情况。
文件 file1,C
定义外部变量 a
main函数
f2;
f1;}
f1
{auto int b ; a的作用域
……,F2;……;} b的作用域
f2函数
( static int c;
……….;}
f1函数所用函数是否会与其它文件中函数同名,通常把只由
同一文件使用的函数和外部变量放在一个文件中,冠以
static使之局部化,其它文件不能引用。
8.10 内部函数和外部函数
8.10.1 内部函数
8.10,2 外部函数
在定义函数时, 如果冠以关键字 extern,表示此函数是
外部函数 。 如
extern int fun( a,b)
函数 fun可以为其它文件调用, 如果在定义函数时省略
extern,则隐含为外部函数 。 本书前面所用的函数都作为
外部函数 。
在需用调用此函数的文件中, 一般要用 extern说明所用
的函数是外部函数 。
[例 8,22]有一个字符串, 内有若干个字符, 今输入一个
字符, 程序将字符串中该字符删去 。 用外部函数实现 。
file1,C ( 文件 l)
main( )
extern enter_string(),delete_string(),
print_string();/ *说明本文件要用到其它文件中的函
数 */
char c;
static char str[80];
enter_string(str);
scanf(, %c”,&c);
delete_string(str,c);
print_string(str); }
file2,c(文件 2)
# indude,stdio.h”
extern enter_string( str)/ *定义外部函数
enter_string*/
char str[80];
{ gets ( str);} / *读入字符串 str*/
file3,c(文件 3)
extern delete_string( str,ch) ;/ *定义外部函数
delete_string*/
char str[],ch;
{ int i,j;
for ( i= j= 0; sir[I]! =’\0’; i++)
if( str[i]! =ch) str[j++]=str[i];
str[j]=’\0’;}
file4,c(文件 4)
extern print_string (str) / *定义外部函数
print_string*/
char str[];
{ printf(, %s”,str) ;
运行情况如下,
abcdefgc (输入 str)
c (输入要删去的字符
abdefg (输出已删去指走字符的字符串)
整个程序由四个文件组成,每个文件包含一个函数。主函
数是主控函数,由 4个函数调用语句组成。其中删 nf是库函
数。另外 3个是用户自己定义的函数,它们都定义为外部函
数。当然,定义时 extern不写也可以,系统隐含它们为外部
函数。在 main函数中用 extern说明在 main函数中用到的
enter一 string,delete一 string,print一 string是外部
函数。在有的系统中,也可以不在调用函数中对被调用的
函数作, 外部说明, 。
函数 de1ete_string的作用是根据给定的字符串 str和要
删除的字符, 对 str作删除处理 。 算法是这样的:对 str数
组的字符逐个检查, 如果不是被删除的字符就将它存放在
数组中, 见图 7,21( 设删除空格 ) 。
T h i s i s a c p r o g r a m \0
T h i s i s a c p r o g r a m \0
图 7.21
从 str[0]开始逐个检查数组元素值是否等于指定要删除
的字符,是就留在数组中,若是就不保留。从图中可以
看到,应该使 str[0]赋给 str[0],str[1]赋给 str[1],
str[2] 赋给 str[2],str[ 3]赋给 str[ 3],然后
str[5] 赋给。 str[5]……·请读者注意分析如何控制 i
和 j的变化,以便使被删除的字符不保留在原数组中。
这个题目当然可以设两个数组,把不删除的字符一一赋
给新数组,但我们只用一个数组,只把不被删除的字符
保留下来。由于 i总是大于或等于入因此最后保留下来
的字符不会覆盖未被检测处理的字符,最后将结束符
‘ \0’也复制到被保留的字符后面。
在用一般的方法进行编译连接时,先分别对 4个文件
进行编译,得到 4个。 oBJ文件。然后用 link把 4个目标
文件(,oBJ文件)连接起来。在 MS C上用以下命令,
link filel + file2+ file3十 file4/
得到一个可执行的文件。当然也可以用
#tinc1ude命令将 file2,C,file3,c和 file4,c包含
到 file1中,在 file1,c中的开头加 3行,
# include,file2,c”
# inciude,file3,C”
# include,11e4,c”
这时,在编译时,系统自动将这 3个文件放到 main函数
的前头,作为一个整体编译,而不是分 4个文件编译。这时,
这些函数被认为是在同一文件中,不再是作为外部函数被
其它文件调用了。 main函数中原有的 extern说明可以不要。