第 10 章 函数与程序结构
10.1 教学要点
本章主要介绍函数的组织、函数的嵌套调用和递归函数的概念与编程,其中递归函数是 本章的重点。本章还介绍了宏——能起简单函数功能的作用,但并不是函数。通过本章的学 习,应帮助学生在函数运用上更上一层楼。
10.1 节通过 5 个简单函数构成的一个示例程序——设计一个常用圆形体体积计算器,引 出了多函数组成程序的方式。教师在讲授时,先介绍和分析该示例程序的总体框架结构,函 数调用的三层结构,并要打消学生对长程序的恐惧心理,让学生理解多函数结构可以有效降 低程序复杂度。教师在教学中要抓住复杂问题需要多个函数解决这条主线。
10.2 节主要介绍递归函数。在介绍本节内容时,应抓住递归函数的两个要点:递归式子 和递归出口。学生对递归函数的掌握有一定难度,主要问题在递归式子上。像求类似阶乘的 例子,递归式子很明显,学生容易理解。但像汉诺塔、整数逆序输出等例子,如何归纳出递 归式子是教学重点。要培养学生递归思维,为后续其它编程方面课程打下基础。
10.3 节介绍编译预处理概念,包括文件包含、宏和条件编译的内容。宏是本节重点:宏 的概念、定义和使用。要结合示例向学生讲清楚宏在编译预处理时起作用,其实质是替换,
并不能像函数那样进行计算。宏能起到简单函数的作用,但它不是函数。像带参数的宏,学 生容易与函数混淆。
10.4 节通过
多文件模块的学生信息库系统的例子,向学生展示大程序构成
,为学生以后编写规模较大程序建立初步概念。本节主要通过文件包含把多个文件模块连接起来,
上机实验时,对照实验指导书掌握 VC++工程文件的实现方式。外部变量、静态全局变量、
外部函数、静态函数等为多文件模块间的通信提供了手段。
讲授学时:4 学时,实验学时同讲授学时。
本章的知识能力结构图见图10.1。
图 10.1 知识能力结构图 多 个 函 数 构
成 的 程 序 结 构
函 数 与 程 序 结
构 宏
递归函数
编译预处理
递归出口 递归式子 递归概念
递归函数定义
宏基本定义
掌握编译预处理概念 带参数的宏
能 够 对 相 对 复 杂 的 问题,合理定义程序 的多函数结构。
能 够 使 用 递 归 函 数 进行编程
掌 握 宏 的 基 本用法。
函数嵌套调用 文件包含 全局变量与外 部变量的使用
10.2 讲稿
1
Chap 10 函数与程序结构
10.1 圆形体积计算器 10.2 汉诺塔问题 10.3 长度单位转换 10.4 大程序构成
中心:函数与程序结构关系 本章分4 节:
1、﹅ 单文件模块内的函数嵌套调用 2、﹅ 递归函数
3、﹅ 宏为主要内容的编译预处理
4、﹅ 通过文件包含来实现稍大规模的多文 件模块程序
2
本章要点
n怎样把多个函数组织起来?
n怎样用结构化程序设计的思想解决问题?
n怎样用函数嵌套求解复杂的问题?
n怎样用函数递归解决问题?
n如何使用宏?
n如何使用多文件模块构建较大规模程序
提出本章的学习要点。
简单函数à多函数构建à递归函数à
函数组织——程序文件模块 编译预处理
3
使用结构化程序设计方法解决复杂的问题
¨把大问题分解成若干小问题,小问题再进一步分解成 若干更小的问题
¨写程序时,用main()解决整个问题,它调用解决小问 题的函数
¨这些函数又进一步调用解决更小问题的函数,从而形 成函数的嵌套调用
10.1 圆形体积计算器
对于复杂问题——本节的讨论核心,以结 构化的方法进行分解是一个有效解决办 法,虽然学生现在编写的都是小程序,但 是结构化思想一定要让学生建立起来。
4
main( )
函数1 函数2 …
… 函数m
函数 1_1
函数 1_2
函数
m_1 函数
m_n
…
…
程序结构
层次(树形)结构描述结构化思想。
5
例10-1 设计一个常用圆形体体积计算器,采用 命令方式输入1、2、3,分别选择计算球体、
圆柱体、圆锥体的体积,并输入计算所需相 应参数。
分析:
¨输入1、2、3选择计算3种体积,其他输入结束 计算
¨设计一个控制函数cal(),经它辨别圆形体的类型 再调用计算球体、圆柱体、圆锥体体积的函数
¨设计单独的函数计算不同圆形体的体积
10.1.1 程序解析-计算常用圆形体体积
提醒学生不要匆忙考虑具体程序实现,首 先应进行整体分析:功能分解与确定。
6
3层结构,5个函数
降低程序的构思、编写、调试的复杂度 可读性好
程序结构
main( ) cal ( )
vol_ball ( ) vol_cylind ( ) vol_cone ( )
直观考虑:
球体、圆柱体、圆锥体体积的计算分 别定义3 个函数,再定义主函数
主函数功能确定:
若把根据输入选择确定3 个体积函数 放在主函数中,会造成主函数规模偏大,
因此通过定义 cal 函数,使主函数功能简 洁明了。
7
例10-1源程序
#define PI 3.141592654 void cal ( int sel );
int main(void) { int sel;
while( 1 ){
printf(" 1-计算球体体积\n");
printf(" 2-计算圆柱体积\n");
printf(" 3-计算圆锥体积\n");
printf(" 其他-退出程序运行\n");
printf(“ 请输入计算命令:" );
scanf("%d",&sel);
if (sel < 1 || sel > 3)
break; /* 输入非1~3,循环结束 */
else
cal (sel ); /* 输入1~3,调用cal() */
} return 0;
}
while(1)为永真循环,循环结束靠循环体中 的break 语句实现。除去 5 个 printf 语句,
主函数非常简洁。
讨论:如果用while(条件),则条件是什 么?循环体需要修改吗?
8 /* 常用圆形体体积计算器的主控函数 */
void cal ( int sel ) { double vol_ball(void );
double vol_cylind(void );
double vol_cone(void );
switch (sel) {
case 1: printf("球体积为:%.2f\n", vol_ball( ));
break;
case 2: printf("圆柱体积为:%.2f\n", vol_cylind( ) );
break;
case 3: printf("圆锥体积为:%.2f\n", vol_cone( ) );
break;
} }
/* 计算球体体积 V=4/3*PI*r*r*r */
double vol_ball( ) { double r ;
printf("请输入球的半径:");
scanf("%lf",&r);
return(4.0/3.0*PI*r*r*r);
}
/* 计算圆柱体积 V=PI*r*r*h */
double vol_cylind( ) { double r , h ;
printf("请输入圆柱的底圆半径和高:");
scanf("%lf%lf",&r,&h);
return(PI*r*r*h);
}
/* 计算圆锥体积 V=h/3*PI*r*r */
double vol_cone( ) { double r , h ;
printf("请输入圆锥的底圆半径和高:");
scanf("%lf%lf",&r,&h);
return(PI*r*r*h/3.0);
}
注意3 个体积计算的函数声明。函数调用
是以printf 的实参形式出现的。
问题:3 个体积计算的函数声明可以放到 别的地方吗?
9
10.1.2 函数的嵌套调用
n顺序调用
int main(void) { ……
y =fact(3);
……
z = mypow(3.5, 2);
……
}
double fact(int n) {
……
}
double mypow(double x, in n) {
……
}
main
fac t mypow
main fac t mypow
先讨论函数的顺序调用
10 n嵌套调用 函数的嵌套调用
int main(void) { ……
cal (sel);
……
}
void cal (int sel) { ……
vol_ball()
……
}
double vol_ball( ) {
……
}
main
c al
vol_ball main
c al vol_ball
对比顺序调用,嵌套调用的概念就比较清 楚。C 语言允许多层的嵌套调用。
11 int main(void) 例10-1 分析
{ ……cal (sel);
}void cal (int sel) { ……vol_ball();
vol_cylind();
vol_cone();
}double vol_ball( ) { ……
}double vol_cylind( ) { ……
}double vol_cone( ) { ……
}
main( ) cal ( )
vol_ball ( ) vol_cylind ( ) vol_cone ( )
例10-1 属于嵌套调用的结构。
12
函数的嵌套调用
在一个函数中再调用其它函数的情况称为函 数的嵌套调用。
如果函数A调用函数B,函数B再调用函数 C,一个调用一个地嵌套下去,构成了函数 的嵌套调用。
具有嵌套调用函数的程序,需要分别定义多 个不同的函数体,每个函数体完成不同的功 能,它们合起来解决复杂的问题。
结构化的程序设计方法一般都存在函数 的嵌套调用结构,用以解决复杂的问题。
13
结构化程序设计方法
n自顶向下,逐步求精,函数实现
¨自顶向下:程序设计时,应先考虑总体步骤,后考虑步 骤的细节;先考虑全局目标,后考虑局部目标。先从最 上层总目标开始设计,逐步使问题具体化。不要一开始 就追求众多的细节。
¨逐步求精:对于复杂的问题,其中大的操作步骤应该再 将其分解为一些子步骤的序列,逐步明晰实现过程。
¨函数实现:通过逐步求精,把程序要解决的全局目标分 解为局部目标,再进一步分解为具体的小目标,把最终 的小目标用函数来实现。问题的逐步分解关系,构成了 函数间的调用关系。
多个函数有效组织的目的之一——结构 化程序设计方法实现:
l 自顶向下 l 逐步求精 l 函数实现
虽然第五章介绍过结构化程序设计方法 的思想,现在重提,让学生有进一步的认 识
14
n 限制函数的长度。一个函数语句数不宜过多,既便于阅 读、理解,也方便程序调试。若函数太长,可以考虑把 函数进一步分解实现。
n 避免函数功能间的重复。对于在多处使用的同一个计算 或操作过程,应当将其封装成一个独立的函数,以达到 一处定义、多处使用的目的,以避免功能模块间的重复。
n 减少全局变量的使用。应采用定义局部变量作为函数的 临时工作单元,使用参数和返回值作为函数与外部进行 数据交换的方式。只有当确实需要多个函数共享的数据 时,才定义其为全局变量。
函数设计时应注意的问题
提出函 数 设 计 时 应 注 意 的 问 题
使学生在今后的函数编写中能遵循这些 原则
15
10.2 汉诺塔问题
10.2.1 程序解析
10.2.2 递归函数基本概念 10.2.3 递归程序设计
本节介绍递归函数,这是函数中一块不易 掌握的内容。先从有趣的例子入手,再讲 递归函数的基本定义与基本使用
16 汉诺塔是递归函数的经典例子,它很难用
非递归实现。由此也可以向学生说明递归 方法的重要性。
递归方法的优越性:
1、﹅ 能实现非递归很难实现的问题 2、﹅ 程序简洁
难点:
递归思想不容易建立。
10.2.1 Hanoi)
64 A B
A B C
A B C
17
分析
A B C
请学生说明搬动3 个盘子的过程,教师可
以ppt 非播放状态下移动图形。
18
分析
A B C
A B C
n
n-1
演示n 个盘子的实现过程,强调如果能把
n-1 个盘子搬开,就能解决 n 个盘子的问 题——这是递归的根本(递归式子)。
19
分析
A B C
A B C
n
演示
20
10.2.1 汉诺(Hanoi)塔问题解析
n递归方法的两个要点
¨(1)递归出口:一个盘子的解决方法;
¨(2)递归式子:如何把搬动64个盘子的问题简 化成搬动63个盘子的问题。
n把汉诺塔的递归解法归纳成三个步骤:
¨n-1个盘子从座A搬到座C
¨第n号盘子从座A搬到座B
¨n-1个盘子从座C搬到座B
从汉诺塔例子归纳出递归方法的 2 个要
点:递归出口与递归式子
后面的例子都从这两点着手分析
21
算法:hanio(n个盘,A→B, C为过渡) { if (n == 1)
直接把盘子A→B else{
hanio(n-1个盘,A→C, B为过渡) 把n号盘 A→B
hanio(n-1个盘,C→B, A为过渡) }
}
A B C
n-1
以算法形式给出汉诺塔的解法,这里注重 分析,不急于写出程序
22
10.2.2递归函数基本概念
n例10-2 用递归函数实现求n!
¨递推法
n在学习循环时,计算n!采用的就是递推法:
n!= 1×2×3×…×n
n用循环语句实现:
result = 1;
for(i = 1; i <= n; i++) result = result * i;
¨递归法
nn!= n ×(n-1)! 当n>1 递归式子
= 1 当n=1或n=0 递归出口
n即求n!可以在(n-1)!的基础上再乘上n。如果把求n!写成函
数fact(n),则fact (n)的实现依赖于fact(n-1)。
相同问题采用不同的表示方式:递推法与 递归法。它将导致不同程序方法的实现:
n 第 4 章通过循环实现递推法 n 例 10-2 用递归法实现
23
10.2.2递归函数基本概念
例10-2 用递归函数求n!。
#include <stdio.h>
double fact(int n);
int main(void) { int n;
scanf ("%d", &n);
printf ("%f", fact (n) );
return 0;
}
double fact(int n) /* 函数定义 */
{ double result;
if (n==1 || n == 0) /* 递归出口 */
result = 1;
else
result = n * fact(n-1);
return result;
}
阶乘函数实现已经学习过,引导学生找出 递归函数实现与以前实现的不同:
n 自己调用自己
n 有 n=0||n=1 的特殊情况考虑
24
10.2.2 递归函数基本概念
递归调用的2 种方式:
n 直接调用自己 n 间接调用自己
25 递归函数的2 要素:
n 递归出口
n 递归式子(表示)
fact(n)=n*fact(n-1)不符合 C 语言的语法 规则,赋值号=左面只能是变量,表示保 存单元。而fact(n)是函数形式,表示函数 计算,最终是数值。
26
main() fact(3) fact(2) fact(1) { .... { .... { .... { ....
printf(fact(3)) f=3*fact(2) f=2*fact(1) f=1 } return(f) return(f) return(f)
} } }
递归函数fact( n )的实现过程
fact(3)= 3*fact(2)=
2*fact(1)=
fact(1)=1 2*1=2 3*2=6 同时有4个函数在运行,且都未完成
动态演示递归函数的调用与返回过程,从 而了解递归函数的计算过程。
递归函数的运算远比递推法的循环来的 复杂,像例子中,main 调用 fact(3),最
后有4 个函数同时打开,然后再一层层返
回,关闭函数,最终回到main。
27
10.2.3 递归程序设计
用递归实现的问题,满足两个条件:
n问题可以逐步简化成自身较简单的形式(递归式)
n! = n * (n-1)!
n n-1
Σi = n +Σ i
i=1 i=1
n递归最终能结束(递归出口)
两个条件缺一不可
解决递归问题的两个着眼点
小结递归函数的2 要素:递归出口和递归
式子。
举几个例子讨论递归出口和递归式子分 别是什么。
n! = n * (n-1)!
Σi = n +Σ i 等等。
28
10.2.3 递归程序设计
n例10-3 编写递归函数reverse(int n)实现将 整数n逆序输出。
分析:
¨将整数n逆序输出可以用循环实现,且循环次数 与n的位数有关。递归实现整数逆序输出也需要 用位数作为控制点。归纳递归实现的两个关键 点如下:
n递归出口:直接输出n,如果n<=9,即n为1位数
n递归式子:输出个位数n%10,再递归调用 reverse(n/10) 输出前n-1位,如果n为多位数
强调:问 题 可 以 逐 步 简 化 成 自 身 较 简 单 的 形 式
重点分析:自 身 较 简 单 的 形 式 着眼点:数据的位 数
10-2
n!
n! = n * (n-1)! n > 1)
n! = 1 n =
0,1)
#include <stdio.h>
double fact(int n) int main(void) { int n;
scanf ("%d", &n);
printf ("%f", fact (n) );
return 0;
}
double fact(int n) { double result;
if (n==1 || n == 0) result = 1;
else
result = n * fact(n-1);
return result;
}
fact(n)=n*fact(n-1);
29 展现程序简洁的形式
可以与以前循环实现进行对比
30
例10-4 汉诺(Hanoi)塔问题
A B C
hanio(n个盘,A→B,C为过渡) { if (n == 1)
直接把盘子A→B else{
hanio(n-1个盘,A→C,B为过渡) 把n号盘 A→B
hanio(n-1个盘,C→B,A为过渡) } }
回到汉诺塔例子,进行程序实现 先回顾算法
31 /* 搬动n个盘,从a到b,c为中间过渡 */ 源程序
void hanio(int n, char a, char b, char c) { if (n == 1)
printf("%c-->%c\n", a, b);
else{
hanio(n-1, a, c, b);
printf("%c-->%c\n", a, b);
hanio(n-1, c, b, a);
} }
int main(void) { int n;
printf("input the number of disk: " );
scanf("%d", &n);
printf("the steps for %d disk are:\n",n);
hanio(n, 'a', ‘b', ‘c') ; return 0;
}
input the number of disk: 3
the steps for 3 disk are:
a-->b a-->c b-->c a-->b c-->a c-->b a-->b
3 个盘子搬动的程序结果。
32
A B C
input the number of disk: 3
the steps for 3 disk are:
a-->b a-->c b-->c a-->b c-->a c-->b a-->b
针对结果,教师可以 ppt 非播放状态下移 动图形来说明结果。
10.2.3
!
void
void reverse(int num) {
if (num<=9)
printf ("%d",num); /* / else {
printf ("%d",num%10);
reverse (num/10); /* / }
}
33
课堂练习:利用递归函数计算x的n次幂
int mi(int x, int n) {
if (n==1) return x;
else
return x*mi(x,n-1);
}
课堂练习
还是从递归的2 个要点检查学生掌握的情
况
34
10.3 长度单位转换
n10.3.1 程序解析
n10.3.2 宏基本定义
n10.3.3 带参数的宏定义
n10.3.4 文件包含
n10.3.5 编译预处理
宏是本节主要内容,文件包含再系统展现 一下,编译预处理了解一下即可
35
10.3.1 程序解析
n 例10-5 欧美国家长度使用英制单位,1英里=1609米,1英 尺=30.48厘米,1英寸=2.54厘米。请编写程序转换。
#include<stdio.h>
#define Mile_to_meter 1609 /* 1英里=1609米 */
#define Foot_to_centimeter 30.48 /* 1英尺=30.48厘米 */
#define Inch_to_centimeter 2.54 /* 1英寸=2.54厘米 */
int main(void)
{ float foot, inch, mile; /* 定义英里,英尺,英寸变量 */
printf("Input mile,foot and inch:");
scanf("%f%f%f", &mile, &foot, &inch);
printf("%f miles=%f meters\n", mile, mile * Mile_to_meter);
/* 计算英里的米数 */
printf("%f feet=%f centimeters\n", foot, foot *
Foot_to_centimeter); /* 计算英尺的厘米数 */
printf("%f inche s=%f centimeters\n", inch, inch * Inch_to_centimeter); /* 计算英寸的厘米数 */
return 0;
}
Input mile,foot and inch:1.2 3 5.1 1.200000 miles=1930.800077 meters 3.000000 feet=91.440000 centimeters 5.100000 inches=12.954000 centimeters
本例中虽然宏名定义得较长,但可读性非 常清晰
36
10.3 宏定义
#define 宏名标识符 宏定义字符串
编译时,把程序中所有与宏名相同的字符串,用宏定义字 符串替代
#define PI 3.14
#define arr_size 4 说明:
¨宏名一般用大写字母,以与变量名区别
¨宏定义不是C语句,后面不得跟分号
¨宏定义可以嵌套使用
#define PI 3.14
#define S 2*PI*PI 多用于符号常量
在符号常量使用的基础上,引入宏概念。
宏的定义、使用方法 宏定义不是C 语句
37
n宏定义可以写在程序中任何位置,它的作用范 围从定义书写处到文件尾。
n可以通过“#undef”强制指定宏的结束范围。
10.3.1 宏基本定义
与变量相同,宏定义也有作用范围
38
#define A “This is the first macro”
void f1() {
printf( “A\n” );
}
#define B “This is the second macro” A 的有效范围 void f2( )
{
printf( B ) ; B 的有效范围
}
#undef B int main(void) {
f1( );
f2( );
return 0;
}
宏的作用范围
通过例子演示宏定义的作用范围。本例子 仅作说明之用,例中A 与 B 的用法不具有 实用性。
39
带参数的宏定义实现简单的函数功能
例10-7 简单的带参数的宏定义。
#include <stdio.h>
#define MAX(a, b) (a) > (b) ? (a): (b)
#define SQR(x) (x) * (x) int main (void) {
int x , y;
scanf (“%d %d” , &x, &y) ;
x = MAX (x, y); /* 引用宏定义 */
y = SQR (x); /* 引用宏定义 */
printf(“%d %d\n” , x, y) ; return 0;
}
本例说明宏能解决简单函数功能,但参数 引用上必须带括号。
40 通过例子说明带参数的宏定义的使用方
法。
带 参 数 的 宏 定 义 不 是 函 数 , 不 能 解 决 f(x+y)的问题——除非加上括号。其根本 原因是宏的替换作用,这点务必要让学生 理解。做习题时必须先替换,再计算。
10.3.3
define f(a) a*a*a int main (void) /* / { int i, x, y, z;
for (i = 1; i <1 000; i++) { x=i%10; y=i/10%10; z=i/100 ; if (x*x*x+y*y*y+z*z*z==i)
printf (“%d\n” ,i);
} return 0;
}
define f(a) (a)*(a)*(a)
x+y*x+y*x+y (f(x)+f(y)+f(z) == i)
f(x+y) = x+y)3 ?
41
#define f(a,b,t) t=a; a=b; b=t;
int main( ) { int x,y,t ;
scanf(“%d%d” ,&x, &y);
f(x,y,t) printf(“%d %d\n”, x, y) ; return 0;
}
t=x ; x=y ; y=t ; 编译时被替换
•带参数的宏定义不是函数,宏与函数是两种不同的概念
•宏可以实现简单的函数功能
示例 用宏实现两个变量值的交换
与函数的区别在哪里?
通过本例和学生讨论宏与函数的异同点:
宏能解决简单函数功能,但计算机运行本 质截然不同,宏是编译预处理时进行替 换,不存在类似函数的调用过程。
42
宏定义应用示例
n定义宏LOWCASE,判断字符c是否为小写字母。
#define LOWCASE(c) (((c) >= 'a') && ((c) <= 'z') ) n定义宏CTOD将数字字符(‘0’~‘9’)转换为相应的
十进制整数,-1表示出错。
#define CTOD(c) (((c) >= '0') && ((c) <= '9') ? c - '0' : -1)
宏有广泛应用方式——简单函数功能。
43
#define F(x) x -‐ 2
#define D(x) x*F(x) int main()
{
printf("%d,%d", D(3), D(D(3))) ; return 0;
}
带宏定义的程序输出
本例学生较容易出错,可以先让学生计 算,然后通过计算机运行,教师再解释原 因。
44
n 阅读带宏定义的程序,先全部替换好,最后再统一计算 n 不可一边替换一边计算,更不可以人为添加括号
D(3) = x*F(x) 先用x替换展开
= x*x-2 进一步对F(x)展开,这里不能加括号
= 3*3-2 = 7 最后把x=3代进去计算
D(D(3)) = D(x*x-2) 先对D(3)用x替换展开,
= x*x-2* F(x*x-2) 拿展开后的参数对D进一步进行宏替换
= x*x-2* x*x-2-2 拿展开后的参数对F进一步进行宏替换
= 3*3-2*3*3-2-2 = -13 最后把x=3代进去计算 运行结果:7 -13
结果分析
分析出错原因:
其根本原因是宏的替换作用,而不是 函数计算,这点务必要让学生理解。做习 题时必须先 替 换 ,再计算。
45
10.3.4 文件包含
n系统文件以stdio.h、math.h等形式供编程 者调用
n实用系统往往有自己诸多的宏定义,也以.h 的形式组织、调用
n问题:如何把若干.h头文件连接成一个完整 的可执行程序?
¨文件包含include
文件包含是解决多个文件程序连接的有 效方法。
46
n格式
¨# include <需包含的文件名>
¨# include “需包含的文件名”
n作用
把指定的文件模块内容插入到 #include 所在的 位 置 , 当 程 序 编 译 连 接 时 , 系 统 会 把 所 有
#include 指定的文件拼接生成可执行代码。
n注意
¨编译预处理命令,以#开头。
¨在程序编译时起作用,不是真正的C语句,行尾 没有分号。
文件包含
系统文件夹 当前文件夹+系统文件夹
区分include 的两种写法,一般:
l <…> 适用于系统头文件 1) “…” 适用于多文件模块连接
47
例10-7 将例10-5中长度转换的宏,定义成头文件length.h,
并写出主函数文件。
头文件length.h源程序
#define Mile_to_meter 1609 /* 1英里=1609米 */
#define Foot_to_centimeter 30.48 /* 1英尺=30.48厘米 */
#define Inch_to_centimeter 2.54 /* 1英寸=2.54厘米 */
主函数文件prog.c源程序
#include<stdio.h>
#include “length.h” /* 包含自定义头文件 */
int main(void)
{ float foot, inch, mile; /* 定义英里,英尺,英寸变量 */
printf("Input mile,foot and inch:");
scanf("%f%f%f", &mile, &foot, &inch);
printf("%f miles=%f meters\n", mile, mile * Mile_to_meter);
printf("%f feet=%f centimeters\n", foot, foot * Foot_to_centimeter);
printf("%f inches=%f centimeters\n", inch, inch * Inch_to_centimeter);
return 0;
}
本例把3 个宏定义单独形成一个.h 文件,
以作自定义头文件的示例
48
将例10-1的5个函数分别存储在2个.C文件上,要 求通过文件包含把它们联结起来。
头文件l engt h. h
#def i ne Mil e_t o_met er 1609
#def i ne Foot _t o_cent i met er 30. 48
#def i ne I nch_t o_cent i met er 2. 54
主函数文件pr og. c
#i ncl ude<s t di o. h>
#i ncl ude “l engt h. h”
i nt mai n( voi d) {
f l oat mil e, f oot , i nch;
……
r et ur n 0;
}
编译连接后生成的程序
… st di o. h的内容
#define Mile_to_meter 1609
#define Foot_to_centimeter 30.48
#define Inch_to_ centimeter 2.54 int main(void)
{
float mile,foot,inch;
……
return 0;
}
文件包含的作用:
在编译时,把需包含的文件插入到include 位置,插入后的程序规模将扩大。
可进一步引申到系统头文件,例如每个程 序都会用到的include <stdio.h>,其作用将 把系统头文件stdio.h 的所有程序插入到我 们编写的程序前,来确保pringf 等功能的 实现。
49
nctype.h 字符处理
nmath.h 与数学处理函数有关的说明与定义
nstdio.h 输入输出函数中使用的有关说明和定义
nstring.h 字符串函数的有关说明和定义
nstddef.h 定义某些常用内容
nstdlib.h 杂项说明
ntime.h 支持系统时间函数
常用标准头文件
可以结合教材后面附录说明。
50
n编译预处理是C语言编译程序的组成部 分,它用于解释处理C语言源程序中的各 种预处理指令。
n文件包含(#include)和宏定义(#define)都 是编译预处理指令
¨在形式上都以“#”开头,不属于C语言中真正 的语句
¨增强了C语言的编程功能,改进C语言程序设 计环境,提高编程效率
10.3.5 编译预处理
结合宏的运行,再强调编译预处理概念与 使用方式。
51
nC程序的编译处理,目的是把每一条C语句 用若干条机器指令来实现,生成目标程序。
n由于#define等编译预处理指令不是C语句,
不能被编译程序翻译,需要在真正编译之前 作一个预处理,解释完成编译预处理指令,
从而把预处理指令转换成相应的C程序段,
最终成为由纯粹C语句构成的程序,经编译 最后得到目标代码。
编译预处理
解释什么是编译,它与编译预处理的关 系。
52
n编译预处理的主要功能:
¨文件包含(#include)
¨宏定义(#define)
¨条件编译
编译预处理功能
53
n条件编译
#define FLAG 1
#if FLAG 程序段1
#else 程序段2
#endif
编译预处理功能
该内容了解即可。
54
10.4 大程序构成
——多文件模块的学生信息库系统
n10.4.1 分模块设计学生信息库系统
n10.4.2 C程序文件模块
n10.4.3 文件模块间的通信
本节将展示给学生处理规模稍大程序(多 函数)的方法,由于教材所限,示例程序 规模仍不够大,但思想需要掌握。
也希望学生最后能完整实现学生信息库 系统。
55
10.4.1 分模块设计学生信息库系统
学生信息库 系统 main()
计算平均成 绩 average()
平均成绩排 序 sort()
修改 modify() 建立
new_student() 输出
output_student() 查询
search_student()
例10-8 请综合例9-1、例9-2、例9-3和例9-4,分模 块设计一个学生信息库系统。该系统包含学生基本 信息的建立和输出、计算学生平均成绩、按照学生 的平均成绩排序以及查询、修改学生的成绩等功能。
函数建立为:
本例来源于第九章多个例题,由于题目本 身已比较熟悉,学生可以把注意力放在集 成实现上。
56
10.4.1 分模块设计学生信息库系统
n由于整个程序规模较大,按照功能图,分成三个程 序文件模块,并把结构体定义也写成一个头文件。
¨头文件student.h
¨输入输出程序文件input_output.c
nvoid new_student (struct student students[ ]) nvoid output_student(struct student students[ ])
¨计算平均成绩与平均成绩排序程序文件aver_sort.c
nvoid average(struct student students[ ]) nvoid sort(struct student students[ ])
¨查询修改程序文件modify.c
nvoid modify(struct student students[ ])
nvoid search_student(struct student students[ ], int num)
程 序 分 成 三 个 程 序 文 件 模 块+一个头文 件 ( 含 结 构 体 定 义 )
57
10.4.1 分模块设计学生信息库系统
n一共定义了三个.c程序文件和一个.h头文件,它们 各自独立,再通过主函数main()调用。主函数放在 student_system.c文件中,各文件存放在同一个 文件夹下,相互间的连接采用文件包含的形式。
¨主函数程序文件student_system.c
#include “student.h”
#include “input _output.c”
#include “aver_sort.c”
#include “modify.c”
int Count = 0; /* 全局变量,记录当前学生总数 */
int main(void)
{ ... } /* 主函数调用各函数 */
主函数构成
58
10.4.2 C程序文件模块
n结构化程序设计是编写出具有良好结构程序的有 效方法
n一个大程序最好由一组小函数构成
n如果程序规模很大,需要几个人合作完成的话,
每个人所编写的程序会保存在自己的.c文件中
n为了避免一个文件过长,也会把程序分别保存为 几个文件。
n一个大程序会由几个文件组成,每一个文件又可 能包含若干个函数。
我们把保存有一部分程序的文件称 为程序文件模块
从结构化程序设计思想出发,解释例10-8 设计的依据,并引出程序文件模块的概念
59
10.4.2 C程序文件模块
n一个大程序可由几个程序文件模块组成,每一个程 序文件模块又可能包含若干个函数。程序文件模块 只是函数书写的载体。
n当大程序分成若干文件模块后,可以对各文件模块 分别编译,然后通过连接,把编译好的文件模块再 合起来,连接生成可执行程序。
n问题:如何把若干程序文件模块连接成一个完整的 可执行程序?
¨文件包含
¨工程文件(由具体语言系统提供)
解释把一个程序用几个文件模块实现的 原因——对复杂程序:
1、﹅ 便于分模块编译、连接与调试,降低 上机调试复杂度
2、﹅ 可实现多人合作开发程序——这样对 程序分头编译查错,十分方便
多文件模块通过文件包含连接,注意各模 块中不能有各自的#include stdio.h 等内 容,统一在main()文件中
60
10.4.2 C程序文件模块
n程序-文件-函数关系
¨小程序:主函数+若干函数 à一个文件
¨大程序:若干程序文件模块(多个文件)à 每 个程序文件模块可包含若干个函数 à 各程序 文件模块分别编译,再连接
¨整个程序只允许有一个main()函数
帮学生理清关系:
程 序 - 文 件 - 函 数 关 系
61
10.4.3 文件模块间的通信
n文件模块与变量
¨外部变量
¨静态全局变量 n文件模块与函数
¨外部函数
¨静态的函数
局部变量、静态局部变量均与具体函数相 关,与文件模块无关。
这里介绍函数模块与变量、函数间的关系
62
10.4.3 文件模块间的通信
n外部变量
¨全局变量只能在某个模块中定义一次,如果其他模 块要使用该全局变量,需要通过外部变量的声明
外部变量声明格式为:
extern 变量名表;
¨如果在每一个文件模块中都定义一次全局变量,模 块单独编译时不会发生错误,一旦把各模块连接在 一起时,就会产生对同一个全局变量名多次定义的 错误
¨反之,不经声明而直接使用全局变量,程序编译时 会出现“变量未定义”的错误。
在某一文件模块中定义的全局变量,想在 其他模块中使用,相关文件模块必须通过 外部变量声明后,方可使用。变量实体只 有一个,在定义处,外部变量声明只是告 诉编译系统,并没有分配单元。
63
10.4.3 文件模块间的通信
n静态全局变量
¨当一个大的程序由多人合作完成时,每个程 序员可能都会定义一些自己使用的全局变量
¨为避免自己定义的全局变量影响其他人编写 的模块,即所谓的全局变量副作用,静态全 局变量可以把变量的作用范围仅局限于当前 的文件模块中
¨即使其他文件模块使用外部变量声明,也不 能使用该变量。
静 态 全局变量可以使全局变量只限于本 文件模块中使用,而不能被其他文件引用
,即使其它文件模块声明了外部变量也无 法使用。
静 态 全局变量的作用是避免多文件模块 间的变量干扰——副作用。
64
10.4.3 文件模块间的通信
n文件模块与函数
¨外部函数
n如果要实现在一个模块中调用另一模块中的函数 时,就需要对函数进行外部声明。声明格式为:
extern 函数类型 函数名(参数表说明);
¨静态的函数
n把函数的使用范围限制在文件模块内,不使某程 序员编写的自用函数影响其他程序员的程序,即 使其他文件模块有同名的函数定义,相互间也没 有任何关联,
n增加模块的独立性。
类似外部变量的概念,多文件模块上的函 数相互间的调用,采用外部函数声 明 。声 明仅对编译起作用,说明要调用的函数在 外部文件模块,本地未定义。
类似静态全局变量的概念,静态函数可以 阻断多文件模块上的函数相互间的调用,
避免多文件模块间的函数干扰——副作 用。
65 回顾和总结本章的教学要点,对学生提出 能力要求:
• 能够对相对复杂的问题,合理定义程序 的多函数结构,能合理运用外部变量、
静态全局变量和静态函数。
• 建立递归思想,具备递归函数的编程能 力,并解决一些特殊问题。
• 掌握宏的基本用法,特别是带参数的宏。
10.3 练习与习题参考答案
10.3.1 练习参考答案
【练习 10-1】使用递归函数计算 1 到 n 之和:若要用递归函数计算 sum=1+2+3+...+n(n 为 正整数),请写出该递归函数的递归式子及递归出口。试编写相应程序。
解答:
#include <stdio.h>
int sum(int n);
int main(void) {
int n;
int result;
scanf("%d",&n);
result=sum(n);
printf ("%d\n", result);
return 0;
}
int sum(int n) {
int result;
if (n == 0) result = 0;
else
result = sum(n-1)+n;
return result;
}
【练习 10-2】请完成以下宏定义:
!
"
"
"
"
!
"
"
!
"
"
" ——
1)MIN(a, b):求 a, b 的最小值。
解答:#define MIN(a, b) (a) < (b) ? (a): (b) 2)ISLOWER(c):判断 c 是否为小写字母。
解答:#define ISLOWER(c) (((c) >= 'a') && ((c) <= 'z') ) 3)ISLEAP(y):判断 y 是否为闰年。
解答:#define ISLEAP(y) ((y) % 4 == 0 && (y) % 100 != 0) || ((y) % 400 = = 0) 4)CIRFER(r):计算半径为 r 的圆周长。
解答:#define PI 3.14159
#define CIRFER(r) 2*PI*(r)
【练习 10-3】分别用函数和带参数的宏实现从 3 个数中找出最大数,请比较两者在形式上 和使用上的区别。
解答:
(1) 使用宏实现
#include<stdio.h>
#define f(x,y,z) x>(y>z?y:z)?x:(y>z?y:z) int main(void)
{
int a,b,c;
printf("input a, b, c: ");
scanf("%d%d%d",&a,&b,&c);
printf("max=%d\n",f(a,b,c));
return 0;
}
(2) 使用函数实现
#include <stdio.h>
int max(int a, int b, int c) {
return ((a>b?a:b)>c? (a>b?a:b):c);
}
int main(void) {
int a, b, c, s;
printf("input a, b, c: ");
scanf("%d%d%d", &a, &b, &c);
s = max(a,b,c);
printf("max = %d\n", max(a,b,c));
return 0;
}
10.3.2 习题参考答案
一 、 选 择 题
1 2 3 4 5 6 7
C A B A D A A
二 、 填 空 题
1、文件包含 宏定义 条件编译 2、 5
3、int t0,t1,t; n>1 t0+t1 t
4、int p=1 p=p*m int s=0 s=s+power(i,k) 5、g=4,g=3,k=6
6、7,-13
三 、 程 序 设 计 题
1.判断满足条件的三位数:编写一个函数,利用参数传入一个3位数n, 找出 101~n
间所有满足下列两个条件的数:它是完全平方数,又有两位数字相同,如144、676 等,函
数返回找出这样的数据的个数。试编写相应程序。
解答:
#include <stdio.h>
#include <math.h>
int fun(int n);
int main(void) {
int n;
printf("input n: ");
scanf("%d", &n);
printf("total=%d\n", fun(n));
return 0;
}
int fun(int n) {
int i, d=0;
for(i=101; i<=n; i++)
if(((int)sqrt(i) * (int)sqrt(i)) == i) {
if (i/100==(i/10)%10|| i/100==i%10||(i/10)%10==i%10) d++;
} return d;
}
2.递归求阶乘和:输入一个整数n(n>0 且n≤10), 求1!+2!+3!+…+n!。
定义并调用函数fact(n)计算n!,函数类型是 double。试编写相应程序。
解答:
double fact(int n);
int main(void) {
int i,n;
double sum;
scanf("%d",&n);
sum=0;
for (i=1; i<=n; i++) sum += fact (i);
printf ("%.0f\n", sum);
return 0;
}
double fact(int n) {
double result;
if (n==1 || n == 0) result = 1;
else
result = n * fact(n-1);
return result;
}
3.递归实现计算xn :输入实数x和正整数n,用递归函数计算 xn的值。试编写相应程序。
解答:
#include <stdio.h>
double fun(double x,int n);
int main(void) {
int n;
double x, root;
scanf("%lf%d", &x,&n);
root = fun(x, n);
printf("Root = %0.2f\n", root);
return 0;
}
double fun(double x, int n) {
if(n == 1) return x;
else if(n == 0) return 1;
else
return x * fun(x, n-1);
}
4.递归求式子和:输入实数x和正整数n,用递归的方法对下列计算式子编写一个函数。
2 3 4 1
( , ) ... ( 1)
n nf x n = − x x + x − x + + −
−× x
(n>0)试编写相应程序。
解答:
#include <stdio.h>
#include <math.h>
double f(double x, int n) {
if(n==1) return x;
return pow(-1,n-1)*pow(x,n)+f(x, n-1);
}
int main(void) {
int n;
double x;
scanf("%lf%d", &x, &n);
printf("%lf\n", f(x,n));
return 0;
}
5.递归计算函数Ack(m,n):输入两个整数 m 和 n(m≥0 且 n≥0),输出函数 Ack(m,n)的值。
Ack(m,n)定义为(m≥0 且 n≥0):
n + 1 m=0
Ack(m, n)= Ack(m - 1, 1) n=0 && m>0 Ack(m - 1, Ack(m, n - 1) ) m>0 && n>0 试编写相应程序。
解答:
#include <stdio.h>
int Ack(int m, int n);
int main(void) {
int m,n;
int result;
scanf("%d%d", &m, &n);
result = Ack(m,n);
printf("Ackerman(%d,%d)=%d\n", m, n, result);
return 0;
}
int Ack(int m, int n) {
if (m==0) return n+1;
else if (n==0) return Ack(m-1, 1);
else return Ack(m-1, Ack(m, n-1));
}
6. 递归实现求Fabonacci 数列:用递归方法编写求斐波那契数列的函数,返回值为整型,
并写出相应的主函数。斐波那契数列的定义为:
f(0) = 0,f(1) = 1
f(n) = f(n - 2) + f(n - 1) (n>1)
解答:
#include <stdio.h>
int fib(int n);
int main(void) {
scanf("%d",&n);
printf("fib(%d)=%d\n",n,fib(n));
return 0;
}
int fib(int n) {
int res;
if(n==0) res=0;
else if(n==1) res=1;
else
res=fib(n-2)+fib(n-1);
return res;
}
7. 递归实现十进制转二进制:输入一个正整数 n,将其转换为二进制后输出。要求定义并调 用函数 dectobin(n),它的功能是输出 n 的二进制。试编写相应程序。
解答:
#include "stdio.h"
int main(void) {
int i,n;
void dectobin(int n);
scanf("%d",&n);
dectobin(n);
return 0;
}
void dectobin(int n) {
if(n<2)
printf("%d", n);
else{
dectobin(n/2);
printf("%d", n%2);
} }
8. 递归实现顺序输出整数:输入一个正整数n,编写递归函数实现对其进行按位顺序输
出。试编写相应程序。
解答:
#include <stdio.h>
void inorder(int n);
int main(void) {
int n;
scanf("%d",&n);
inorder(n);
return 0;
}
void inorder(int num) {
if(num < 10)
printf("%d\n",num);
else {
inorder(num/10);
printf("%d\n",num%10);
} }
9. 使用文件包含统计素数:输入 n(n<10)个整数,统计其中素数的个数。要求程序由两个文件
组成,一个文件中编写main 函数,另一个文件中编写素数判断的函数。使用文件包含的方
式实现。
解答:
文件prog.cpp
#include<stdio.h>
#include "prime.h"
int main(void) {
int i,n,count,x;
count=0;
scanf("%d",&n);
for(i=1;i<=n;i++) {
scanf("%d",&x);
if(IsPrime(x)) count++;
}
printf("count=%d\n",count);
return 0;
}
文件prime.h int IsPrime(int m) {
int i,n;
if(m==1) return 0;
n=m/2;
for(i=2;i<=n;i++) if(m%i==0)
return 0;
return 1;
}
10.三角形面积为
area = s×(s − a)×(s − b)×(s − c) 其中, s = (a + b + c)/2
a、b 、c 分别为三角形的 3 条边。请分别定义计算 s 和 area 的宏。再使用函数实现,比较 两者在形式上和使用上的区别。
解答:
(1) 使用宏实现
#include <stdio.h>
#include <math.h>
#define S(a ,b, c) ((a)+(b)+(c))/2
#define AREA(s,a,b,c) sqrt((s)*((s)-(a))*((s)-(b))*((s)-(c))) int main(void)
{
double a, b, c, s;
printf("input a, b, c: ");
scanf("%lf%lf%lf", &a, &b, &c);
s = S(a,b,c);
printf("s = %lf, area = %lf\n", s, AREA(s,a,b,c));
return 0;
}
(2) 使用函数实现
#include <stdio.h>
#include <math.h>
double f1(double a, double b, double c) {
return (a+b+c)/2;
}
double f2(double s, double a, double b, double c) {
return sqrt(s*(s-a)*(s-b)*(s-c));
}
int main(void){
double a, b, c, s;
printf("input a, b, c: ");
scanf("%lf%lf%lf", &a, &b, &c);
s = f1(a,b,c);
printf("s = %lf, area = %lf\n", s, f2(s,a,b,c));
return 0;
}
10.4 实验指导教材参考答案
一 、 调 试 示 例 ( 略 ) 二 、 基 础 编 程 题
(1)判断满足条件的三位数:编写一个函数,利用参数传入一个 3 位数 number,找出 101~number 之间所有满足下列两个条件的数:它是完全平方数,又有两位数字相同,如 144、676 等,函数返回找出这样的数据的个数,并编写主函数。
输入输出示例(括号内为说明文字)
150 (number=150)
count=2
解答:参见《C 语言程序设计》习题 10 中的三、程序设计题,第 1 题。
(2)递归求阶乘和:输入一个整数 n(n>0 且 n≤10),求 1!+2!+3!+...+n!。定义并调用函 数fact(n)计算 n!,函数类型是 double。试编写相应程序。
解答:参见《C 语言程序设计》习题 10 中的三、程序设计题,第 2 题。
(3)递归实现计算 x^n:输入双精度浮点数 x 和整数 n (n≥1),调用函数求 x 的 n 次方,
并保留两位小数。
输入输出示例(括号内为说明文字)
2 (x=2)
3 (n=3)
Root = 8.00
解答:参见《C 语言程序设计》习题 10 中的三、程序设计题,第 3 题。
(4)递归求式子和:用递归的方法对下列计算式子编写一个函数,并写出相应主函数,
计算结果保留两位小数。
2 3 4 1
( , ) ... ( 1)
n nf x n = − x x + x − x + + −
−× x
(n>0)输入输出示例(括号内为说明文字)
2 (x=2)
4 (n=4)
f(2,4) = -10
解答:参见《C 语言程序设计》习题 10 中的三、程序设计题,第 4 题。
(5)递归计算函数 Ack(m,n):输入两个整数 m 和 n(m≥0 且 n≥0),输出函数 Ack(m,n) 的值。Ack(m,n)定义为(m≥0 且 n≥0):
n + 1 m=0
Ack(m, n)= Ack(m - 1, 1) n=0 && m>0 Ack(m - 1, Ack(m, n - 1) ) m>0 && n>0 输入输出示例(括号内为说明文字)
2 (m=2)
3 (n=3)
Ack(2, 3)=9
解答:参见《C 语言程序设计》习题 10 中的三、程序设计题,第 5 题。
(6)递归实现求 Fabonacci 数列:用递归方法编写求斐波那契数列的函数,返回值为整型,
并写出相应的主函数。斐波那契数列的定义为:
f(0) = 0,f(1) = 1
f(n) = f(n - 2) + f(n - 1) (n>1)
输入输出示例(括号内为说明文字)
6 (n=6)
fib(6)=8
解答:参见《C 语言程序设计》习题 10 中的三、程序设计题,第 6 题。
三 、 改 错 题
改正下列程序中的错误,输入一个整数n (n≥0)和一个双精度浮点数 x,输出函数 P(n,x) 的值(保留2 位小数)。(源程序 error10_2.cpp)
1 (n=0) P(n, x) = x (n=1) ((2*n-1)*P(n-1,x)-(n-1)*P(n-2,x))/n (n>1) 输入输出示例
Enter n, x: 10 1.7 P(10,1.70) = 3.05
源程序(有错误的程序)
1 #include <stdio.h>
2 int main(void)
3 {
4 int n;
5 double x, result;
6
7 printf(“Enter n, x: ”);
8 scanf("%d%lf", &n, &x);
9 result = p(n,x);
10 printf("P(%d,%.2lf) = %.2lf\n", n, x, result);
11
12 return 0;
13 } 14
15 double p(int n, double x) 16 {
17 p(n,x) = ((2 * n - 1) * p(n - 1,x) - (n - 1) * p(n - 2,x)) / n;
18
19 return p(n,x);
20 }
(1)初次编译后共有 2 个[Error],请填写出错信息并分析原因。
① `p' undeclared (first use this function) ,函数p 未声明 ② non-lvalue in assignment 赋值语句中左边缺少变量
(2)请填写修改后的正确语句。
改错汇总:
错误行号: 6 正确语句: double p(int n, double x);
错误行号: 16 正确语句: double result;
错误行号: 17 正确语句: result = ((2 * n - 1) * p(n - 1,x) - (n - 1) * p(n - 2,x)) / n;
错误行号: 19 正确语句: return result;
(3)改正上述编译错误后,再次编译无错误出现,运行程序。
运行输入测试数据为 10 1.7,运行结果为: 程序错误,无输出 ,是否正确: 否