1、程序设计技术,C语言数据描述和C程序设计初步 结构化程序设计基础和C语言的控制结构 数组及其应用 函数与C程序结构 指针与函数 指针与数组 字符串及其应用 结构体类型和联合体类型 C语言的文件处理及其应用 位运算与枚举类型,函数与程序结构,函数的定义和调用 函数的嵌套调用和递归调用 变量的作用域和生存期 编译预处理 多源文件C程序的组织方法,函数与程序结构,模块化程序设计技术就是通过开发和维护一些小的程序块(即模块)的方法构建一个大型程序,是人类解决较大的复杂问题所采用的一种“分而治之”的策略。本章主要讨论C语言实现模块化程序设计技术的手段以及在模块化实现过程中所遇到的一系列问题。,41 函数
2、的定义和调用,C程序的一般结构,41 函数的定义和调用,在C+语言程序中的若干个函数中必须有一个且只能有一个函数成为主函数。程序的执行从main函数开始,调用其他函数后流程回到main函数,在main函数中结束整个程序的运行。在一个函数中可以使用另一个函数的功能,这成为函数调用。,411 函数的定义和声明,函数的定义不但要能够表达出其所描述的模块功能,还必须具体描述出如何实现所定义的模块功能。同时函数定义中还必须描述出函数的三个特征,即函数的名字、函数的参数表以及函数的返回值类型。,411 函数的定义和声明,C语言函数定义的现代风格形式如下: 返回值类型说明符 函数名(形式参数表及其说明)/函
3、数头 函数的操作对象(数据)定义和说明部分 /函数体函数的执行语句部分,411 函数的定义和声明,求阶乘的C程序如下所示:#include void main() int i, n;long fact=1;printf(“Input n:“); scanf(“%d“, ,程序实现了计算从键盘输入一个整数n,并求其阶乘的功能。,411 函数的定义和声明,411 函数的定义和声明,(2)函数执行结果的返回和返回值类型的确定 返回值用关键字return组成,如:return ; 注意函数执行结果是用类型名作为关键字在函数的头部予以确定。,long为函数返回值类型,Return语句将n!返回给调用函数
4、,花括号给函数确定了边界区域,long factorial( ) int i,n;long fact=1;printf(“Input n:“); scanf(“%d“,411 函数的定义和声明,(3)函数的参数表设计 两个步骤来实现: 一是将函数内部用于从键盘上接收数据的数据对象定义移到函数的形式参数表中; 二是删去函数中从键盘获取数据的语句。函数factorial可以改造为如下形式:,n的值函数调用者(使用者)处获取,long factorial( int n) int i;long fact=1;for(i=1;i=n;i+)fact*=i;return fact; ,411 函数的定义和
5、声明,C函数定义一般形式中函数组成成分的确切含义: (1)返回值类型说明符用以制定函数返回值的数据类型。若一个函数无返回值,则返回值数据类型应定义为void。 (2)函数的名字函数的名字也是一种标识符,必须遵循C命名规则。主函数只能命为main,其他可自给定,做到“见名知意”。 (3)形式参数表函数的形式参数表用圆括号括起来的、由零个到多个形式参数的定义组成,两个形式参数之间用逗号分隔。若一个函数没有形式参数,作为函数运算符使用的圆括号也不能省略。,411 函数的定义和声明,(4)return ;语句 如果返回值不是void,则函数定义中必有return ;语句。当函数执行到该C语句时,先计算
6、该语句中的表达式的值,然后再将该值强制转化为指定的函数返回值的数据类型,返回到主调函数中。 如果返回值是void,则函数定义中可以没有用return构成的语句,若函数需要使用return语句,则其形式只能是:return;。,411 函数的定义和声明,C语言中规定,在一个函数的内部不能定义其他函数(即函数不能嵌套定义)。这个规定保证了每个函数都是一个相对独立的程序模块。 在由多个函数组成的C程序中,各个函数的定义是并列的并且顺序是任意的,函数在一个C程序中的定义顺序与执行顺序无关。,411 函数的定义和声明,函数的声明 根据C语言的规定,函数也要先定义后使用。即一个函数能够被调用,它必须是一个
7、已经定义好(已经存在)的函数,而且必须在调用之前使用某种方式向系统描述所调用函数的基本特征,这也就是函数声明的作用。C语言中的函数分为标准库函数和用户自定义函数两大类。,411 函数的定义和声明,(1)标准库函数的声明方式使用标准库函数时,由于系统提供的标准库函数的说明都分门别类集中在一些称为“头文件”的文本文件中,所以在程序中如果要调用系统标准库函数,也要在程序的适当位置使用编译预处理语句来进行声明。,411 函数的定义和声明,其使用形式为: #include 当使用尖括号时,指定系统首先查找C编译系统配置的头文件路径(include路径); #include “头文件名” 当使用双引号时,
8、指定系统首先查找当前目录。,411 函数的定义和声明,(2)用户自定函数的声明方式对于用户自定义函数,如果被调用函数(称为被调函数)与调用它的函数(称为主调函数)在同一源文件中,需要在函数调用之前对被调函数进行声明。函数声明的作用是在调用之前向系统描述所调用函数的基本特征,所以函数声明一般形式为:返回值类型说明符 函数名(形式参数表及其说明);,411 函数的定义和声明,/*Name: ex04-01.cpp*/ #include void main() long factorial(int n); int num;printf(“Input the num: “);scanf(“%d“, ,
9、函数声明告诉编译系统factorial是一个返回值是long,只有一个int参数的函数,程序演示,411 函数的定义和声明,在上面程序中,主函数中的long factorial(int n);语句就是对函数factorial的声明。C程序中,对被调函数的声明也可以书写在主调函数定义之前,这种方式下函数声明语句之后的所有函数都能对被声明函数进行调用,如下面的程序段所示: #include long factorial(int n);/*对函数factorial的声明*/ void main() ,411 函数的定义和声明,在函数的声明语句中,形式参数变量的名字是无关紧要的(可以与函数定义中的不同
10、甚至可以缺省),函数声明语句的关键是形式参数的类型、个数和次序必须与所声明的函数定义相同。例如上面对函数factorial的声明语句还可以写成为如下两种形式: long factorial(int);/*对函数factorial的声明中无形式参数名*/ long factorial(int x);/*对函数factorial的声明中形式参数名与函数定义不同*/,411 函数的定义和声明,C语言规定在下列情况下可以不对被调函数进行声明: 1.被调函数的返回值数据类型是整型或字符型在这种情况下,系统自动按整型进行隐式声明。但从现代程序设计技术的观点出发,对任何类型的函数在调用之前都必须声明,所以许
11、多较现代的C编译系统在这种情况下仍然强制要求对被调函数进行声明。 2. 被调函数的定义出现在主调函数之前在这种情况下,系统在执行程序中的函数调用语句之前已知道了被调函数的所有特征。,411 函数的定义和声明,/*Name: ex04-02.cpp*/ #include long factorial(int n)/函数factorial的定义出现在主调函数main的前面 int i;long fact=1;for(i=1;i=n;i+)fact*=i;return fact; void main() /主函数中没有对函数factorial进行声明的语句 int num;printf(“Input
12、 the num: “);scanf(“%d“, ,程序演示,412 值参数传递的函数调用,程序执行时一个函数调用另外一个函数以完成某一特定功能的过程称之为函数调用。在函数的调用关系中,调用者称为主调函数,被调者称为被调函数。C语言中,函数调用时必须提供函数的名字,如果函数是有参函数还必须同时提供传递给被调函数形式参数的实际参数,函数调用的一般形式为:函数名(实际参数表),412 值参数传递的函数调用,C程序中对函数的调用方式有三种:函数语句方式在这种调用函数的方式中,将函数调用作为一个单独的语句,此种方式主要对应于返回值为空类型(void)的函数调用。如果使用函数语句的方式调用一个返回值类型
13、为非void类型的函数,则表示程序中对函数的返回值不予使用。函数表达式方式在函数调用的这种方式下,函数调用出现在一个表达式中,这个表达式亦称为函数表达式。此时要求函数被调用后必须要返回一个确定的值以参加表达式运算。需要注意的是,返回值类型为空类型(void)的函数不能用该方式调用。函数参数方式在函数调用的这种方式下,函数调用作为另外一个函数调用的实际参数出现。此时要求函数被调用后必须要返回一个确定的值以作为其外层函数调用的实际参数。仍然需要注意,返回值类型为空类型(void)的函数不能用该方式调用。,412 值参数传递的函数调用,当被调函数是有参函数时,函数的调用必然伴随着参数传递。在C程序函
14、数调用的数据传递中,传递的是实际参数所具有的值。当实际参数是常量、变量或函数调用时,传递的数据就是这些数据对象所具有的内容,这种方式亦称为传数据值方式。,412 值参数传递的函数调用,如果函数调用时所传递的实际参数是数据对象在内存中存储的首地址值,则称之为传地址值方式,对于指针参数和数组参数就是使用的传地址值调用方式,将分别在本章的4.1.3和4.1.4小节中予以讨论。,412 值参数传递的函数调用,无论函数调用时的传递时数值值还是地址值,函数调用的执行过程都可以分为下面四个步骤: (1)系统为被调函数中的局部变量分配存储; (2)如果是有参函数调用则进行参数传递,主调函数将实际参数值传递给被
15、调函数的形式参数,传递时要保证参数的个数、类型、位置等一一对应; (3)程序执行的控制流程转移到被调函数执行; (4)执行完被调函数后,程序执行的控制流程以及被调函数的执行结果返回到主调函数中的调用点。,412 值参数传递的函数调用,函数的传数据值调用方式是一种数据复制的方式,在这种方式下,实际参数值通过复制的方式传递给形式参数,传递方(主调函数)中的原始数据和接受方(被调函数)中的数据复制品各自占用内存中不同的存储单元,当数据传递过程结束后,它们是互不相干的,因此被传递的数据在被调函数中无论怎样变化,都不会影响该数据在主调函数中的值。,412 值参数传递的函数调用,例4.3的程序讨论函数调用
16、的执行过程,为了讨论方便为程序加上行号。 1 /*ex04-03.cpp*/ 2 #include 3 void main() 4 void swap(int x, int y); 5 int a=3,b=5; 6 printf(“swap调用前:a=%d,b=%dn“,a,b); 7 swap(a,b); 8 printf(“swap调用后:a=%d,b=%dn“,a,b); 9 10 void swap(int x, int y) 11 int t; 12 t=x,x=y,y=t; 13 printf(“swap调用中:x=%d,y=%dn“,x,y); 14 ,412 值参数传递的函数调
17、用,C程序执行时,函数在被调用之前其形式参数表中的形式参数变量和函数体中定义的普通变量在系统中都是不存在的,它们在系统中出现或消失与函数调用的过程有着密切的关系,在例4.3程序执行到第7行之前,函数swap中的形参变量x和y以及函数体中定义的变量t在系统中均不存在,参见图4.2a)。,412 值参数传递的函数调用,函数swap传数据值调用的过程如下: 系统为被调函数中的局部变量分配存储。如在例4.3程序中,程序执行到第7行时系统才会创建变量x、y和t(即为这些变量分配存储),参见图4.2b)。,412 值参数传递的函数调用,(2)参数传递。传递参数值实质上是将实参变量的内容拷贝给形式参数变量,
18、一旦拷贝完成则实际参数与形式参数就没有任何关系。在例4.3程序中,传递参数时将实参变量a的值拷贝给形参变量x,将使参变量b的值拷贝给形参变量y,拷贝完成后实参变量a、b与形参变量x、y就断开联系,参见图4.2c)。,412 值参数传递的函数调用,(3)控制流程转移到被调函数执行。在例4.3程序中,参数调用完成后程序的控制流程(执行顺序)就从第7行转移到第12行开始执行函数swap,参见图4.2c)d)e)。,412 值参数传递的函数调用,(4)控制流程返回主调函数。在例4.3程序中,程序执行到第14行时将控制流程返回到第7行的函数调用点后。与此同时,调用swap函数时创建的变量x、y和t都自动
19、被系统撤消。程序控制流程执行到被调函数中的return语句或函数体的函数的最后一个右花括号“”时,将程序执行的控制流程以及被调函数的执行结果返回到主调函数中的调用点。特别需要注意的是,随着程序控制流程的返回,系统会自动收回为被调函数的形式参数和局部变量分配的存储单元,即在函数被调用时创建的形式参数和局部变量会自动撤销。,412 值参数传递的函数调用,从上面的分析可以得到,虽然在swap函数内部对变量x、y的值进行了交换,但这种交换对函数调用时的实际参数变量a和b没有任何影响。程序执行的结果如下所示: swap调用前:a=3,b=5 swap调用中:x=5,y=3 swap调用后:a=3,b=5
20、,程序演示,4.1.3 指针基本概念和地址值参数传递函数调用,如果需要在被调函数中对主调函数中实际参数进行操作,则需要将主调函数中实际参数在内存中存放的地址起始值传递给被调函数对应的形式参数。这种方式下,被调函数中用于接收对应地址值的形式参数需要使用指针变量。,4.1.3 指针基本概念和地址值参数传递函数调用,本小节主要讨论指针变量的基本用法和实际参数值是地址值时的函数调用问题,4.1.3 指针基本概念和地址值参数传递函数调用,1指针和指针变量的概念程序中的任何数据对象在运行过程中一旦被使用,就会对应计算机系统内存中的一个地址。由于系统内存储器是按字节编址的,一个数据对象有可能占用一至若干个字
21、节的存储单元,在程序设计语言中一般将数据对象的名字与其所占用的存储单元的首地址相对应。在计算机系统中,内存单元的地址是用有序整型数进行编址的,所以存储系统的地址序号本质上就是无符号的整型数据。,4.1.3 指针基本概念和地址值参数传递函数调用,在C语言中需要注意的是,一些数据对象如函数、数组等的名字直接与其所占存储单元首地址对应,即它们的名字本身就直接表示地址;而一般意义下的变量名字则直接对应的是它们的内容(值),需要使用特定的表示方法才能表示出它们所对应的地址。,4.1.3 指针基本概念和地址值参数传递函数调用,C语言通过使用指针的概念来表示数据对象的首地址,所以在C语言中数据对象的地址和数
22、据对象的指针是一个相同的概念,即指针就是地址。所谓指针变量,就是其值是某数据对象指针(地址)的变量,当一个指针变量的内容是某个数据对象的首地址时,称为该指针变量指向这个数据对象(在不混淆的情况下,通常也可以说成指针指向数据对象)。,4.1.3 指针基本概念和地址值参数传递函数调用,指针变量定义:在定义指针变量时除了需要为其取名外,还必须指定该指针变量能够指向的数据对象的数据类型,定义指针变量的一般形式为:数据类型符 *指针变量名1,*指针变量名2,;其中,数据类型符是指针变量所指向目标数据对象的数据类型,可以是基本数据类型、也可以是以后要讨论到的构造数据类型;指针变量名由程序员命名,命名规则与
23、普通变量相同;在指针变量名之前的星号(*)只是一个标志,表示其后紧跟的变量是一个指针变量而不是一个普通变量。,4.1.3 指针基本概念和地址值参数传递函数调用,例如: int *p,*y; 定义了两个整型的指针变量p和y,注意指针变量是p和y,而不是*p和*y 如果有需要,指针变量也可以和同类型的普通变量混合定义。例如: char ch1,ch2,*p; 定义了两个字符变量ch1、ch2以及一个指针变量,4.1.3 指针基本概念和地址值参数传递函数调用,虽然地址量本质上是一个无符号整型常量,但C语言规定除了符号常量NULL外不能直接将任何其它常量赋值给指针变量。指针变量赋值的方法有两种:一种是
24、使用赋值号的方式;另外一种是初始化方式。无论使用哪种方式为指针变量赋值,在获取被指针变量指向的变量所对应的地址值时要使用C语言中提供的取地址运算符“&”,取出一个变量所对应的地址值的形式如为:&例如,有变量x,则&x表示变量x所对应存储单元的首地址。,4.1.3 指针基本概念和地址值参数传递函数调用,指针变量在定义时进行初始化的一般形式为:数据类型符 *指针变量名=初始化地址值; 指针变量赋值的一般形式为:指针变量名=地址值;,4.1.3 指针基本概念和地址值参数传递函数调用,int x,*y= /*将变量x的首地址赋值给指针变量y*/ (假设x的地址是25000,其关系如图),4.1.3 指
25、针基本概念和地址值参数传递函数调用,可以用C系统已经定义好的符号常量NULL(空)对指针变量进行初始化或将它赋值给一个指针变量,例如:float *p=NULL; /*定义实型指针变量并将其初始化为常量NULL*/float *p; /*定义实型指针变量*/p=NULL; /*将符号常量NULL赋值给指针变量p*/,4.1.3 指针基本概念和地址值参数传递函数调用,在C程序设计中,对于指针变量的理解和使用时还应该特别注意以下几点: 在指针变量的定义形式中,星号(*)只是一个标志,表示其后面的变量是指针变量。例如,在指针变量定义语句int x,*y;中,y是指针变量。 一个指针变量只能指向与它同
26、类型的普通变量,即只有数据类型相同时普通变量才能将自己存储单元的首地址赋值给指针变量,其原因是不同类型的变量所占存储单元的字节数是不同的,当指针变量从指向一个对象改变到指向另外一个对象时,会随它指向对象的数据类型不同而移动不同的距离。,4.1.3 指针基本概念和地址值参数传递函数调用,例如,下列用法是错误的:int x;float *ptr;ptr= /*将整型变量x的存储首地址赋值给空类型指针变量p*/,4.1.3 指针基本概念和地址值参数传递函数调用,指针变量只能在有确定的指向后才能正常使用,也就是说指针变量中必须要有确定的地。没有确定指向的指针称为“空指针”或称为“悬挂指针”,使用这种指
27、针变量有可能引起不可预知的错误。 指针变量中只能存放地址值,不能把除NULL外的整型常数直接赋给指针变量。例如,下面的指针变量的赋值是错误的: int *ptr; ptr=100;/*错误,整型常数值直接赋给指针变量*/,4.1.3 指针基本概念和地址值参数传递函数调用,3指针变量的引用C程序中需要使用指针运算符(*)来表示对指针的引用。指针运算符(*)又称为间接运算符,它是一个单目运算符,只能作用于各种类型的指针变量上,其作用是表示被指针变量所指向的数据对象。 其一般使用形式如下:*,4.1.3 指针基本概念和地址值参数传递函数调用,例如有语句序列为:int x,*y;y=,4.1.3 指针
28、基本概念和地址值参数传递函数调用,例4.4 取地址运算符( 程序执行的结果为: 13ff7c: 300,300(注意变量y的十六进制值在不同的机器上可能是不同的)。,程序演示,4.1.3 指针基本概念和地址值参数传递函数调用,4地址值参数传递调用函数调用时如果被调函数的形式参数使用指针型参数(即某种数据类型的指针变量作为函数的形式参数),则主调函数中的实际参数就必须是指针值(地址量)。这种在函数调用过程中传递主调函数实际参数的指针(即实际参数存储单元的首地址)的方式提供了在被调函数中操作主调函数中实际参数的可能性。,4.1.3 指针基本概念和地址值参数传递函数调用,例4.5 地址值参数传递函数
29、调用示例。 /* Name: ex04-05.cpp */ #include void main() void swap(int *x,int *y);int a=3,b=5;printf(“swap函数调用前:a=%d,b=%dn“,a,b);swap( ,4.1.3 指针基本概念和地址值参数传递函数调用,执行过程如图:,4.1.3 指针基本概念和地址值参数传递函数调用,程序执行后的输出结果为: swap函数调用前:a=3,b=5 swap函数调用后:a=5,b=3从上面程序执行的过程可以得出使用地址传送方式在函数之间传递数据的特点是:数据在主调函数和被调函数中均使用同一存储单元,所以在被调
30、函数中对形参数据任何的变动必然会反映到主调函数中来。,程序演示,4.1.3 指针基本概念和地址值参数传递函数调用,5. 指针变量与被指针指向变量的区别从上面程序执行的过程t=*x;*x=*y; *y=t;可以看出在操作的对象是被指针指向变量。而下面这个例子是交换的对象是指针变量本身,体会它们的不同之处。,4.1.3 指针基本概念和地址值参数传递函数调用,例4.6 地址值参数传递函数调用示例。 /* Name: ex04-06.cpp */ #include void main() void swap(int *x,int *y);int a=3,b=5;printf(“swap函数调用前:a=
31、%d,b=%dn“,a,b);swap( ,4.1.3 指针基本概念和地址值参数传递函数调用,实际参数和形式参数的变化:,4.1.3 指针基本概念和地址值参数传递函数调用,程序执行的结果并没使得主函数中的实参变量a和b交换内容。程序执行的结果为:swap函数调用前:a=3,b=5swap函数调用后:a=3,b=5,程序演示,4.1.3 指针基本概念和地址值参数传递函数调用,虽然在被调用函数中使用指针型参数就提供了在被调函数中操作主调函数中实际参数的可能性。但并不是用了指针变量作函数的形式参数就一定可以在被调函数中操作或修改主调函数中的实参。在被调函数中是否能够操作或修改主调函数中实参值还要取决
32、于在被调函数中对指针形参的操作方式,操作指针形参变量指向的对象(即实参本身)则可以达到在被调函数中操作或修改主调函数实参的目的;但若操作的是指针形参变量本身则不能实现在被调函数中操作或修改主调函数实际参数的目的。,4.1.4 数组参数传递函数调用,在C程序设计中,既可以用数组的元素作为函数的参数,也可以将数组看成一个整体作为函数的参数。使用数组元素作为参数传递,其用法都与普通变量用法一样,实现的是函数间的传值调用。,4.1.4 数组参数传递函数调用,/*Name: ex04-07.cpp*/ #include #include #include #define N 5 void main()
33、void myprint(int x);int aN,bNN,i,j;srand(time(NULL);printf(“下面是数组a的数据.n“);for(i=0;iN;i+) ai=rand()%100;myprint(ai);,printf(“n下面是数组b的数据.n“);for(i=0;iN;i+) for(j=0;jN;j+) bij=rand()%100;myprint(bij);printf(“n“); void myprint(int x) printf(“%4d“,x); ,4.1.4 数组参数传递函数调用,例4.7程序在执行中,对于主函数中传递过来的一维数组a和二维数组b的每
34、一个数组元素,利用自定义函数myprint进行输出。,程序演示,4.1.4 数组参数传递函数调用,将数组看成一个整体作为函数参数时,用数组名作为函数的形式参数或实际参数,实现的是函数间的传地址值调用,下面分别讨论一维数组和二维数组作为函数参数的问题。,4.1.4 数组参数传递函数调用,1一维数组名作为函数参数实现的是“传地址值调用”,其本质是将它的全部存储区域或者部分存储区域提供给形式参数数组共享,即形参数组与实参数组是同一存储区域或者形参数组是实参数组存储区域的一部分。 存储关系如下图:,4.1.4 数组参数传递函数调用,需要把实参数组中从某个元素值后的部分传递给被调函数中的形参数组,则使用
35、实参数组某个元素的地址(参见4.7)。,4.1.4 数组参数传递函数调用,例4.8 编制求和函数并通过该函数求数组的元素值和。,int sum(int v,int n) int i,s=0;for(i=0;in;i+)s+=vi;return s;,/* Name:ex04-08.cpp */ #include #define N 10 void main() int sum(int v,int n);int aN=1,2,3,4,5,6,7,8,9,10,total;total=sum(a,N);printf(“total=%ldn“,total); ,程序演示,4.1.4 数组参数传递函数
36、调用,例4.9 编制求和函数并通过该函数求数组自某一元素后的所有元素值和,起始点元素序号从键盘上输入。,/* Name:ex04-09.cpp */ #include #define N 10 void main() int sum(int v,int n);int aN=1,2,3,4,5,6,7,8,9,10,total,pos;printf(“请输入求和起始元素序号: “);scanf(“%d“, ,int sum(int v,int n) int i,s=0;for(i=0;in;i+)s+=vi;return s; ,程序演示,4.1.4 数组参数传递函数调用,比较例4.8和例4.9
37、的程序,可以发现函数sum没有任何改变,程序中有所改变的是主调函数中的调用表达式:sum(&apos,N-pos),其中,参数&apos表示将数组a自apos元素以后的元素全部提供给形参数组共享,N-pos是传递到函数add中共享的数组元素个数。,4.1.4 数组参数传递函数调用,2二维数组作函数的参数,二维数组在存储时也是有序地占用一片连续的内存区域,数组的名字表示这段存储区域的首地址。需要特别注意的是,二维数组起始地址有多种表示方法,而且这些表示方法在物理含义上还有表示平面起始地址和表示线性起始地址之分,所以在使用二维数组的起始地址使必须注意区分需要用哪一种起始地址。,4.1.4 数组参数
38、传递函数调用,例4.10编制求二维矩阵最大元素的函数(假定矩阵为3行4列),用相应主函数进行测试。 /* Name: ex04-10.cpp */ #include #define M 3 #define N 4 void main() int max(int vN);int aMN=38,23,56,9,56,2,789,45,76,7,45,34;printf(“Max value is:%dn“,max(a); ,int max(int vN) /注意数组参数只能省略最高为的长度指定 int i,j,maxv;maxv=v00;for(i=0;imaxv)maxv=vij;return
39、maxv; ,程序演示,4.1.4 数组参数传递函数调用,(1)用二维数组名字作为实际参数实参用a,形参用b5图4.9 实际参数为二维数组名字,用二维数组名作为函数参数实现的是“传地址值调用”,其本质仍然是在函数调用期间实际参数数组将它的全部存储区域提供给形式参数数组共享,即形参数组与实参数组是同一存储区域。,4.1.4 数组参数传递函数调用,例4.10程序的函数max中使用了二维数组样式的形式参数接收从主调函数中传递过来的二维数组首地址,使得形参数组v共享实参数组a的存储区域;然后通过对形参数组v的操作达到操作是参数a的目的,即在形参数数组v中寻找最大值实质上是在实参数组a中寻找最大值,程序
40、执行的结果为:Max value is:789。,4.1.4 数组参数传递函数调用,(2)用二维数组起始地址的一级地址形式作为实际参数在实际计算机应用的程序设计中有时需要能够处理任意行列大小的二维数组的函数(例如要求上例中的函数max能够查找任意二维数组中的最大元素),此时直接用二维数组作为形式参数的设计形式就不太适合。,4.1.4 数组参数传递函数调用,为了编制较通用的函数,可以借助一维数组作为形式参数时可以不指定长度的特点,使用一维数组样式的形式参数接收二维数组实参,数组存储区域全部共享或部分共享时形参数组与实参数组的关系如图4.10所示。,4.1.4 数组参数传递函数调用,在实现这种参数
41、传递时还须注意以下两点: 函数调用时的实际参数必须是一级地址形式(参见图4.8中列出的3种以及地址方式),同时将二维数组的行数和列数传递到被调函数中。 由于在被调函数中只知道被处理得二维数组的起始地址,所以在处理过程中二维数组每一行的长度由程序员根据参数表中传递过来信息自己控制。,4.1.4 数组参数传递函数调用,例4.11 重新编制例4.10中的函数max,使其能够处理任意行列的二维数组。 /* Name: ex04-11.cpp */ #include #define M 3 #define N 4 void main() int max(int v,int m,int n);int aM
42、N=38,23,56,9,56,2,789,45,76,7,45,34;printf(“Max value is:%dn“,max(a0,M,N); ,int max(int v,int m,int n) int i,j,maxv;maxv=v0;for(i=0;imaxv)maxv=vi*n+j;return maxv; ,程序演示,4.1.4 数组参数传递函数调用,程序中函数max用一维数组样式的形式参数v来接收从主调函数中传递过来的二维数组首地址,注意到二维数组的名字表示的是二级地址,所以被传递的二维数组的首地址不能直接用二维数组名表示而应该使用3种一级地址形式,本示例中使用的是a0,还
43、可以使用&a00和*a形式。在被调函数中将传递过来的二维数组当作一维数组处理,其元素对应关系应该是:aijvi*n+j。程序执行的结果为:Max value is:789。,函数与程序结构,函数的定义和调用 函数的嵌套调用和递归调用 变量的作用域和生存期 编译预处理 多源文件C程序的组织方法,4.2 函数的嵌套调用和递归调用,4.2.1 函数的嵌套调用在C程序中函数不能嵌套定义。但C语言允许函数嵌套调用,所谓函数的嵌套调用就是一个函数在自己被调用的过程中又调用了另外的函数。一个两层嵌套函数调用的过程如图4.10。,例4.12 编程序计算,要求对n项的求和以及每一项ik的计算都用独立的函数实现,
44、k和n的值在主函数中从键盘输入。程序设计思路:可以把问题分解为两个模块:求幂次方模块和求和模块,在求和模块中要包含求幂模块。f1()函数的参数为n和k,其返回值是n的k次方。f2()函数的参数为n和k,返回值是幂次方的累加和。,4.3 函数的嵌套调用,#include long f1(int n,int k) long power=n;int i;for(i=1;ik;i+) power *= n;return power; long f2(int n,int k) long sum=0;int i;for(i=1;i=n;i+) sum += f1(i, k);return sum; ,vo
45、id main() int n,k;scanf(“%d,%d”, ,输入:4,5 结果:Sum of 5 powers of integers from 1 to 4= 1300 该程序中,main()中调用了f2()函数,而f2()函数中又调用了f1()函数,这就是函数的嵌套调用。,程序演示,4.2.2 函数的递归调用,一个函数直接地或间接地自己调用自己,称为函数的递归调用。函数的递归调用可以看成是一种特殊的函数嵌套调用,它与一般的嵌套调用相比较有几个不同的特点: (1)递归调用中每次嵌套调用的函数都是该函数本身; (2)递归调用不会无限制进行下去,即这种特殊的自己对自己的嵌套调用总会在某种
46、条件下结束。,4.2.2 函数的递归调用,递归调用在执行时,每一次都意味着本次的函数体流程没有执行完毕。所以函数递归调用的实现必须依靠系统提供一个特殊部件(堆栈)存放未完成的操作,以保证当递归调用结束回溯时不会丢失任何应该执行而没有执行操作。计算机系统的堆栈是一段先进后出(FILO)的存储区域,系统在递归调用时将在递归过程中应该执行而未执行的操作依次从堆栈栈底开始存放,当递归结束回溯时再依存放时相反的顺序将它们从堆栈中取出来执行,在压栈和出栈操作中,系统使用堆栈指针指示出应该存入和取出数据的位置。,4.2.2 函数的递归调用,例4.13 函数递归调用示例(使用递归调的方法反向输出字符串)。 1
47、 /* Name: ex04-13.cpp */ 2 #include 3 void main() 4 void reverse(); 5 printf(“输入一个字符串,以#作为结束字符:“); 6 reverse(); 7 printf(“n“); 8 9 void reverse() 10 char ch; 11 ch=getchar(); 12 if(ch=#) 13 putchar(ch); 14 else 15 reverse(); 16 putchar(ch); 17 18 ,执行如下图,4.2.2 函数的递归调用,所以程序执行时输入数据为字符串:abc#,则输出数据为字符串:#
48、cba。,程序演示,4.2.2 函数的递归调用,在对例4.13程序的分析中,由于分析了系统堆栈的行为,过程显得十分复杂。但在对递归函数调用进行理解或者在阅读分析含有递归调用函数的C程序时,没有必要过分地追求系统堆栈变化的细节,只要掌握在函数递归的过程中需要将应该执行而未执行的程序代码依次保留下来,当递归结束程序回溯时将保留下来的程序代码用相反的次序依次执行一遍即可。,4.2.2 函数的递归调用,例4.14 编程序使用递归方式求n!。 /* Name: ex04-14.cpp */ #include void main() long fac(long n);long n,result;printf(“Input the n: “);scanf(“%ld“, ,4.2.2 函数的递归调用,fac(5)等于120,执行如下:,程序演示,4.2.3 递归函数设计初步(*),递归是程序设计中一种非常重要的技术,与程序设计中其它控制方法策略相比较,递归程序设计的难度在于递归在人类社会的现实生活中没有直接对应的概念存在,而必须通过推理分析才能理解递归思想进而实现递归程序设计。在实际设计递归函数程序时,我们可以将重点放在分析递推公式和递归终止条件上,可以忽略系统的具体执行过程,只要算法和递推公式正确,结论一定是正确的。,