第 5 章 模块化程序设计
5.1 概述
5.2 函数定义的一般形式
5.3 函数参数和函数的值
5.4 函数的调用
5.5 函数的嵌套调用
5.6 函数的递归调用
5.7 局部变量和全局变量
5.8 动态存储变量与静态存储变量
5.9 内部函数和外部函数
5.10 编译预处理
5.11,文件包含” 处理
5.12 条件编译
第 5 章 模块化程序设计
在进行程序设计时,程序员总是将复杂的问题进行
分解,化整为零。编写一段小程序就能解决一个小问题,
然后将这些小程序拼装起来就能解决非常大而复杂的问
题。在 C 中,这些小程序统称为函数。
在 C 中,由系统提供的函数放在函数库中供用户选
用,以减少重复编写程序模块的工作量。
?所有函数都是平行的,相互独立不能嵌套,但可以相
互调用。
? C 函数分标准函数 (库函数 )和自定义函数。
? C 函数本身分有参函数和无参函数。
5.1 概述
5.2 函数定义的一般形式
1.无参函数的定义形式
类型标识符 函数名 ( )
{ 说明部分 ;
语句;
}
无参函数一般不需要返回函数值,可以省略类型标识符。
2.有参函数的定义形式
类型标识符 函数名 (形式参数表 )
形式参数说明;
{ 说明部分 ;
语句;
}
例,int max(x,y)
int x,y; /? 形式参数说明 ?/
{ int z; /? 函数体中的说明部分 ?/
z=x>y? x, y;
return(z);
}
类型说明符 函数名 ( )
{ }
“空函数”什么操作也不做。其作用是在此处留一函数的
位置,以便将来扩充功能之用。函数名也在将来换取实
际的函数名。
这两行可以写成一行:
int max(int x,int y)
3.,空函数,
5.3 函数参数和函数的值
1.形式参数和实际参数
例 5.1 从键盘输入两个数, 输出其中较大的一个 。
main( )
{ int a,b,c ;
scanf(“%d,%d”,&a,&b);
c=max(a,b); /? a,b为实际参数 ?/
printf(“Max is %d”,c);
}
max(x,y) /? x,y为形式参数 ?/
int x,y;
{ int z;
z=x>y? x, y;
return(z);
}
注意,
?形参变量在被调用前不占用存储单元;在被调用结束
后,形参所占存储单元亦被释放。
?实参可以是常量、变量或表达式。
?必须指定形参类型,且必须与实参的类型一致。
?实参对形参的数据传递是“值传递”。即单向传递,不
能逆传。
?可以在形参表中直接说明形参类型。 如:
int max(int x,int y);
float fun(int a[10],int n);
2.函数的返回值
?函数的返回值是通过 return语句获得的。当不需返
回函数值时,可省去 return语句。
? return语句的后面可以有括号,也可以没有。 如,
return z; ? return(z);
? return语句的后面可以是变量,也可以是表达式。 如,
return(x >y? x, y);
? return语句返回值的类型应与该函数的类型一致。
否则以函数类型为准。
?若函数中没有 return语句,则该函数被调用后也会带
回 不确定的值 。
{ int a,b,c ;
a = printstar( );
b = print_message( );
c = printstar( );
printf(“%d,%d,%d\n”,a,b,c);
}
输出的 a,b,c的值将是各个被处理的字符串的长度。
?为了明确表示不需要函数返回值,可以用,void”定义
函数为, 无类型,。此时,不得使用 a=printstar( ) 之
类的语句。
凡不需要返回值的函数,一般均定义为,void”类型。
如,
均为处理字符串的函数
且均没有 return 语句。
5.4 函数的调用
1.函数调用的一般形式
函数名 (实参表 )
说明,
?对于无参函数,尽管没有“实参表”,但也不得省略
括号。
?,实参表”中的参数之间用 逗号 分开。
?实参与形参之间的个数及类型必须一一对应。
?对实参求值的顺序是自左至右还是自右至左,视具体
的系统而定。 Turbo C 和 MS C 是按 自右至左 的顺
序求值。
例 5.2
main( )
{ int i =2,p ;
p = f( i,++i );
printf (,%d”,p ) ;
}
int f(a,b ) ;
int a,b ;
{ int c ;
if ( a > b ) c = 1;
else if ( a == b ) c = 0 ;
else c = –1 ;
return( c ) ;
}
输出结果,
0
注意, 这里是按 自右至左 求
值的,相当于 fun(3,3)。若
按 自左至右 求值,则相当于
fun(2,3),则输出为 –1。
先执行 ++i 使 i 的值为 3
再将 i 的值 3 传递给 b
b的值为 3
然后将 i 的值 3 传递给 a
a的值为 3,
2.函数调用的方式
调用函数,可以有如下三种方式:
? 将函数调用作为一个语句。 如,
printf(“MS C pragramming”); gets(s);
? 将被调用的函数写在表达式中。 如,
c = 2 ? max(a,b);
注意,被调用函数 max 必须有确定的返回值。
? 将函数调用作为一个函数的实参。 如,
m = max(max(a,b),c);
3.对被调用函数的说明
在一个函数中调用另一个函数时:
? 如果被调函数为库函数,则应在文件开头用,#include”命令
声明相应的,头文件”。 如,
#include“stdio.h”
#include“math.h”
?
? 如果被调函数为自定义函数且其定义在主调函数定义之后,则
应在主调函数中说明其类型。
类型标识符 被调函数名 ( );
如, float add( );
其语句形式为:
? 如果被调函数为自定义函数且其定义在主调函数定义之前,则
在主调函数中可不必说明其类型。因为编译程序已知道其类型
? 如果被调函数的值是整型或字符型,可不必说明类型,系统自
动按整型说明。
? 如果在所有被调函数定义之前、在文件的开头、在函数的外部
已对被调函数作了类型说明,则在各主调函数中可不必说明其
类型。 如,
char letter( );
float f( );
int i( );
main( )
{?}
?
/? 主调函数中不必说明它所调用的函数的类型 ?/
一开始就将所有要被
调用的函数作出声明
例 5.3 编程求 11~999之间的数 m,而 m,m2 和 m3 均是回文数 。
如,m=11,m2=121,m3=1331,?。
#include“stdio.h”
main( )
{long int m;
for (m=11; m<1000; m++)
if ( fun(m) && fun(m?m) && fun(m?m?m) )
printf("m=%ld,\t m?m=%ld,\t m?m?m=%ld\n",
m,m?m,m?m?m);
}
int fun(long n)
{long i,m;
i= n; m=0;
while(i) { m = m?10+i%10; i /= 10; }
if (m==n) return 1;
else return 0;
}
局部变量 i 存放 n 的值即主
调函数中 m 传递过来的值,
这里的 m 是局部变量,不要
与主函数中的 m 混淆。
5.5 函数的嵌套调用
C 语言函数的定义都是相互平行、独立的,
不能嵌套定义。但可以嵌套调用函数。所谓函
数的嵌套调用就是在被调用的函数中又调用另
外的函数。
5.6 函数的递归调用
在调用一个函数的过程中又出现直接或间
接地调用该函数本身,称为函数的递归调用。
例 5.4 有 5人排成一队,从最后一人开始,其年龄均比
前面的人大 2岁,而最前面的人年龄是 10岁,问
最后一人的年龄是多少岁?
main( )
age(5) age(4)+2
age(n)
n=5
age(3)+2
age(n)
n=4
age(2)+2
age(n)
n=3
age(1)+2
age(n)
n=2
age(1)
age(n)
n=1
age(1)=10age(2)=12age(3)=14age(4)=16age(5)=18输出 age(5)
age(n)
int n;
{ int c;
if (n==1) c=10;
else c=age(n–1) + 2;
return(c);
}
main( )
{
printf(“%d\n”,age(5));
}
运行结果,
18
函数的递归调用利用了堆栈技术。在本例中:
Age(5)
Age(5–1)+2
Age(4–1)+2
Age(3–1)+2
Age(2–1)+2 10+2=12
12+2=14
14+2=16
16+2=18
18
Age(2–1)+2345Age(5)
Age(5)
Age(5–1)+2
Age(4–1)+2
Age(3–1)+2
Age(2–1)+2
入栈 出栈 出栈结果
5.7 局部变量和全局变量
1,局部变量
在一个函数内部定义的变量,只能在本函数内使用和有
效,称为“局部变量”。
? 主函数 main中定义的变量,也只能在主函数内使用和有效。
? 不同函数中可以使用相同名字的变量,且互不干扰。
? 形式参数也是局部变量,也只能在所在函数内使用和有效。
? 可以在一个函数内的复合语句中定义变量,且这些变量只
在本复合语句中有效。
这种复合语句也称为,分程序” 或,子模块”。
2.全局变量
在函数 (包括 main函数 )外定义的变量为外部变量,称为
“全局变量”。全局变量的有效范围为从定义变量的位置开始
到本源文件结束。
? 全局变量增加了函数间数据联系的渠道。由于同一文件中的所
有函数都能引用全局变量的值,当需要从一个函数中带回多个
值时,就能克服函数调用只能返回一个值的局限性。
? 如无必要,不要使用全局变量。因为全局变量既降低程序的清
晰性和函数的通用性,且又在程序的全部执行过程中都占用存
储空间。
? 在文件开头定义的外部变量才可在整个文件范围内使用,若在
定义点之前的函数需引用外部变量,则可用关键字,extern”作
“外部变量说明”。
注意, 外部变量定义和外部变量说明并不是同一回事。外部变量
的定义只能有一次,它的位置在所有函数之外。而同一程序中的外
部变量说明可以有多次,它的位置在函数之内 (哪个函数要用就在
哪个函数中说明 )。系统根据外部变量的定义 (而不是根据外部变量
的说明 )分配存储单元。对外部变量的初始化只能在“定义”时进行,
“extern”只是申明该变量是一个已在外部定义过的变量而已 。
? 如果在同一源文件中,外部变量与局部变量同名,则在局部变
量的作用范围内,外部变量不起作用。
例 5.5
#include,stdio.h”
main( )
{ int a=1,b=2,c=3;
a++; c+=b;
{ int b=4,c;
c=2?b; a+=c;
printf(“%d,%d,%d\n”,a,b,c );
}
printf(“%d,%d,%d\n”,a,b,c );
}
输出结果,
10,4,8
10,2,5
a 为全局变量,
b,c为局部变量
a,b,c均为全局变量
5.8 动态存储变量与静态存储变量
1.变量的存储类别
静态变量在程序的运行期间占用固定的存储空间,
直到程序的终止而释放;而动态变量是在程序的运行期
间随着函数的调用随时动态地占用和释放存储空间。即
存储方式是根据变量的存储类别决定的。
C的存储类别有四种,
自动的 (auto) 静态的 (static)、
寄存器的 (register) 外部的 (extern)。
2.局部变量的存储方式
1) 函数中的局部变量若未专门说明,都是由编译系统自
动动态分配存储空间,这类局部变量称为自动变量,
其类型说明前不论是否有关键字, auto”,都属于动态
存储类别。
2) 若希望被调函数在结束后,其局部变量占用的存储空间不释放,
以便保留其变量的值,用于下次调用该函数,则用,static”说明
为, 局部静态变量,。
?局部静态变量在静态存储区分配存储单元,在程序的整个运行
期间都不释放。而动态变量在动态存储区分配存储单元,函数
调用结束后即释放。
?局部静态变量在编译时只赋初值一次,以后每次被调用时不再
重新赋值而只保留前次被调用结束时的值。动态变量赋初值不
是在编译时而是在函数被调用时进行的,函数每次被调用时重
新赋值,相当于赋值语句。
?静态变量若未赋初值,则在编译时自动赋初值 0(数值型 )或 空字
符 (字符型 )。而动态变量若未赋初值,则其值是不确定的。
?只有将数组定义为全局变量或静态变量时才能赋初值。但应注
意,如果数组的值在函数被调用过程中改变了,则影响下一次
调用时的初值。
?虽然局部静态变量在函数被调用结束后其值仍然存在,但其它
函数是不能引用的。
例 5.6 打印 1 到 5 的阶乘值。
int fac(n)
int n;
{static int f=1;
f = f ?n ;
return( f );
}
main( )
{int i;
for (i=1; i<=5; i++)
printf(“%d!=%d\n”,i,fac(i));
}
f 被定义为静态变量,“f = 1;” 的初始在
fac(n) 第一次被调用时进行。随后无论
fac(n) 函数被调用多少次也不会再进行
初始化,且 f 被修改的值一直保留至本源
程序运行结束为止,在本函数外不可使用
f 的初值
1
1
2
6
24
调用次序
1
2
3
4
5
3.全局变量的存储方式
在文件开头用 extern作说明,可以引用另一个文件
中定义的全局变量。但用 static说明的全局变量不能被
其它文件引用。
5.9 内部函数和外部函数
就函数的本质而言都是全局的 。 但根据其是否能被
其它文件所调用, 将它们区分为内部函数和外部函数 。
1.内部函数
如果一个函数只能被本文件的其它函数所调用, 则将它
称为内部函数 。 可以用, static”将函数定义为内部函数 。
如, static int fun(a,b);
同静态变量一样, 内部函数只局限于本文件内使用,
因而不会和其它文件中的同名函数发生冲突 。
2.外部函数
在定义函数时, 如果在其类型说明前冠以 extern,
则表示此函数为外部函数 。 如,
extern int fun(a,b);
外部函数可以被其它文件所使用 。 如果省略 extern,
则隐含该函数为外部函数 。
课外练习:习题册中习题五全部
5.10 编译预处理
“编译预处理” 是 C 与其它高级语言的一个重要区别。
宏定义 ;
文件包含 ;
条件编译 ;
1,宏定义
1) 不带参数的宏定义
用指定的标识符 (宏名 )代表一个常量或字符串,
#define 标识符 常量 / 字符串
它们是,
例 5.7 求正圆锥体的底周长、底面积和它的体积。
#define PI 3.1415926
main( )
{ float l,s,r,v ;
scanf(“%f,,&r);
l = 2.0 ? PI ? r;
s = PI ? r ? r ;
v = 4.0 / 3 ? PI ? r ? r ? r ;
printf(“l=%10.4f \ns=%10.4f \nv=%10.4f \n”,l,s,v);
}
宏名 PI 用大写,行末无分号
?宏名的有效范围为该宏定义命令之后至本源文件结束。
通常,宏定义命令一般写在文件开头或函数之前作
为该文件的一部分。
?可以用 #undef 命令终止宏定义的作用域。 如:
#define G 9.8
main( )
{ }
?
?进行宏定义时,可以引用已定义的宏名,层层置换。
?对程序中用双引号括起来的字符串,即使与宏名相同
也不会被置换。
G 的作用范围
#undef G
float f( )
?
2.带参数的宏定义
语句形式,
#define 宏名 (形参表 ) 表达式
例 5.8
#define PI 3.1415926
#define S(r) PI?r?r
main( )
{float a,area;
a=3.6;
area=S(a);
printf(“r=%f \narea=%f \n”,a,area);
}
area=3.1415926?a?a,但不会置换 a为 3.6
?对带参数的宏定义的置换展开是用“表达式” 对等 的置
换“形参表”中的参数。上例中的,?”是不会被置换的。
?若将 area=S(a) 改写成 area=S(a+2),将置换为,
area=PI?a+2?a+2,而不是, area=PI?(a+2)?(a+2)。
若要达到后者之目的, 则应将宏定义命令改写为:
#define S(r) PI?(r)?(r)
?宏名与括号之间不得有空格, 因为宏名与表达式之间
的分隔符为空格 。 如果用 #define S (r) PI?r?r 的
话, 则被置换为 area=(r) PI?r ?r (a)
?宏展开并不进行值的传递, 即不求表达式的值, 也没
有, 返回值, 的概念 。
?宏不存在类型问题, 宏名无类型, 参数也无类型, 表
达式可以是任何类型 。
?定义带参数的宏,可以实现一些简单的函数功能。
如,#define MAX(x,y) (x)>(y)?(x), (y)
?
main( )
{int a,b,c,t ;
?
t = MAX(a+b,c+d);
?
}
注,这里的 t 展开后为 t = (a+b)>(c+d)?(a+b):(c+d)
如果第一行写成,#define MAX(x,y) x>y?x:y
则这里的 t 展开后为 t = a+b>c+d?a+b:c+d
因为 置换展开是用,表达式” 对等 的置换,形参表” 中的参数
5.11,文件包含” 处理
在 C中可以用 #include命令实现一个文件包含另一
个文件。 语句形式,
#include,文件名”
例 5.9
#include“stdio.h”
#define N 2
#define M N+1
#define NUM (M+1)?M/2
main( )
{ int i,n=1;
for (i=1; i<=NUM; i++) { n++; printf(“%2d”,n); }
printf(“\n”);
}
输出结果,
1 2 3 4 5 6 7 8
NUM 被代换展开后为,(2+1+1) ?2+1/2
?一个 include命令只能指定一个被包含文件,如果要
包含 n 个文件,则要用 n 个 include 命令。
?如果“文件 1”包含“文件 2”,而“文件 2”又包含“文件 3”
则可在,文件 1” 中使用两个 include 命 令。 即,
文件 file1.c
#include“file3.h”
#include“file2.h”
file3.h 应出现在 file2.h 之前
?文件包含可以嵌套。即一个被包含文件中又可以包含
另一个文件。 如,
文件 file1.c
#include“file2.h”
?
文件 file2.c
#include“file3.h”
?
? 在 #include 命令中,文件名既可用双引,, 号也可用
尖括号 < >括起来。但用双引号更方便系统查找被包含的
文件。
5.12 条件编译
1.条件编译的语句形式
其作用是:如果“标识符”已定义,则编译“程序段 1”,
否则编译“程序段 2” 。
1) #ifdef 标识符
程序段 1
#else
程序段 2
#endif
?,标识符”一般为 #define命令所定义。
?其中的“程序段 1”或“程序段 2”可以是命令,也可以是
语句或语句组。
? #else部分可有可无。
?所谓“标识符已定义”是无论“标识符”定义为什么内容。
?用条件编译的作用是缩短编译时间,减少目标程序的
长度。
例,#ifdef IBM_PC
#define INTEGER_SIZE 16
#else
#define INTEGER_SIZE 32
#endif
其作用是:如果“标识符”未定义,则编译“程序段 1”,
否则编译“程序段 2”。
其作用是:当“表达式”值为非 0,则编译“程序段 1”,
否则编译“程序段 2”。
2) #ifndef 标识符
程序段 1
#else
程序段 2
#endif
3) #if 表达式
程序段 1
#else
程序段 2
#endif
例 5.10
#define LETTER 1
main( )
{char str[20] =,Clabguage”,c ;
int i ;
i = 0;
while ( ( c = str[i] != ?\0?)
{ i + + ;
# if LETTER
if ( c >= ?a?&& c < = ?z?)
c = c – 32 ;
#else
if ( c >= ?A? && c< = ?Z?)
c = c + 32 ;
#endif
printf (,%c”,c) ;
}
}
运行结果:
C LANGUAGE
课外练习, 习题册中 习题六 全部例 5.12
#define N 2
#define M N+1
#define NUM 2?M+1
main( )
{int i;
for (i=1; i<=NUM; i++) printf(“%d\n”,i );
}
例 5.11 #define MAX(x,y) (x)>(y)? (x), (y)
main( )
{ int a=5,b=2,c=3,d=3,t;
t = MAX(a+b,c+d)?10;
printf(“%d\n”,t);
} t 的值是多少?
程序中的 for 循环执行的次数是多少?