第 5章 模块设计
5.1 模块的实现 —— 函数
5.2 模块间的参数传递
5.3 模块的递归调用
5.4 程序举例
5.5 编译预处理
5.1 模块的实现 —— 函数
5.1.1 函数的概念
在 C语言中, 函数分为以下两种 。
( 1) 标准库函数
这种函数用户不必定义, 但可直接使用 。 例如 scanf(),
printf(), fabs(), sqrt(), exp(),sin(),cos()等都是 C语言
中常用的库函数 。
( 2) 用户自己定义的函数
这种函数用以解决用户的专门问题, 一般由用户自己编
写 。
例 5.1 从键盘输入两个正整数 m与 n(m≥n),求 的值 ( 即
求 ) 。
其 C程序如下:
#include "stdio.h"
main() /*主函数 */
{ int m,n;
int p(); /*说明本函数中要调用的函数 p()是整型 */
scanf("%d,%d",&m,&n);
if (m>= n) printf("%d\n",p(m)/p(m- n));
else printf("m< n ! \n");
}
int p(k) /*计算阶乘值的函数 */
int k;
{ int s,i;
s= 1;
for (i= 1; i<= k; i= i+ 1) s= s*i;
return(s);
}
)!(
!
nm
m
?
nmA
下面对函数作几点说明:
( 1)一个完整的 C程序可以由若干个函数组成,其中必
须有一个且只能有一个主函数 main()。
( 2)一个完整 C程序中的所有函数可以放在一个文件
中,也可以放在多个文件中。
① 在编译命令行中键入各个函数所在的文件名(各文
件名之间用空格分隔)。
② 在主函数中用 #include 语句将各函数所在的文件包
含进来。
( 3) C语言中的函数没有从属关系,各函数之间互相独
立,可以互相调用。
5.1.2 函数的定义
在 C语言中, 函数定义的一般形式为
类型标识符 函数名 ( 形参表列 )
形参类型说明
{ 说明部分
语句部分
}
在定义 C函数时要注意以下几点。
( 1)函数类型标识符同变量类型说明符,它表示返回
的函数值类型。
( 2)如果省略函数的类型标识符,则默认为是 int型。
( 3) C语言允许定义空函数。如
dummy(){ }
( 4)函数中返回语句的形式为
return(表达式);

return 表达式;
( 5)如果“形参表列”中有多个形式参数,则它们
之间要用“,”分隔。
( 6) C语言允许在形参表中直接对形参的类型进行
说明。
5.1.3 函数的调用
函数调用的一般形式为
函数名(实参表列)
( 1) 函数调用可以出现在表达式中 ( 有函数值返回 ) ;也
可以单独作为一个语句 (无函数值返回 )。
( 2)实参表中的各实参可以是表达式,但它们的类型和个
数应与函数中的形参一一对应。
( 3)在调用函数中,通常要对被调用函数的返回值类型进
行说明(一般在调用函数的函数体中的说明部分),包括函
数类型、函数名和一对圆括号。
例 5.2 下列程序的功能是计算输出一个圆台两底面积之和 。
#include "stdio.h"
main()
{ double r1,r2;
double q();
printf("input r1, r2,");
scanf("%lf,%lf",&r1,&r2);
printf("s= %f\n",q(r1,r2));
}
double q(x,y)
double x,y;
{ double s;
s= 3.1415926*(x*x+ y*y);
return(s);
}
但 C语言规定, 在以下几种情况下可以不在调用函数中
对被调用函数作类型说明:
① 被调用函数为整型或字符型,自动按整型处理。
② 被调用函数的定义出现在调用函数之前。
③ 在调用函数之前已经由别的函数 ( 它可能也要调用该
被调用函数 ) 对被调用函数作了类型说明 。
( 4) C语言虽不允许嵌套定义函数,但可以嵌套调用函
数。
5.2 模块间的参数传递
5.2.1 形参与实参的结合方式
1,地址结合
所谓地址结合,是指在一个模块调用另一个模块时,并不
是将调用模块中的实参值直接传送给被调用模块中的形参,而
只是将存放实参的地址传送给形参。
2,数值结合
所谓数值结合,是指调用模块中的实参地址与被调用模块
中的形参地址是互相独立的,在一个模块调用另一个模块时,
直接将实参值传送给形参被存放在形参地址中。
例 5.3 分析下列 C程序:
void swap(x,y)
int x,y;
{ int t;
t= x; x= y; y= t;
return;
}
#include "stdio.h"
main()
{ int x,y;
scanf("x= %d,y= %d",&x,&y);
swap(x,y);
printf("x= %d,y= %d\n",x,y);
}
在这个程序中共有两个函数 。 在主函数中分别为整型变
量 x与 y输入数据, 然后调用函数 swap(x,y)。 而函数 swap(x,
y)的功能是实现变量 x值与 y值的交换 。 但在实际运行该程序
时, 如果从键盘输入
x= 6,y= 8?
则输出的结果为
x= 6,y= 8
即没有达到交换的目的 。 这是因为在主函数中调用函数
swap()时, 只是将实参 x和 y的值分别传递给了 swap()函数中
的形参 x和 y,但由于主函数中的实参 x和 y与函数 swap()中的
形参 x和 y在计算机中的存储地址是不同的, 因此, 在函数
swap()中虽然交换了形参 x与 y的值, 但实参 x与 y的值实际上
没有改变, 即它们没有被交换 。
由此可以看出,在形参与实参为数值结合的情况
下,实参与形参在计算机内存中的存储地址不是同一
个,因此,即使在被调用函数中改变了形参值,调用
函数中的实参值也是不会被改变的。
5.2.2 局部变量与全局变量
1,局部变量
在函数内部定义的变量称为局部变量 。 函数内部定义的变
量只在该函数范围内有效, 因此, 不同函数中的局部变量可
以重名, 互不混淆 。
2,全局变量
在函数外定义的变量称为全局变量 。
除非十分必要, 一般不提倡使用全局变量, 其原因有以
下几点:
① 由于全局变量属于程序中的所有函数, 因此, 在程序
的执行过程中, 全局变量都需要占用存储空间, 即使实际正
在执行的函数中根本用不着这些全局变量, 它们也要占用存
储空间 。
② 在函数中使用全局变量后, 要求在所有调用该函数的
调用程序中都要使用这些全局变量, 从而会降低函数的通用
性 。
③ 在函数中使用全局变量后, 使各函数模块之间的互相
影响比较大, 从而使函数模块的, 内聚性, 差, 而与其他模
块的, 耦合性, 强 。
④ 在函数中使用全局变量后, 会降低程序的清晰性, 可
读性差 。
5.2.3 动态存储变量与静态存储变量
1,用户程序的存储分配
一般来说, 用户程序在计算机中的存储分配如图 5.1所示 。
程序区
静态存储区
动态存储区
图 5.1 用户程序在计算机中的存储分配
其中:
程序区用于存放程序;
静态存储区是在程序开始执行时就分配的固定存储单
元, 如全局变量;
动态存储区是在函数调用过程中进行动态分配的存储
单元, 如函数形参, 自动变量, 函数调用时的现场保护和返
回地址等 。
2.变量的存储类型
① 数据类型:如整型( int)、实型( float)、字符型(
char)、双精度型( double)等。
② 数据的存储类型:分为自动类型( auto)、静态类型(
static)、寄存器类型( register)、外部类型( extern)。
例 5.4 计算并输出 1~ 5的阶乘值 。 其 C程序如下:
int fac(n)
int n;
{ static int f= 1;
f= f*n;
return(f);
}
#include "stdio.h"
main()
{ int i;
for (i= 1; i<= 5; i= i+ 1)
printf("%d!= %d\n",i,fac(i));
}
下面对静态存储变量作几点说明:
① 形参不能定义成静态类型 。
② 对局部静态变量赋初值是在编译时进行的, 在调用时
不再赋初值;而对自动变量赋初值是在调用时进行的, 每次
调用将重新赋初值 。
③ 定义局部静态变量时若不赋初值, 则在编译时将自动
赋初值 0;但在定义自动变量时若不赋初值, 则其初值为随
机值 。
④ 若无多大必要, 尽量不用局部静态变量 。
3.外部变量
全局变量如果在文件开头定义,则在整个文件范围
内的所有函数都可以使用该变量。
一般来说, 全局变量有以下几种用法:
① 在同一文件中,为了使全局变量定义点之前的函
数中也能使用该全局变量,则应在函数中用 extern加以
说明。
② 使一个文件中的函数也能用另一个文件中的全局
变量。
③ 利用静态外部变量,使全局变量只能被本文件中
的函数引用。
5.2.4 内部函数与外部函数
在 C语言中, 函数可以分为内部函数与外部函数 。 只
能被本文件中其他函数调用的函数称为内部函数, 内部函
数又称为静态函数 。
定义内部函数的形式如下:
static 类型标识符 函数名 ( 形参表 )
定义外部函数的形式如下:
[extern] 类型标识符 函数名 ( 形参表 )
5.3 模块的递归调用
人们在解决一些复杂问题时,为了降低问题的复杂程
度(如问题的规模等),一般总是将问题逐层分解,最后
归结为一些最简单的问题。
例 5.5 编写一个 C函数,对于输入的参数 n,依次打印输
出自然数 1到 n。
这是一个很简单的问题,实际上不用递归就能解决,其 C
函数如下:
#include "stdio.h"
wrt(int n)
{ int k;
for (k= 1; k<= n; k++ ) printf("%d\n",k);
return;
}
解决这个问题还可以用以下的递归函数来实现:
#include "stdio.h"
wrt1(int n)
{ if (n!= 0)
{ wrt1(n- 1); printf("%d\n",n); }
return;
}
自己调用自己的过程称为递归调用过程 。 在 C语言中,
自己调用自己的函数称为递归函数 。
递归分为直接递归与间接递归两种 。
所谓直接递归,是指直接调用函数本身。
5.4 程序举例
例 5.6 编写一个函数, 其功能是判断给定的正整数是否是素数,
若是素数则返回函数值 1,否则返回函数值 0。
其 C函数如下:
#include "math.h"
sushu(int n)
{ int k,i,flag;
k= sqrt((double)n); i= 2; flag= 0;
while ((i<= k)&&(flag== 0))
{ if (n%i== 0) flag= 1;
i= i+ 1;
}
return(!flag);
}
例 5.8 Hanoi塔问题 。
相传古代印度有一座 Bramah庙, 庙中有三根插在黄铜板上的
宝石柱, 在其中的一根柱子上放了 64个金盘子, 大盘在下, 小盘
在上, 称为 Hanoi塔 。 有一天, 庙里的和尚们想把这些盘子从一
根柱子上移到另一根柱子上, 规定每次只允许移动一个盘子, 并
且, 在移动过程中都不允许出现大盘子压在小盘子上面的现象,
但在移动盘子的过程中可以利用三根柱子中的任何一根 。
为了使问题具有普遍性, 假设圆盘数为 n,按直径从小到大
依次编号为 1,2,…, n;三根柱子的名称分别为 X,Y,Z。 开始
时, n个圆盘按从大到小的顺序 ( 即下面放大圆盘, 上面放小圆
盘 ) 放在 X柱子上, 现在要将 X柱子上的 n个圆盘移到 Z柱子上, 其
移动的原则如上所述 。 这个问题称为 n阶 Hanoi塔问题 。
可以写出 C程序如下:
#include "stdio.h"
main()
{ int n;
char x= 'X',y= 'Y',z= 'Z';
void hanoi();
printf("input n= ");
scanf("%d",&n);
hanoi(n,x,y,z);
}
void hanoi(n,x,y,z)
int n;
char x,y,z;
{ void move();
if (n== 1) move(x,n,z);
else
{ hanoi(n- 1,x,z,y);
move(x,n,z);
hanoi(n- 1,y,x,z);
}
return;
}
void move(x,n,z)
int n;
char x,z;
{
printf("%c(%d)---- >%c\n",x,n,z);
}
5.5 编译预处理
编译预处理功能是 C语言的一个重要特点 。 所谓编译预处
理, 是指 C语言编译系统首先对程序模块中的编译预处理命令
进行处理 。
C语言提供的编译预处理命令主要有以下 3种:
( 1) 宏定义;
( 2) 文件包含命令;
( 3) 条件编译命令 。
编译预处理命令一般是在函数体的外面 。 正确使用编译
预处理命令, 可以编写出易于调试, 易于移植的程序模块 。
5.5.1 文件包含命令
文件包含是指一个源文件可以将另一个指定的源文件包括
进来 。
文件包含命令的一般形式为
#include 〈 文件名 〉

#include "文件名 "
其功能是将指定文件中的全部内容读到该命令所在的位置
后一起被编译 。
在使用文件包含命令时, 要注意以下几个问题:
( 1) 当 #include命令指定的文件中的内容改变时, 包含这个文
件的所有源文件都应该重新进行编译处理;
( 2) 一个 #include命令只能指定一个被包含文件, 如果需要包
含多个文件, 则要用多个 #include命令实现;
( 3) 被包含的文件应该是源文件, 不能是经编译后的目标文
件;
( 4) 文件包含可以嵌套使用, 即被包含的文件中还可以使用
#include命令;
( 5) 由 #include命令所指定的文件中可以有任何语言成分, 因
此, 通常可以将经常使用的, 具有公用性质的符号常量, 带参数
的宏定义以及外部变量等集中起来放在这种文件中, 以尽量避免
一些重复操作 。
5.5.2 条件编译命令
C语言的编译预处理程序提供了条件编译能力, 以便使
同一个源程序在不同的编译条件下能够产生不同的目标代码
文件 。
1,#ifdef,#else,#endif
其一般形式为
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
其作用是, 如果, 标识符, 已经定义过 ( 一般是指用
#define命令定义 ), 则程序段 1参加编译, 而程序段 2不参加
编译;否则程序段 2参加编译, 而程序段 1不参加编译 。
2,#ifndef,#else,#endif
其一般形式为
#ifndef 标识符
程序段 1
#else
程序段 2
#endif
其作用是,如果标识符没有定义过,则程序段 1参加编译,而
程序段 2不参加编译;否则程序段 2参加编译,而程序段 1不参加编
译。
3,#if,#else,#endif
其一般形式为
#if 常量表达式
程序段 1
#else
程序段 2
#endif
其作用是,如果常量表达式的值为, 真, (值非 0),则
程序段 1参加编译,而程序段 2不参加编译;否则程序段 2参加
编译,而程序段 1不参加编译。
4,#undef
其一般形式为
#undef 标识符
其作用是, 将已经定义的标识符变为未定义 。