C语言(谭浩强)第4章模块化程序设计.ppt_第1页
C语言(谭浩强)第4章模块化程序设计.ppt_第2页
C语言(谭浩强)第4章模块化程序设计.ppt_第3页
C语言(谭浩强)第4章模块化程序设计.ppt_第4页
C语言(谭浩强)第4章模块化程序设计.ppt_第5页
已阅读5页,还剩55页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、第4章 模块化程序设计,函 数 变量的存储属性 模块的编译和链接 宏定义与宏替换,第4章 模块化程序设计,C语言是一种较现代的程序开发语言。它提供如下一些支持模块化软件开发的功能: (1) C语言用函数组织程序,在C语言程序中,一个程序由一个或多个程序文件组或,每一个程序文件就是一个程序模块,每一个程序模块由一个或多个函数组成。程序设计的任务就是设计一个个函数,并且确定它们之间的调用关系。在设计函数时要使每个函数都具有各自独立的功能和明显的界面。 (2) 通过给变量定义不同的存储类别,控制模块内部及外部的信息交换。 (3) 具有编译预处理功能,为程序的调试、移植提供了方便,也支持了模块化程序设

2、计。,4.1函 数,4.1.1 设计C语言程序就是设计函数 无论涉及的问题是复杂还是简单,规模是大还是小,用C语言设计程序,任务只有一种,就是编写函数,至少也要编写一个main函数。执行C程序也就是执行相应的main函数。即从main函数的第一个前花括号开始,依次执行后面的语句,直到最后的后花括号为止。 模块化程序设计有一个原则:就是每个模块的规模一般不能太大(有人认为要控制在4060行之间),以便于阅读,便于检查其中的错误。在C语言中,减少主函数规模的一项基本措施就是通过调用其它函数来实现主函数需要的一些功能。,4.1.2 函数结构,为了定义函数,必须首先了解函数的结构。一个C语言函数的结构

3、形式如下: 函数头 函数体 ,1. 函数头函数类型 函数名(形式参数表列) 一个函数的函数头的结构如下: (1)函数类型。指定函数值的类型,即函数返回值的类型。 (2)函数名。函数名必须采用合法的用户标识符。 (3)圆括号:在函数名后面的一对圆括号是“函数运算符”,表示进行函数运算,函数运算符具有很高的运算优先级别 (4)形式参数表。形式参数表由写在一对圆括号(函数运算符)中的一系列参数组成。每一个参数由一个类型符和一个参数名组成。参数名也应当是合法的用户关键字。函数可以没有参数,这时在函数运算符内写一个“void”,也可以空允白。,2. 函数体 函数体由一些语句组成。主要是三种类型的语句:

4、声明语句:声明在函数中要使用的变量等程序实体。 用来实现函数的功能的可执行语句:包括若干流程控制 语句和表达式语句,。 return语句。使流程返回到调用处。 这里主要介绍return语句的用法。 当函数执行到return语句时,将停止本函数的执行,将流程送回到调用处。同时,编译器将函数分为三类进行处理: 纯粹计算型函数,如squt()和sin()。这类函数将返回一个计算结果。 完成一项具体工作,返回完成的成败。如printf()执行成功时,返回显示的字节数;失败时,返回一个负整数。 只执行一个过程的函数,不产生返回值,其类型应定义为void。 C99规定,对于非void类型的函数,必须使用有

5、返回值的return语句。,int absolutevalue (int x) return (x=0?x:-x); void spc (int n) int i; for (i=0; in;i+)它 printf ( ); return; ,3. 函数中变量的作用域,作用域指的是一个程序段中的代码的作用范围,在一个函数中定义的变量只在本函数中有效,在其它函数中不能使用这个变量,因此说,该变量的作用域是它所在的函数(从定义该变量的行开始到函数末尾)。即使在不同的函数中定义了同名的变量,它们数也指的是不同的变量。,#include int func(int x) x=5; return x+3;

6、 int main(void) printf(x=%dn,x); return 0; 这个程序企图在main函数中使用func函数中的变量x。编译这个程序,系统将给出如下编译错误: c(13) : error C2065: x : undeclared identifier,4. 空函数,空函数是一个不产生任何有效操作的函数,但它却是一个合法的C函数。例如函数 void null (void) 就是一个空函数。 空函数多使用在模块化程序的设计或测试中。一般首先写好main函数,确定需要调用的函数,然后逐步编写这些函数,如果在有一些函数还未编写好时想对已有俩编好的函数进行调试,可以先用空函数(函

7、数名用将来使用的实际函数名,如sort)放在程序中原定的位置上,这样就可以调试程序的其它部分,等以后再逐步补上。,4.1.3 函数定义与函数声明,1. 函数定义 函数定义是按照C语言的语法规则引入新的函数,并提供如下信息: 函数的返回值类型(如果有); 参数的个数及类型和名称; 调用函数时要执行的代码; 函数的有效性。,2. 函数声明,函数声明是对所用到的函数的特征进行必要的声明。编译系统以函数声明中给出的信息为依据,对调用表达式进行检测,例如,形参与实参类型是否一致,使用函数返回值的类型是否正确,以保证调用表达式与函数之间的参数正确传递。 声明语句提供的必要信息包括:函数名,函数的类型,函数

8、参数的个数、排列次序以及每个参数的类型。函数声明的一般格式为: 类型标识符 函数名(类型标识符形参,类型标识符形参,);,设有一函数的定义为: double func (double a, int b, char c) /* 函数体*/ 与之相应的函数声明应为: double func (double x, int y, char z);/* 注意末尾的分号 */,4.1.4 虚实结合与传值调用,一个函数中的函数体,只有在该函数被调用时才会执行。在函数被调用时,将要进行如下两个操作: 将函数调用中的实际参数值传送给函数定义中的形式参数; 将流程从调用处转到被调用的函数的开头,开始执行函数体中的

9、代码。,1. 函数调用时的虚实结合,参数是函数调用时进行信息交换的载体。实参是调用函数中的变量,形参是被调函数中的变量。在函数调用过程中实现实参与形参的结合。这称为“虚实结合”。 float add( ); int main(void) float x=1.5, y=-5.7; printf (%f+%f=%fn, x,y, add(x,y); float add (unsigned int a, unsigned int b) printf(a=%u, b=%un, a,b); return (a+b); ,2. 传值调用的虚实结合过程,程序进行编译时,并不为形式参数分配存储空间。只有在被调

10、用时,形式参数才临时地占有存储空间,其过程如下: (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 (

11、a=%d, b=%dn, a,b); return 0; void swap (int x, int y) int temp; temp=x, x=y, y=temp; /* 交换变量的值 */ printf(x=%d,y=%dn,x,y); 执行结果: x=5,y=3 a=3,b=5,4.1.5 递归函数,由前面的学习已经知道,一个函数可以调用另一个函数。C语言还允许一个函数自己调用自己(直接地或间接地调用自己)。于是形成一种特殊的函数结构递归函数,并且,前者称为直接递归函数调用,后者称为间接递归调用函数。,递归计算n!的函数rfact( )。 通常,n的阶乘可以描述为: n!=12n 但是

12、,也可以描述为: n!= n(n-1)21 或者可以写为: n!= n(n-1)! 更一般的形式是:,这就形成一个递归表达式。这个递归表达式可以用下面的函数实现。 long rfact(int n) if (n0) printf(Negative argument to fact !n); exit (-1); else if (n=1) return (1); else return (n*rfact (n-1); /*自己调用自己*/ ,当n=5时rfact的其调用和回代过程。,汉诺塔(Tower of Hanoi)问题。,据传古代印度布拉玛庙里僧侣们玩一种称为汉诺塔的游戏,据说游戏结束就

13、标志着世界末日的到来。游戏的装置是一块铜板,上面有三根杆,最左杆自下而上、由大到小顺序串有64个金盘,呈一个塔形(图4.8)。游戏要求把左边杆上的金盘全部移到最右边的杆上,条件是一次只能够动一个盘,并且不允许大盘在小盘上面。容易推出,n个盘从一根杆移到另一根杆需要2n-1次,所以64个盘的移动次数为:264-1=18,446,744,073,709,511,615,这是一个天文数字,即使一台功能很强的现代计算机来解汉诺塔问题,每一微秒可能计算(不印出)一次移动,那么也需要几乎100万年。而如果每秒移动一次,则需近5800亿年。,下面设计一个模拟僧侣们移动盘子的算法。 假定僧侣们要把n个盘子按题

14、中的规定由a杆借助c杆移到b杆。模拟这一过程的算法称为hanoi(n,a,b,c)。那么,很自然的想法是:,第一步:先把上面的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 (*Pr

15、ogram 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, ,hanoi (n,a,b,c); return 0; void hanoi (int n, char a, char b, char c)/*汉诺塔问题*/ if (n0) hanoi (n-1, a,c,b); printf ( Move disc %d from pi

16、le %c to %cn, n,a,b); (n-1, c,b,a); ,4.2 变量的存储属性,变量是对程序中数据存储的抽象。如前所述,C语言程序中的变量都是有类型的,数据类型是变量的运算属性的抽象,决定了该变量的取值范围和可以施加的运算种类。除此之外,变量还有变量一些属性,例如: 一个变量在程序的哪个范围内是可以使用的变量的可用域; 它什么时候生成以及什么时候被撤消变量的生存期; 它存储在什么哪种类型的存储器中以及用什么机制进行存储变量的存储区。 这些都称为变量的存储属性。,4.2.1 变量的可用域和生存期,1. 局部变量和全局变量 变量的可用域是指一个变量的标识符在程序的哪个域内才是可以

17、被识别的,也称为可见(或可用)的。大体上可以分为两种:全局可用全局变量与局部可用局部变量。 局部变量是定义在一个程序块(用一对花括号括起的语句块)内的变量。程序块可能是一个函数体(主函数),也可能是一个循环体或是选择结构中的一个分支语句块,也可以是任何一个用花括号扩起的语句块。而全局变量是定义在函数之外的变量。一般说来,定义在什么范围的变量,其作用域就在那个范围,并且在从定义语句开始到这个域结束的范围(域)内被使用,在这个域之外是不可见的。 在C语言中,凡是声明在函数内部的变量都是局部变量。,2. 动态变量与静态变量,任何一个运行中的程序,在内存中都被分成代码区和数据区两大部分,而数据区又被分

18、为静态存储区、自动存储区和动态分配区等三部分。 自动存储区是按栈组织的存储区。除特别声明的外,局部变量通常被存放在栈区。这些变量在进入所在的块时被创建,所在的块结束时被撤销,当所在的块结束后,各变量的值不再保留。并且,变量必须有程序员显式地进行初始化,否则它们的初始值是不确定的。前面例 存储区是在程序编译时就分配的存储区,分配在静态存储区的变量在程序开始执行时被创建并自动初始化(数值变量被初始化为0),当程序结束时才被撤销。所以常称静态变量的生存期是永久的。全局变量就是被分配在静态存储区的。下面的例子说明将例4.8中的程序改用全局变量实现后的情形。,4.2.2 C语言中变量的存储类型,根据程序

19、中实际应用的需要,C语言将可用域和生存期整合成4种存储类型: 局部自动类型,在函数内部用标识符auto或register声明。 静态局部类型,在函数内部,使用static声明。 静态全局类型,在函数外部,使用static声明,也称静态外部变量。 全局类型,不需要标识符声明。在函数外部直接声明即可,通称外部变量。,auto存储类型和register存储类型 auto声明符的作用就是告诉编译器将变量分配在自动存储区。也就是说,使用auto声明的变量是局部变量。其格式为: auto 数据类型变量名=初值表达式,; 其中,方括号表示可省略,auto是自动变量的存储类别标识符。如果省略auto,系统隐含

20、认为此变量为auto。 函数的参数也是一种自动变量。 register的作用是用来声明寄存器存储自动变量。这类变量具有局部变量的所有特点。当把一个变量指定为寄存器存储类别时,系统就将它存放在CPU中的一个寄存器中。通常把使用频率较高的变量(如循环次数较多的循环变量)定义为register类别。,2. 静态局部类型,定义局部变量时,使用static修饰,可以在不改变局部量的可见域的前提下,使变量成为静态的,即当函数撤销后,变量的值还保留。或者说,这些变量的生存期是永久的,只是在变量的作用域外是不可见的。这样,避免了全局变量的值可以被多处修改将引起的副作用,又可以使函数基于前一次调用的值的基础上工

21、作。,#include void increment(void); int main(void) increment ( ); increment ( ); increment ( ); return 0; void increment(void) int x=0; /*auto*/ x+; printf (%dn,x); 运行结果: 1 1 1,#include void increment (void); int main(void) increment ( ); increment ( ); increment ( ); return 0; void increment (void) s

22、tatic int x=0; /*使用了static说明符 */ x+; printf (%dn,x); 运行结果: 1? 2 3,3. 静态全局类型,在多文件程序中,用static声明外部变量,这样,该外部变量的作用域仅限于所在的文件,而不用static声明的外部变量的作用域为整个程序。例如,某个程序中要用到大量函数,而有几个函数需要共同使用几个全局变量时,可以将这几个函数组织在一个文件中,而将这几个全局变量定义为静态的,以保证它们不会与其他文件中的变量发生名字冲突,保证文件的独立性。,4. 外部变量的定义与声明,严格地说,定义与声明是两个不相同的概念。声明的含义更广一些,定义的含义稍窄一些

23、,定义是声明的一种形式,定义具有分配存储的功能,凡是定义都属于声明,称为定义性声明(defining declaration)。另一种声明称为引用性声明(referencing declaration),它仅仅是对编译系统提供一些信息,声明并不都是定义。 前几章例题程序中定义变量的声明语句,是定义性声明,为方便,一般简称为定义。而函数声明就是引用性声明,它和函数定义的作用是不同的。一般把引用性声明简称为声明(狭义的声明)。对同一作用域中的同一变量只能定义一次,如果对同一变量定义多次,就会出现语法错误。在定义变量时可以用auto,static, register等关键字声明该变量的存储类型。,4

24、. 外部变量的定义与声明,对于外部变量(除了静态全局变量外),除了可以定义一次外部变量外,还可以多次进行声明(引用性声明)。 (1)通过声明将外部变量的作用域在本文件范围内扩充向前引用,通常,一个外部变量的作用域是从其定义点到本文件末尾。对于位于定义点之前的函数,可以用extern声明使变量的作用域扩充到需要用到它的函数。 (2)利用声明语句将作用域扩大到其他文件建立对象的外部链接 假设一个程序由两个以上的文件组成。当一个全局变量定义在文件file1中时,在另外的文件中使用extern声明,可以通知编译系统一个信息:“此变量到外部去找”。或者说在链接时,告诉链接器:“到别的文件中找这个变量的定

25、义”。,(4) 全局变量的副作用 前面已提到,如果没有外部变量,函数只能通过参数与外界发生数据联系,有了外部变量以后,增加了一条与外界传递数据的渠道。这种方法有利有弊。外部变量作为公共信息的一种载体,虽然给程序设计带来一些方便,但也会产生一些副作用,4.2.3 用const声明将变量存储在只读区,C语言除了可以为程序开辟栈区、堆区、静态区外,还可以开辟一个只读区。既然只读区是内存的一个区,所以数据是以变量的形式存储的,但它又是只读的,即不可修改的。所以这也就是把变量定义为只读变量。定义只读变量的方法是在声明变量时使用修饰符const。格式为: const 数据类型 变量1=初始表达式1,变量2

26、=初始表达式2,,4.3 模块的编译和链接,4.3.1 分别编译 C语言是一种支持模块化程序设计的语言,它允许将一个大型程序分成多个程序文件分别进行编译。这样的好处在于 当程序的一个局部有错误或对局部进行了修改时,可以只重新编译该局部,不需要将整个程序都重新编译。 某些经过考验的函数的目标代码可以添加为的库函数,供其他程序中使用。,4.3.2 用项目管理多文件程序的编译与链接过程,多文件程序的编译、链接过程是比较麻烦的。为了方便用户进行多文件程序的编译、链接,各种程序开发软件都提供了用项目(project)管理多文件程序的编译和链接过程。其使用方法因开发工具而异,请参考有关手册。,4.3.3

27、头文件,用户显式地保证程序一致性的基本方法是保持声明的一致性。保持声明一致性的简单办法是提供一个头文件,让头文件中的信息作为各模块之间的接口信息,有利于提供可重用的模块。使用头文件将把程序分为程序头和程序体两部分。 好的头文件应包含如下一些内容: 类型定义,如定义一个枚举类型enum color RED,BLUE,GREEN,YELLOW; 函数声明,如extern int strlen const char*; 嵌入函数声明,如inline char get() return *p+; 数据声明,如extern int a; 常量定义,如const float pi=3.141593; 包含

28、指令,如#include ,4.3.3 头文件,宏定义,如#define case break; case 注释 好的头文件不能包含以下内容: 一般函数定义 数据定义,如int a; 常量聚集定义,如 const tbl=/* */; 用户头文件是由用户自己定义的头文件。系统将用双引号方式进行搜索。程序员可以用它们建立各文件之间的信息窗口。 应当注意,修改了头文件时,所有涉及该头文件的程序都应重新编译,所以在编制头文件时应尽量考虑周全;另外,当使用很多头文件时,常常有重复嵌入的情形发生,在这种情形下,应防止标识符重复定义的错误。为避免这些错误,应使用条件编译来测试有无重复定义。,4.4 宏定义

29、与宏替换,编译预处理是以“#”开头的一些命令,它通知编译系统:在进行正式编译前应当先进行一些前期工作(即编译预处理)。目前,C语言的编译预处理功能主要有如下一些: (1)文件包含,使用命令:#include。 (2)宏定义,使用命令:#define、#undef。 (3)条件编译,使用命令:#if、#ifdef、#ifndef、#else、#endif、#elif、defind。 (4)提供行号信息,使用命令:#line。 (5)编译杂注,使用命令:#progma。 (6)错误信息,使用命令:#error。 在这些编译预处理中,应用最多的就是对宏的处理。它允许程序员使用define定义并应用宏

30、名字书写C语言程序,同时也指示编译器在正式编译前通过宏替换,使程序能按照C语言的语法进行编译。,4.4.1 字符串宏定义及其基本格式,define命令最简单的应用是将一个字符串定义为一个宏名,格式为: define 宏名 字符串 编译预处理时,预处理程序将把程序文件中在该宏定义之后的所有宏名,用#define命令中定义的字符串(称为宏体)替换,这个过程称为宏替换。符号常数的定义就是这种宏定义的一种应用。例如 #define PI 3.14159265 #define RADIUS 2.0 使用宏定义的好处是: (1) 提高了程序的可读性。如果人们看到以下语句 return (2.0* 3.14

31、15926536* 2.0);,和 return (3.1415926536* 2.0* 2.0); 是否能很快地看出它们在计算什么呢?显然不能。如果写成下面的形式 return (2.0* PI* RADIUS); 和 return (PI *RADIUS* RADIUS); 任何人一眼都可以看出:这是在计算圆周长和圆面积。使用宏定义可以提高可理解性。 (2) 易修改性好。如要将RADIUS的值由2.0修改为3.0,只要在define命令中修改一处便可。而在不使用宏定义的文件中,要将3处的2.0修改为3.0,而且很可能也错把circum函数中的第一个返回语句也修改成 return (3.0*

32、3.1415926536*3.0);,(3)宏定义可以给程序设计带来许多方便。如熟悉PASCAL的读者,完全可以用自己已习惯的风格来写程序。 #define BEGIN #define END #define AND 将不进行宏定义。 宏名中不能含有空格。例如想用“A NAME”定义“SMISS”,而写成:,#define A NAME SMISS 则实际进行的宏定义是A为宏名字,宏体是“NAME SMISS”。 C程序员一般都习惯用大写字母定义宏名字。这样的表示方法使宏名与变量名有明显的区别,以避免混淆。此外有助于快速识别要发生宏替换的位置,提高程序的可读性。 不能进行宏名字的重定义。 (3

33、)定义一个宏名字以后,就可以在后面使用这个宏名字了,包括在后面的宏定义中使用。例如求圆的周长和面积的程序可以改写为 #include ,#define PI3.1415926 #define R1.0 #define CIRCUM2.0*PI*R /* 使用了前面定义的R和PI */ #define AREAPI*R*R int main(void) printf(“The circum is %f and area is %fn”,CIRCUM,AREA); 这种宏定义称为嵌套宏定义。运行这个程序的结果为: The circum is 6.283185 and area is 3.14159

34、3 下面是另外一种形式的宏定义: #include #define PI 3.14159265 #define RADIUS 1.0 #define CIRCUM return(2.0*PI* RADIUS); /* 最后的分号是return语句的一部分 */,#define AREA return(PI* RADIUS* RADIUS); double circum( ) CIRCUM double area( ) AREA int main(void) printf(The circum is %f and area is %fn,circum(),area(); (4)不能进行的宏替换: 不可以替换作为用户标识符中的成分。例如,在例4.23中,不可以用“R”替换“CIRCUM”中的“R”。 不能替换字符串常量中的成分。,#include #define PI3.1415926 #define R1.0 #define CIRCUM2.0*PI*R #define AREAPI*R*R int main(void) printf(“The CIRCUM is %f and AREA is %fn”,CIRCUM,AREA); 运行结果为: The CIRCUM is

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论