本文由瓦斯202贡献
ppt文档可能在WAP端浏览体验不佳。建议您优先选择TXT,或下载源文件到本机查看。
第4章 模块化程序设计
●函 数 ●变量的存储属性 ● 模块的编译和链接 ● 宏定义与宏替换
第4章 模块化程序设计
C语言是一种较现代的程序开发语言。它提 供如下一些支持模块化软件开发的功能: (1) C语言用函数组织程序,在C语言程序中, 一个程序由一个或多个程序文件组或,每一个程 序文件就是一个程序模块,每一个程序模块由一 个或多个函数组成。程序设计的任务就是设计一 个个函数,并且确定它们之间的调用关系。在设 计函数时要使每个函数都具有各自独立的功能和 明显的界面。 (2) 通过给变量定义不同的存储类别,控制模 块内部及外部的信息交换。 (3) 具有编译预处理功能,为程序的调试、移 植提供了方便,也支持了模块化程序设计。
4.1函 4.1函
4.1.1 设计 语言程序就是设计函数 设计C语言程序就是设计函数 无论涉及的问题是复杂还是简单,规模是大 还是小,用C语言设计程序,任务只有一种,就 是编写函数,至少也要编写一个main函数。执行C 程序也就是执行相应的main函数。即从main函数 的第一个前花括号开始,依次执行后面的语句, 直到最后的后花括号为止。 模块化程序设计有一个原则:就是每个模块 的规模一般不能太大(有人认为要控制在40~60行 之间),以便于阅读,便于检查其中的错误。在 C语言中,减少主函数规模的一项基本措施就是 通过调用其它函数来实现主函数需要的一些功能。
f1() { main(void) { ┇ f1(); ┇ f2(); ┇ return 0; } ┇ ┇ } f11();
f11() { ┇ } f21() { ┇ } f22( );┇ f22() { ┇ }
操 作 系 统
f1() { ┇ ┇ } main() 参数 返回 参数 f1() 返回 f2() 参数 f21();
参数 返回 f11()
返回 f21()
参数
返回 f22()
4.1.2 函数结构
为了定义函数,必须首先了解函数的结 构。一个C语言函数的结构形式如下: 函数头 { 函数体 }
1. 函数头函数类型 函数名(形式参数表列) 一个函数的函数头的结构如下: (1)函数类型。指定函数值的类型,即函数返 回值的类型。 (2)函数名。函数名必须采用合法的用户标识 符。 (3)圆括号:在函数名后面的一对圆括号是 “函数运算符”,表示进行函数运算,函数运算符 具有很高的运算优先级别 (4)形式参数表。形式参数表由写在一对圆括 号(函数运算符)中的一系列参数组成。每一个参数 由一个类型符和一个参数名组成。参数名也应当是 合法的用户关键字。函数可以没有参数,这时在函 数运算符内写一个“void”,也可以
空允白。
2. 函数体 函数体由一些语句组成。主要是三种类型的语句: ? 声明语句:声明在函数中要使用的变量等程序实体。 ? 用来实现函数的功能的可执行语句:包括若干流程控制 语句和表达式语句,。 ? return语句。使流程返回到调用处。 这里主要介绍return语句的用法。 当函数执行到return语句时,将停止本函数的执行, 将流程送回到调用处。同时,编译器将函数分为三类进行 处理: ? 纯粹计算型函数,如s
qut()和sin()。这类函数将返回一 个计算结果。 ? 完成一项具体工作,返回完成的成败。如printf()执行成 功时,返回显示的字节数;失败时,返回一个负整数。 ? 只执行一个过程的函数,不产生返回值,其类型应定义 为void。 C99规定,对于非void类型的函数,必须使用有返回 值的return语句。
int absolutevalue (int x) { return (x>=0?x:-x); } void spc (int n) { int i; for (i=0; i<n;i++)它 printf (″ ″); return; }
3. 函数中变量的作用域
作用域指的是一个程序段中的代码的 作用范围,在一个函数中定义的变量只在 本函数中有效,在其它函数中不能使用这 个变量,因此说,该变量的作用域是它所 在的函数(从定义该变量的行开始到函数末 尾)。即使在不同的函数中定义了同名的变 量,它们数也指的是不同的变量。
#include  int func(int x) { x=5; return x+3; } int main(void) { printf("x=%d\n",x); return 0; } 这个程序企图在main函数中使用func函数中的 变量x。编译这个程序,系统将给出如下编译错误: c(13) : error C2065: 'x' : undeclared identifier
4. 空函数
空函数是一个不产生任何有效操作的函数,但 它却是一个合法的C函数。例如函数 void null (void){}
就是一个空函数。 空函数多使用在模块化程序的设计或测试中。 一般首先写好main函数,确定需要调用的函数, 然后逐步编写这些函数,如果在有一些函数还未 编写好时想对已有俩编好的函数进行调试,可以 先用空函数(函数名用将来使用的实际函数名,如 sort)放在程序中原定的位置上,这样就可以调试 程序的其它部分,等以后再逐步补上。
4.1.3 函数定义与函数声明
1. 函数定义 函数定义是按照C语言的语法规则引入新的函 数,并提供如下信息: ? 函数的返回值类型(如果有); ? 参数的个数及类型和名称; ? 调用函数时要执行的代码; ? 函数的有效性。
2. 函数声明
函数声明是对所用到的函数的特征进行 必要的声明。编译系统以函数声明中给出 的信息为依据,对调用表达式进行检测, 例如,形参与实参类型是否一致,使用函 数返回值的类型是否正确,以保证调用表 达式与函数之间的参数正确
传递。 声明语句提供的必要信息包括:函数名, 函数的类型,函数参数的个数、排列次序 以及每个参数的类型。函数声明的一般格 式为:
类型标识符 函数名(类型标识符形参,类型标 识符形参,…);
设有一函数的定义为: double func (double a, int b, char c) { /* 函数体*/ } 与之相应的函数声明应为: double func (double x, int y, char z); 注意末尾的分号 */
/*
4.1.4 虚实结合与传值调用
一个函数中的函数体,只有在该函数 被调用时才会执行。在函数被调用时,将 要进行如下两个操作: ? 将函数调用中的实际参数值传送给函数定 义中的形式参数; ? 将流程从调用处转到被调用的函数的开头, 开始执行函数体中的代码。
1. 函数调用时的虚实结合
参数是函数调用时进行信息交换的载体。实参是调 用函数中的变量,形参是被调函数中的变量。在函数调用 过程中实现实参与形参的结合。这称为“虚实结合”。 float add( ); int main(void) { float x=1.5, y=-5.7; printf (″%f+%f=%f\n″, x,y, add(x,y)); } float add (unsigned int a, unsigned int b) { printf(″a=%u, b=%u\n″, a,b); return (a+b); }
2. 传值调用的虚实结合过程
程序进行编译时,并不为形式参数分配存储 空间。只有在被调用时,形式参数才临时地占有 存储空间,其过程如下: (1)调用开始,系统为形参开辟一个临时存 储区,形参与实参各占一个独立的存储空间。 (2)然后将各实参之值传递给形参,这时形 参就得到了实参的值。这种虚实结合方式称为 “值结合”。 (3)函数返回时,临时存储区也被撤销。 要特别注意在C程序中实参与形参结合的传值 传值 调用(call by value) 的特点。即函数中对形参变量 调用 的操作不会影响到调用函数中的实参变量,即形 参值不能传回给实参。
#include  void swap (int x, int y); int main(void) { int a=3, b=5; swap (a,b); printf (″a=%d, b=%d\n″, a,b); \ return 0; } void swap (int x, int y) { int temp; temp=x, x=y, y=temp; printf(″x=%d,y=%d\n″,x,y); \ } 执行结果: 执行结果: x=5,y=3 a=3,b=5
/* 交换变量的值 */
c程序是什么程序设计语言4.1.5 递归函数 由前面的学习已经知道,一个函数可 以调用另一个函数。C语言还允许一个函数 自己调用自己(直接地或间接地调用自己)。 于是形成一种特殊的函数结构——递归函 数,并且,前者称为直接递归函数调用, 后者称为间接递归调用函数。
递归计算n!的函数rfact( )。 通常,n的阶乘可以描述为: n!=1·2·…·n 但是,也可以描述为: n!= n·(n-1)·…·2·1 或者可以写为: n!= n·(n-1)! 更一般的形式是:
1 n!= (x≤1)
n·(n-1)! (x>1) ( )
这就形成一个
递归表达式。这个递归表达式可以用下 面的函数实现。 long rfact(int n) { if (n<0) { printf(″Negative argument to fact !\n″); exit (-1); } else if (n<=1) return (1); else return (n*rfact (n-1)); /*自己调用自己*/ }
当n=5时rfact的其调用和回代过程。 n=5时rfact的其调用和回代过程
rfact(5) 5*rfact(4) 4*rfact(3) 3*rfact(2) 2*rfact(1) 1 120 5*24 4*6 3*2 2*1
汉诺塔(Tower 汉诺塔(Tower of Hanoi)问题。 Hanoi)问题。
据传古代印度布拉玛庙里僧侣们玩一种称为汉 诺塔的游戏,据说游戏结束就标志着世界末日的到 来。游戏的装置是一块铜板,上面有三根杆,最左 杆自下而上、由大到小顺序串有64个金盘,呈一个 塔形(图4.8)。游戏要求把左边杆上的金盘全部移到 最右边的杆上,条件是一次只能够动一个盘,并且 不允许大盘在小盘上面。容易推出,n个盘从一根 杆移到另一根杆需要2n-1次,所以64个盘的移动次 数为:
264-1=18,446,744,073,709,511,615,这是 一个天文数字,即使一台功能很强的现代计算机来 解汉诺塔问题,每一微秒可能计算(不印出)一次移 动,那么也需要几乎100万年。而如果每秒移动一 次,则需近5800亿年。
a
b
c
① no1 no2 no3 ② no4 no4 ③ no1 no2 no3
下面设计一个模拟僧侣们移动盘子的算法。 假定僧侣们要把n个盘子按题中的规定由a杆借 助c杆移到b杆。模拟这一过程的算法称为 hanoi(n,a,b,c)。那么,很自然的想法是:
a
b
c
① no1 no2 no3 ② no4 no4 ③ no1 no2 no3
第一步:先把上面的n-1个盘子设法借助b杆放到c杆, 如图4.8中的箭头①所示,记做hanoi(n-1, a,c,b)。 第二步:把第n个盘子从a杆直接移到b杆,如图4.8中的 箭头②所示。 第三步:把c杆上的n-1个盘子借助a杆移到b杆,如图4.8 中的箭头③所示,记做hanoi(n-1,c,b,a)。
#include  void hanoi(int n, char a, char b ,char c); int main(void) { int n; printf ("*************************************\n"); printf ("*Program for simulating the solution*\n"); printf ("** of the game of 'tower of Hanoi' **\n"); printf ("*************************************\n"); printf ("Please enter the number of disks to be moved:"); scanf ("%d", &n);
hanoi (n,'a','b','c'); return 0; } void hanoi (int n, char a, char b, char c)/***汉诺塔问题 ***/ { if (n>0) { hanoi (n-1, a,c,b); printf (" Move disc %d from pile %c to %c\n", n,a,b); (n-1, c,b,a); } }
h(3,a,b,c) { h(2,a,c,b); no3:a->b; h(2,c,b,a); }
h(2,a,b,c) { h(1,a,b,c); no2:a->c; h(1,b,c,a); }
h(2,c,b,a) { h(1,c,a,b); no2:c->b; h(1,a,b,c); }
h(1,a,b,c) { h(0,a,c,b); no0:a->b; h(0,c,b,a); } h(1,b,c,a) { h(0,b,a,c); no1:b->c; h(0,a,c,b); } h(1,c,a,b) { h(0,c,b,a); no1:c->a; h(0,b,a,c); } h(1,a,b,c) { h(0,a,c,b); no1:a->b; h(0,c,b,a); }
4.2 变量的存储属性
变量是
对程序中数据存储的抽象。如前所述, C语言程序中的变量都是有类型的,数据类型是变 量的运算属性的抽象,决定了该变量的取值范围和 可以施加的运算种类。除此之外,变量还有变量一 些属性,例如: ? 一个变量在程序的哪个范围内是可以使用的——变 量的可用域; ? 它什么时候生成以及什么时候被撤消——变量的生 存期; ? 它存储在什么哪种类型的存储器中以及用什么机制 进行存储——变量的存储区。 这些都称为变量的存储属性。
4.2.1 变量的可用域和生存期
1. 局部变量和全局变量 变量的可用域是指一个变量的标识符在程序的哪个域 内才是可以被识别的,也称为可见(或可用)的。大体上 可见( 可见 可以分为两种:全局可用——全局变量 全局变量与局部可用——局 全局变量 局 部变量。 部变量 局部变量是定义在一个程序块(用一对花括号括起的 语句块)内的变量。程序块可能是一个函数体(主函数), 也可能是一个循环体或是选择结构中的一个分支语句
块, 也可以是任何一个用花括号扩起的语句块。而全局变量是 定义在函数之外的变量。一般说来,定义在什么范围的变 量,其作用域就在那个范围,并且在从定义语句开始到这 个域结束的范围(域)内被使用,在这个域之外是不可见 的。 在C语言中,凡是声明在函数内部的变量都是局部变 量。
#include  int main(void) { /* printf("a1=%d",a); */ int a=3; printf("a2=%d",a); { int b=5; printf("b1=%d",b); } /* printf("b2=%d",b); */ printf("a3=%d",a); return 0; } 则运行可以得到如下结果: a2=3 b1=5 a3=3
b的作用 域
a的作用 域
2. 动态变量与静态变量
任何一个运行中的程序,在内存中都被分成代码区和 数据区两大部分,而数据区又被分为静态存储区、自动存 储区和动态分配区等三部分。 自动存储区是按栈组织的存储区。除特别声明的外, 局部变量通常被存放在栈区。这些变量在进入所在的块时 被创建,所在的块结束时被撤销,当所在的块结束后,各 变量的值不再保留。并且,变量必须有程序员显式地进行 初始化,否则它们的初始值是不确定的。前面例 存储区是在程序编译时就分配的存储区,分配在静态 存储区的变量在程序开始执行时被创建并自动初始化(数 值变量被初始化为0),当程序结束时才被撤销。所以常 称静态变量的生存期是永久的。全局变量就是被分配在静 态存储区的。下面的例子说明将例4.8中的程序改用全局变 量实现后的情形。
4.2.2 C语言中变量的存储类型 C语言中变量的存储类型
根据程序中实际应用的需要,C语言将 可用域和生存期整合成4种存储类型:
? 局部自动类型,在函数内部用标识符auto 或register声明。 ? 静态局部类型,在函数内部,使用static 声明。 ? 静态全局类型,在函数外部,使用static 声明,也称静态外部变量。 ? 全局类型,不需要标识符声明。在函数外 部直接声明即可,通称外部变量。
1. auto存储类型和register存储类型 auto声明符的作用就是告诉编译器将变量分配 在自动存储区。也就是说,使用auto声明的变量是 局部变量。其格式为: [auto] 数据类型变量名[=初值表达式],…; 其中,方括号表示可省略,auto是自动变量的存 auto, 储类别标识符。如果省略auto,系统隐含认为此变量 为auto。 auto 函数的参数也是一种自动变量。 register的作用是用来声明寄存器存储自动变量 寄存器存储自动变量。 寄存器存储自动变量 这类变量具有局部变量的所有特点。当把一个变 量指定为寄存器存储类别时,系统就将它存放在 CPU中的一个寄存器中。通常把使用频率较高的变 量(如循环次数较多的循环变量)定义为register类别。
2. 静态局部类型
定义局部变量时,使用static修饰,可 以在不改变局部量的可见域的前提下,使 变量成为静态的,即当
函数撤销后,变量 的值还保留。或者说,这些变量的生存期 是永久的,只是在变量的作用域外是不可 见的。这样,避免了全局变量的值可以被 多处修改将引起的副作用,又可以使函数 基于前一次调用的值的基础上工作。
#include  void increment(void); int main(void) { increment ( ); ; increment ( ); ; increment ( ); ; return 0; } void increment(void) { int x=0; /*auto*/ x++; printf (″%d\n″,x); } 运行结果: 运行结果: 1 1 1
#include  void increment (void); int main(void) { increment ( ); ; increment ( ); ; increment ( ); ; return 0; } void increment (void) { static int x=0; x++; printf (″%d\n″,x); } 运行结果: 运行结果: 1? 2 3
/*使用了 使用了static说明符 */ 说明符 使用了
3. 静态全局类型
在多文件程序中,用static声明外部变 量,这样,该外部变量的作用域仅限于所 在的文件,而不用static声明的外部变量的 作用域为整个程序。例如,某个程序中要 用到大量函数,而有几个函数需要共同使 用几个全局变量时,可以将这几个函数组 织在一个文件中,而将这几个全局变量定 义为静态的,以保证它们不会与其他文件 中的变量发生名字冲突,保证文件的独立 性。
4. 外部变量的定义与声明
严格地说,定义与声明是两个不相同的概念。 声明的含义更广一些,定义的含义稍窄一些,定 义是声明的一种形式,定义具有分配存储的功能, 凡是定义都属于声明,称为定义