《C语言程序设计》课件2第8章_第1页
《C语言程序设计》课件2第8章_第2页
《C语言程序设计》课件2第8章_第3页
《C语言程序设计》课件2第8章_第4页
《C语言程序设计》课件2第8章_第5页
已阅读5页,还剩192页未读 继续免费阅读

下载本文档

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

文档简介

第8章函数8.1函数概念8.2函数的参数和返回值8.3函数间的参数传递8.4函数的嵌套与递归调用8.5*函数与指针8.6main()函数的参数和返回值8.7文件包含与条件编译8.8*C程序项目设计8.9模块化程序设计举例习题8

本章学习要求:

1.了解函数的定义,掌握函数的调用(一般调用、嵌套调用、递归调用),了解动态存储与静态存储的区别。

2.理解内部函数与外部函数的概念,理解形式参数、实际参数、局部变量、全局变量的概念。

3.了解条件编译和C程序项目设计,了解函数指针变量的概念及其使用。

4.掌握return语句的使用,掌握参数的传递方式(值传递、地址传递),掌握变量作用域,掌握静态变量的使用,掌握函数嵌套调用和递归调用,掌握文件包含。

构成C语言程序的基本单位是函数。函数也是C程序中模块化程序设计的基础。C函数可分为标准库函数和用户定义函数两类。前者是系统定义的,它们的定义分别存在不同的头文件中,用户只要用

#include文件将头文件包含到程序中即可调用它们;后者则是用户为解决自己的特定问题而自行编写的。本章主要介绍用户定义函数的设计和调用问题。当然,在自行设计程序时,充分利用系统提供的库函数,可以大大减轻程序设计的负担。

一个较大的程序一般应分为若干个程序模块,每个模块用于实现一个特定的功能。一个C程序由一个主函数(main)和若干个其他函数(0个到多个)构成。程序的执行总是从主函数开始,到主函数结束。同一个函数可以被一个或多个函数调用任意多次。如图1.9中“子模块5”就被调用两次。下面举例予以说明。8.1函数概念

例8.1

从键盘输入两个正整数m与n(m大于n),求m!/(m-n)!的值(即求)。

程序如下:

#include<stdio.h>

main() /*主函数*/

{intm,n;

longjiec(); /*声明函数,说明本函数中要调用的函数jiec(),

在本函数后面定义*/

printf("Pleaseinputm,n(m>n):");

scanf("%d,%d",&m,&n);

while(m<=n||n<=0||m<=0)/*本循环用来保证输入的m大于 n,并且m,n都是正整数*/

{printf("\nPleaserepeatinputm,n(m>n):");scanf("%d,%d",&m,&n);

}

printf("\nm!/(m-n)!=%ld\n",jiec(m)/jiec(m-n));

}

longjiec(intk) /*计算阶乘值的函数,函数名前的int 表示返回值的类型*/

{ints,i;

s=1;

for(i=1;i<=k;i++)s*=i; /*计算1*2*3*…*k,并将计算结果赋 值给变量s*/

return(s); /*将计算得到的阶乘值返回调用函数 (这里是主函数)*/

}

上面程序中,一共有两个函数:一个是主函数main(),完成的功能是从键盘输入两个正整数m与n,通过调用函数jiec()计算并输出jiec(m)/jiec(m-n)的值;另一个函数是jiec(),它完成的功能是计算阶乘值,它通过从主函数得到一个参数k,计算k!,并将计算结果返回调用函数(主函数)。下面作几点说明:

(1)一个C程序可以由若干个函数组成,其中必须有且仅有一个主函数main()。C程序总是从主函数开始执行(不管它在程序中的什么位置),而其他函数只能被调用。

(2)一个C程序可以由一个或多个源程序文件组成。C程序的编译以源程序文件为编译单位,而不是以函数为单位进行编译的。被调用函数与调用函数可以分别放在不同的源程序文件中,可以分别编写、分别编译,但最后必须连接成一个程序进行运行。

(3)在C语言中,所有函数都是平行的,即在定义函数时是互相独立的,互不从属。即C函数不能嵌套定义。

(4)程序中的

#include是将要用到的库函数的头文件包含到程序中来,这里因为用到了标准输入/输出函数scanf()和printf(),才使用

#include<stdio.h>。

(5)如果被调用函数在调用函数的后面定义,则必须在调用函数中进行声明。函数声明的一般形式如下:

类型标识符函数名([形参表列]);

其中“形参表列”是可以省略的。最简单声明函数的方式,就是将函数定义时的头部(在括号中含形参类型说明)作为声明的表达式,加上分号就构成了函数的声明语句。例如,例8.1中函数的声明就是将其省略,该声明可以写成以下两种形式:

longjiec(int);

或 longjiec(intk);

1.函数的定义

在使用一个函数前,需要先对其进行定义。

函数定义通常由两部分组成:函数头部和函数体。

函数的一般定义形式如下:

类型标识符函数名(形参表列)

形参类型声明

{声明部分

语句部分

}在定义函数时需要注意以下几点:

(1)函数头部的“类型标识符”是指函数类型,即函数返回值的数据类型。如果函数无返回值,则“类型标识符”可以使用空类型(即void类型)。如果省略函数的类型标识符,则默认数据类型为int型。例如,例8.1中的函数jiec()返回值类型为int型,可以省略jiec(k)前面的类型标识符int。

(2)

“函数名”必须为有效的标识符(标识符的命名规则见第2章)。

(3)

“形参表列”可以没有,如果没有则表明函数是无参函数。当“形参表列”中有多个参数时,则它们之间要用逗号(,)分隔。“形参类型声明”类似于变量声明。当然,C语言中允许在函数的头部与函数体之间对形参的类型进行声明。例如,在例8.1中函数jiec()可以定义成下面的形式:

longjiec(k)

intk;

{ints,i;

s=1;

for(i=1;i<=k;i++)s*=i;

return(s);

}

(4)

C语言中允许定义空函数。例如:

dummy(){}

在程序中调用此函数时,实际上什么工作也不做,没有任何实际作用。但是空函数在模块化程序设计中是很有用的。在程序设计中往往根据需要确定若干模块,分别由一些函数来实现。但在开始时,一般不可能将所有的函数模块都设计完成,只能将一些最重要、最基本的函数设计出来,而对于一些次要的函数模块在以后需要时陆续补上。因此,在程序设计的开始阶段,为了程序的完整性,用一些空函数先放在那里占一个位置,以后用一个编好的函数代替空函数。这样做,程序的结构清楚,可读性好,以后扩充新功能也比较方便,对程序结构影响不大。所以空函数在模块化程序设计中是很有用的。

2.函数的调用

自定义函数在定义好后,必须使用才能体现函数定义的目的。

函数调用的一般形式如下:

函数名(实参表列);

例如,在例8.1中,计算m!的函数调用表达式是jiec(m),如果要计算5!并赋值给变量s,则调用语句为

s=jiec(5);函数调用的方式可以有以下三种:

(1)函数语句。直接由函数的调用作为一个语句。如例8.1中标准库函数的调用:

printf("Pleaseinputm,n:");

这种调用方式不要求函数向主调函数带回值,只要求完成一定的操作。

(2)函数表达式。这种调用方式是将函数调用作为一个子表达式,这种表达式称为函数表达式,通常情况下,这种函数调用方式要求带回一个值(即在函数中有return语句)参加表达式的计算。例如:

printf("\nm!/(m-n)!=%d\n",jiec(m)/jiec(m-n));

在这个调用中,jiec(m)函数调用和jiec(m-n)的调用都是作为子表达式,先将返回值代入表达式,再将两个返回值相除得到表达式的值,最后将表达式的值输出。

(3)函数参数。这种函数调用方式是将函数调用作为另一个函数的实参。

如果使用例8.1中的函数jiec()计算3!!,并将计算结果赋值给变量m,则可以使用下面的语句:

m=jiec(jiec(3));

其中jiec(3)是一次函数调用,它应该先执行,执行后返回的值6作为另一次函数调用的实参,即m=jiec(6);,最后再调用函数jiec()计算出6!,并将结果赋值给变量m。

实质上,函数调用作为函数的参数,还是函数作为表达式形式调用的一种,因为函数的参数本来就是表达式。

8.2.1形式参数和实际参数

在函数的定义和调用过程中会涉及到函数的参数,函数的参数有两种:形式参数和实际参数,分别简称为形参和实参。

形参是指在定义函数时函数名后面括号中的参数。而实参是指在主调函数中调用一个函数时,函数名后面括号中的参数。8.2函数的参数和返回值例如,在第1章的例1.5中,函数在定义时,max()函数的头部为

floatmax(floatx,floaty)

以上就定义了两个形参x和y,并且类型都是float型。

而在函数调用时(在main()函数中),例如:

c=max(a,b);

其中a和b作为实参。关于形参与实参作如下几点说明:

(1)形参必须是单个的变量名(普通变量名、指针变量名、一维数组、二维数组),而实参可以是常量、变量或表达式,如:

max(3,a+b);

不管实参是常量、变量还是表达式,它们必须有确定的值。

(2)在定义函数中,形参必须指定数据类型。形参的类型声明可以放在函数名后的括号中,也可以放在函数的头部与函数体之间(参见函数定义的一般形式)。例如:

floatmax(x,y)

floatx,y;

{…}

(3)形参在函数调用之前,它并不占用内存中的存储单元。只有当函数调用时,实参的值传递(拷贝)给形参时,才给形参分配内存存储单元。在函数调用结束后,形参所占的内存单元立即被释放。

(4)在C语言中,实参向形参的数据传递是“单向传递”,即只能由实参传递给形参,而不能由形参传回给实参。在实参向形参传递数据时,是将实参的值拷贝给形参,因此,实参与形参占用不同的内存单元。如果形参是数组名,则传递的是数组首地址而不是数组中元素的值。

(5)实参与形参的个数必须相同、顺序一致,并且实参与对应的形参类型应相同或赋值兼容。例如,在例8.1中形参与实参都为int型,这是合法的、正确的。如果实参为int型,而形参为实型,或者相反,则按不同类型数值的赋值规则进行转换。如实参值为6.5,而形参值为int型,则会将实参6.5的值转换成int型的值6,再送给形参。除实型与整型数据之间可转换外,字符型与整型数据之间也能相互转换。8.2.2函数的返回值

在调用函数过程中,经常希望得到一个从被调用函数中带回来的值,这就是函数的返回值。例如,在例8.1中,jiec(m)就要带回来一个m!的值。

在C程序中,函数返回值是通过如下格式的语句带回来的:

return(表达式);

当然上述语句也可以是这样的形式:

return表达式;说明:

(1)

return语句括号中(或其后)的表达式也可以没有,当没有这个表达式时,说明只是要求函数返回,但不要求带回一个值,其功能就是中断被调用函数的执行,返回主调用函数,并不带回返回值。这种用法类似于exit()函数。

(2)只有当主调函数中需要得到一个从被调用函数带回来的值时,才在被调用函数中使用return语句带回一个返回值。即一个函数中如果有return语句,并且return语句后的表达式不是空的,则调用该函数时一定会得到一个返回值。

当然,在函数中可能不止一个return语句,但每次调用函数时,必定只执行其中一个return语句。例如:

if(x>0)

return(1);

else

return(0);

这段程序中,当满足x>0时,返回1;否则,返回0。

(3)函数返回值的类型。

如果函数有返回值,则这个值必定属于一个确定的数据类型,这个类型是在函数定义的头部说明的。例如,在例8.1中的函数jiec()定义时:

intjiec(k)

intk;

{…}

在函数名之前的int就是指出了该函数返回值的类型。因为这个函数返回值类型是int型,在定义时也可以省略。如果函数类型和return语句中表达式的值的类型不一致,则以函数类型为准。对于数值型数据,可以自动进行类型转换。例如,若定义的返回值类型是int型,而return语句中表达式的类型是float型,则会将表达式的值自动转换成int型返回。

如果函数中没有return语句,则说明并不带回一个值,这种情况下,在函数定义时,函数名前的类型可以使用void型。void型是指空类型,即在函数定义时,如果函数名前的类型是void型,则说明该函数在调用时并不带回一个确定的值。

(4)函数返回值,每次只能带回一个值。尽管在函数中可能有多个return语句,但只有其中的一个return语句会执行,也就只能带回一个值。如果想从函数中带回多个值,可以采用的方式有两种:①使用指针变量,通过地址传递方式;②使用全局变量。这些在后面将会介绍到。

(5)当函数返回值是指针值时,函数头部的定义形式一般如下:

数据类型*函数名(参数表列)

这种情况下,在主调用函数中,函数返回值所赋给的变量必须是指针变量。

例8.2

下面程序中有四处错误,请根据题意改正。

主函数接收从键盘上输入的一组整数(以-999作为输入结束)并保存到整型数组xx中,调用fun函数。函数fun的功能是:对长度为n的数组a,不考虑这组整数中的最大数和最小数(其有重复,则都不考虑),求出余下数中的最大数max及最大数的个数cnt1、最小数min及最小数的个数cnt2,分别保存到全局变量max、cnt1、min、cnt2中。含有错误的源程序如下:

#include<stdio.h>

#defineN100

intmax,min,cnt1,cnt2;

intfun(inta[],intn)

{inti,j,k,x;

for(i=0;i<n;i++)

{k=i;

for(j=i+1;j<=N;j++)

if(a[k]<a[j])k=j;

if(k!=i){x=a[i];a[i]=a[k];a[k]=x;}

}

for(i=0;i<N;i++)printf("%5d",a[i]);

printf("\n");

i=1;

j=N-2;

while(a[i]==a[i-1])i++;

while(a[j]==a[j+1])j--;

max=a[i];min=a[j];cnt1=cnt2=1;

while(a[i+1]==a[i]){cnt1++;i++;}

while(a[j-1]==a[j]){cnt1++;i++;}

}

main()

{intxx[N],i=0,m=0;

scanf("%d",&xx[i]);

while(xx[i++]!=-999)scanf("%d",&xx[i]);

m=i-1;

fun(m,xx);

for(i=0;i<m;i++)printf((i+1)%5?"%5d":"%5d\n",xx(i));

printf("\n");

printf("cmax=%d,cmin=%d,cnt1=%d,cnt2=%d\n",max,min,cnt1,cnt2);

}本例中共四处错误,第一处在fun()函数中,由于fun()函数并没有返回值,因此,函数名fun前的int型不对,应改成void;第二处也在fun()函数中,由于数组a共有N个元素,但在内层的for循环中,表达式2为j<=N,当j=N时,数组就会越界;第三处在主函数中,fun(m,xx);对函数进行调用时,实参顺序与函数fun()中的形参顺序不一致,所以应改成fun(xx,m);;第四处错误也在主函数中,在主函数for循环中,输出的应是xx数组的各元素,但其调用数组是用xx(i),在C语言中,调用函数应用中括号,因此应改成xx[i]。

通过本例我们可以看出,函数在调用时特别要注意:形参与实参的对应(个数、类型、顺序的对应);函数返回值的类型与函数中定义类型的一致性。

8.3.1形参与实参的结合方式

函数在定义与调用过程中,如果需要在主调函数与被调用函数之间进行数据的传递,例如:主调函数要向被调用函数传过去实参的值,而被调用函数执行完后有可能要将运算结果带回到主调函数中。这就涉及到被调用函数中的形参与主调函数中的实参互相结合的问题。在C程序中,一般来说,形参与实参的结合方式有以下两种。8.3函数间的参数传递

1.数值传递

所谓数值传递,就是指主调函数中的实参地址与被调用函数中的形参地址是互相独立的,在函数调用时,直接将实参值拷贝给形参在内存中的存储单元中。在这种结合方式下,被调用函数在执行过程中,当需要存取形参值时,直接存取形参地址中的数据,而不影响实参地址中的值。因此,如果在被调用函数中改变了形参的值,是不会改变实参值的,因为形参和实参的地址是互不相同的。显然,当被调用函数执行完返回主调函数时,被调用函数中形参的新值也不会传回到主调用函数。由此可以看出,在形参与实参为数值传递的方式下,被调用函数中对形参的操作不影响主调用函数中的实参值,因此只能实现数据的单向传递,即从主调函数的实参将值传送给被调用函数的形参。所以,这种传递又称为“单向值传递”。

在这种方式中,实参可以是变量、常量、也可以是表达式。

在C语言中,当形参为简单变量(即整型、实型、字符型)时,均采用数值传递。在这种情况下,一个函数只能返回一个函数值,在例8.3中就很好地说明了这个问题。

例8.3

无效的数值交换。

本例我们将验证:在赋值调用中,形参的改变不会对实参产生影响。

程序如下:

#include<stdio.h>

voidswap(intx,inty); /*声明函数*/

main()

{inta=7,b=14;

printf("数值交换\n\n");

printf("未交换前:a=%d,b=%d\n",a,b);

swap(a,b);

printf("调用函数swap(a,b)后:a=%d,b=%d\n",a,b);

}

voidswap(x,y)

intx,y;

{inttemp;

temp=x; /*开始交换形参内容*/

x=y;

y=temp;

printf("在函数swap()中:x=%d,y=%d\n",x,y);

}程序运行结果如下:

数值交换

未交换前:a=7,b=14

在函数swap()中:x=14,y=7

调用函数swap(a,b)后:a=7,b=14

在上面的程序中,主函数中定义了两个变量,即a=7,b=14,并分别赋了值,当调用swap()函数时,只是将实参a的值复制给形参x,将实参b的值复制给y。由于此时a和x、b和y分别占用不同的内存单元,在swap()函数中,将形参x和y的值互换,这只是在x和y的内存单元内的值互换,但对a和b存储单元中的值并不产生影响,当swap()函数执行结束时会释放x和y的存储单元,交换后的形参值丢失。所以,在主函数中最后输出a和b的值仍然是7和14,并没有交换。

2.地址传递

所谓地址传递,是指在一个函数调用另一个函数时,并不是将主调函数中的实参值直接传送给被调用函数中的形参,而只是将存放实参的存储单元地址传送给形参。在这种结合方式下,被调用函数在执行过程中,当需要存取形参值时,实际上是通过对形参(形参是一个指针或地址)指向的存储单元进行数据存取,而此时形参内存放的值(地址)与实参存放的值(地址)相同,也就是说,形参与实参指向同一块存储单元。因此,如果在被调用函数中改变了形参指向存储单元的值,实际上也就改变了主调函数中实参所指向存储单元的值。由此可见,在这种方式下,被调用函数中对形参所指向存储单元的操作实际上就是对主调函数中实参所指向存储单元的操作。显然,当被调用函数执行完成时,形参空间释放,丢失的是形参中存放的地址,但是形参所指向的存储单元并不释放,因此,对形参的操作就保留了下来,在主调函数中,又可以通过实参中的地址访问经被调用函数修改后的存储单元的值。所以这种传递方式称为“地址传递”。

在这种传递方式中,实参可以是变量的地址或指针变量。

在C语言中,当形参为数组名或指针变量时,均采用地址传递。

如果我们将例8.3中的程序进行如下修改:

#include<stdio.h>

voidswap(int*x,int*y); /*声明函数*/

main()

{inta=7,b=14;

printf("数值交换\n\n");

printf("未交换前:a=%d,b=%d\n",a,b);

swap(&a,&b);

printf("调用函数swap(a,b)后:a=%d,b=%d\n",a,b);

}

voidswap(x,y)

int*x,*y;

{inttemp;

temp=*x; /*开始交换形参内容*/

*x=*y;

*y=temp;

printf("在函数swap()中:*x=%d,*y=%d\n",*x,*y);

}

程序运行结果如下:

数值交换

未交换前:a=7,b=14

在函数swap()中:*x=14,*y=7

调用函数swap(a,b)后:a=14,b=7在这个程序中,实现了数值的交换。在调用swap()函数时,传递给它的是变量a和b的地址。在swap()函数定义中,形参定义成指针类型,当调用时,将变量a的地址拷贝给指针变量x,而将变量b的地址拷贝给指针变量y,在swap()函数中的交换是针对指针变量x和y所指向单元的值的互换,即交换了主调函数中变量a、b的值。

在地址传递方式中,我们常见到使用数组名作为实参与形参,实际上,也是通过将实参数组的首地址传递给被调用函数的形参数组,在被调用函数中对数组名所指向的若干连续存储单元进行操作,即是对实参数组进行操作,返回后能够在主调函数中得到处理后的数组元素值。下面通过例子来说明。

例8.4

从键盘输入10个整数,通过调用一个函数将这10个数使用直接选择排序法从小到大排序,再输出这10个整数。

问题分析:本例中要求有两个函数,子函数(sort())用于将10个数排序,主函数负责输入和输出10个整数。

程序如下:

#include<stdio.h>

#defineN10

voidsort(inta[]) /*对10个整数进行直接选择排序*/

{inti,j,k,temp;

for(i=0;i<N;i++)

{k=i;

for(j=i+1;j<N;j++)

if(a[k]>a[j])k=j;

if(k!=i) /*如果最小值不是下标为i的元素,交换最小 值和当前位置的值*/

{temp=a[i];a[i]=a[k];a[k]=temp;}

}

}

main()

{inti,arr[N];

printf("请输入10个整数:\n");

for(i=0;i<N;i++)scanf("%d",&arr[i]);

sort(arr); /*调用排序函数,用一维数组名作为实参*/

printf("\n排序后的10整数是:\n");

for(i=0;i<N;i++)

{printf("%d",arr[i]);

if((i+1)%5==0)printf("\n"); /*每行输出5个整数*/

}

}

在本例中,实参和形参都是使用的一维数组名,即一维数组的起始地址。实参向形参传递的数据是一个地址。下面再列举一个以二维数组的数组名作为参数传递的例子。

例8.5

有一个4

×

3的矩阵,求所有元素中的最小值。

问题分析:本例可以通过两个函数来实现,子函数求最小值,而主函数负责输入/输出矩阵中的值(二维数组元素值)。在这两个函数之间传递的数据是二维数组名。

程序如下:

#include<stdio.h>

intmin_value(inta[][3])

{inti,j,min;

min=a[0][0];/*先将第1个元素的值当成最小值*/

for(i=0;i<4;i++) /*将其他元素与最小值min进行比较,如果其 他元素的值比min小,就将其值赋值给min*/

for(j=0;j<3;j++)

if(a[i][j]<min)min=a[i][j];

return(min);

}

main()

{inti,j,arr[4][3];

printf("请输入4×3的矩阵\n");

for(i=0;i<4;i++)

for(j=0;j<3;j++)

scanf("%d",&arr[i][j]);

printf("\n矩阵中最小值为:%d",min_value(arr));

}

本例是用二维数组的名字作为实参和形参,需要注意的是:在min_value()函数定义时,作为形参的二维数组第二维的长度不能少。8.3.2变量的作用域

变量的作用域是指变量的作用范围,在这个范围内,变量是有效存在的。超出这个范围时,为变量分配的存储空间将被释放,在范围外引用变量是非法的,就会产生错误。

根据变量作用域的不同,可将其分成局部变量和全局变量。

1.局部变量

在函数内部定义的变量称为局部变量,又称内部变量。函数内部定义的变量作用域从定义位置开始,到本函数结束为止(主函数main()也不例外)。因此,不同函数中的局部变量可以重名,互相独立。特别指出的是,函数中的形参也是局部变量。例如,例8.3中,主函数中定义的变量a、b是局部变量,它们只在主函数内有效;而函数swap()中的形参x、y以及在swap()函数中定义的变量temp也都是局部变量,它们只在函数swap()中有效。

注意:在复合语句(由{}括起来的语句)中定义的变量也是局部变量,但其作用域是从定义位置到本复合语句结束。

2.全局变量

一个C源程序文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量则称为全局变量,又称外部变量。全局变量的作用域从定义变量的位置开始到本源程序文件结束。

例如,在例8.2中,函数fun()之前的语句“intmax,min,cnt1,cnt2;”就是定义全局变量max、min、cnt1、cnt2,因此其作用域包括了fun()和main()函数。在例8.3中并没有实际数值的交换,如果使用全局变量,就可以实现数值的交换,即程序修改如下:

#include<stdio.h>

voidswap(intx,inty); /*声明函数*/

intx,y; /*定义全局变量x、y*/

main()

{x=7;

y=14;

printf("数值交换\n\n");

printf("未交换前:x=%d,b=%d\n",x,y);

swap();

printf("调用函数swap()后:x=%d,y=%d\n",x,y);

}

voidswap()

{inttemp;

temp=x; /*开始交换全局变量x和y的值*/

x=y;

y=temp;

printf("在函数swap()中:x=%d,y=%d\n",x,y);

}

在上述程序中,变量x和y在程序中的开头进行定义,它们在后面两个函数swap()和main()中都起作用。该程序先在main()函数中给全局变量x、y赋值,再在swap()函数中将它们的值互换,这样就可以达到数值交换的目的。原因是swap()函数中的变量x和y与主函数中的变量x和y均是同一个变量,即使用同一块内存单元。

全局变量的作用范围是定义变量的位置开始到本源程序文件结束。但如果在全局变量的作用域内,有与其同名的局部变量,则在该局部变量的作用域内,全局变量不起作用(即变量屏蔽)。

例8.6

全局变量与局部变量同名示例。

有如下程序:

intx=1;

main()

{inty;

intf(int); /*声明函数f()*/

y=f(3);printf("%d\n%d",x,y);

}

intf(intx)

{if(x==3)

{intx=2;

returnx;

}

elsereturnx;

}

该程序运行后,输出x的值是_________,y的值是_________。程序分析:

在该程序中有两个函数,在程序开始处定义了全局变量x,并赋值为1。而在函数f()中形参为x,并且在复合语句中用“intx=2;”还定义了局部变量x,它们各自的作用域见程序中。在复合语句中定义的变量x的作用域内,形参局部变量x和全局变量x都不起作用。而在函数f()中,除复合语句外的部分,形参变量x起作用,而全局变量x不起作用。因此,在主函数中输出时的变量x应该是全局变量x,第一个空应该填1;而“y=f(3);”语句在调用函数f()时,执行的是复合语句部分,因此返回值应是2,y的值应是2,第二个空应填2。8.3.3动态存储变量与静态存储变量

除了变量的作用域外,C语言中还提供了变量的存储类型来对变量作用域进行说明。各种变量的作用域不同,就其本质来说是因为变量的存储类型不同。对一个变量的说明,不仅应说明其数据类型,还应说明其存储类型。数据类型如int、float、char等。

C语言中,变量的存储类型分为静态存储和动态存储两大类。对变量的存储类型说明有以下四种:自动变量(auto)、寄存器变量(register)、外部变量(extern)和静态变量(static)。自动变量和寄存器变量属于动态存储方式,外部变量和静态变量属于静态存储方式。

1.内部变量

内部变量又称局部变量,是指在函数内部定义的变量。

1)静态变量

静态变量是在变量定义时就分配存储单元,该存储单元一直保持不变,直到整个程序结束才释放存储单元,但其作用域只是在定义静态变量的函数内。

静态变量的定义格式如下:

static数据类型变量名表列;

说明:

(1)静态变量的作用域与生存期不相同。静态变量是一种比较特殊的变量。在定义开始时,给其分配存储空间,供其在调用时使用,函数调用结束时并不释放存储空间,而是直到程序结束才释放存储空间。

(2)静态变量在定义时赋初值实际上是发生在程序编译时。如果在定义静态变量后退出它的作用域时,该变量依然存在,下次进入作用域时就不用再进行定义和初始化,不再重新赋初值,只是保留上次调用结束时的值。

(3)如果静态变量定义时没有赋初值,int型变量默认初值为0,float型变量默认初值为0.0,char型变量默认初值为'\0',等等。

(4)形参不能定义成静态变量。静态局部变量经常使用,但建议尽可能少用,因为它们占用的存储空间直到程序结束才释放。

例8.7

利用静态变量实现:计算并输出1~5的阶乘值。

程序如下:

#include<stdio.h>

longfun(intn)

{staticlongs=1;

s=s*n;

return(s);

}

main()

{inti;

for(i=1;i<=5;i++)

printf("\n%d!=%ld",i,fun(i));

}程序运行结果:

1!=1

2!=2

3!=6

4!=24

5!=120

在函数fun()中,变量s定义为静态局部变量,因此,在程序编译时为它赋初值1。在每次调用fun()函数时静态变量s均保留上次调用时得到的值,在此基础上再进行s=s*n的计算。例如,计算5!是在计算的4!值(存放在s中)上再乘以5。

试想如果将上述程序中的“static”去掉,程序输出结果会是什么样?

2)自动变量

自动变量就是动态变量,在程序执行过程中,需要使用时才分配存储单元,使用完立即释放空间。例如函数的形参,在函数定义时并不给形参分配存储空间,只是在函数被调用时才为其分配存储空间,当函数调用结束后,自动释放存储空间。

动态变量的定义格式如下:

auto数据类型变量名表列;/*关键字auto可省略*/

说明:

(1)在定义时,关键字auto可省略,本节以前所定义的局部变量都省略了auto,则说明它们都是自动变量。

(2)在函数中定义的自动变量,只在该函数内有效;在函数被调用时分配存储空间,调用结束就释放。函数在复合语句中定义的自动变量,只在该复合语句中有效;退出复合语句后,也不能再使用,否则将引起错误。

(3)如果只定义动态变量而不赋初值,则其初值是不确定的(即是一个随机值)。如果在定义时就赋初值,则赋初值操作是在调用时进行的,且每次调用都要重新赋一次初值。这一点与静态变量有很大区别。

(4)自动变量的作用域和生存期是一致的,都局限于定义它的函数或复合语句内,因此在不同的函数或复合语句内就可以使用同名的自动变量。

3)寄存器变量

通常变量的值都是存储在内存中的,但有些变量由于要大量重复使用(如for循环中的循环记数变量),为了提高执行效率,将这样的变量存放在CPU的寄存器中,这种变量就称为寄存器变量。

寄存器变量的定义格式如下:

register数据类型变量名表列;下面的程序中使用了寄存器变量来作为循环计数变量。

main()

{intsum=0;

registerinti;

for(i=1;i<100;i++)sum+=i;

printf("%d",sum);

}说明:

(1)只有局部变量才能定义为寄存器变量。

(2)如果系统不支持寄存器变量,或者CPU中的寄存器不够时,实际上是将寄存器变量当作自动变量处理。

(3)由于CPU中的寄存器数目有限,不能定义过多的寄存器变量,一般不超过3个为宜,并且寄存器变量使用完后立即释放。

(4)由于寄存器变量不在内存中,因此不能进行地址运算。例如:

registerintk;

scanf("%d",&k);

这种使用方式就不对。

4)自动变量、静态变量和寄存器变量的比较

自动变量、静态变量和寄存器变量的比较如表8.1所示。

表8.1自动变量、静态变量和寄存器变量比较2.外部变量

外部变量是在函数外部定义的变量,也就是全局变量。

外部变量的定义格式如下:

extern数据类型变量名表列;

全局变量如果在源程序文件开头定义,则在整个文件范围内的所有函数都可以使用该变量。但如果不在文件开头定义全局变量,则只限于在定义点到文件结束范围内的函数使用该变量。如果在全局变量的作用域外还要使用全局变量,则应事先用extern加以说明。由此可以看出,使用extern说明可以扩展全局变量的作用域。扩展全局变量作用域的方式可以分成两种:一是在同一文件内扩展全局变量作用域;二是将一个文件中的全局变量作用域扩展到另一个文件。

1)同一文件内扩展全局变量作用域

在同一文件中,由于全局变量不是在文件开头就定义的,因此,如果想在全局变量定义点之前的函数中使用全局变量,则应在函数中用extern加以说明。例如,下列程序可以实现两个变量值的交换。

#include<stdio.h>

voidswap(intx,inty); /*声明函数*/

main()

{externintx,y; /*声明全局变量,说明变量x、y在后 面有定义*/

x=7; /*给全局变量x赋初值*/

y=14; /*给全局变量y赋初值*/

printf("数值交换\n\n");

printf("未交换前:x=%d,b=%d\n",x,y);

swap();

printf("调用函数swap()后:x=%d,y=%d\n",x,y);

}

intx,y; /*定义全局变量x和y*/

voidswap()

{inttemp;

temp=x; /*开始交换全局变量x和y的值*/

x=y;

y=temp;

printf("在函数swap()中:x=%d,y=%d\n",x,y);

}上述程序中,虽然全局变量x和y的定义在主函数后面,但由于在主函数中用extern声明了变量x和y是外部变量,因此,将全局变量x和y的作用域扩展到了主函数,这样在主函数中就可以对全局变量x和y进行操作(赋值、输出)。

2)全局变量的作用域扩展到另一个文件中

当需要在一个源程序文件中使用另一个源程序文件中定义的全局变量时,也可以用extern对全局变量进行声明。例如,在下列程序中,两个函数分别存放在两个文件中。

/*file1.c文件*/

#include<stdio.h>

#include"file2.c" /*将源程序文件file2.c包含到当前文件中*/

intx,y; /*定义全局变量*/

main()

{x=7; /*给全局变量x赋初值*/

y=14; /*给全局变量y赋初值*/

printf("数值交换\n\n");

printf("未交换前:x=%d,b=%d\n",x,y);

swap();

printf("调用函数swap()后:x=%d,y=%d\n",x,y);

}

/*file2.c文件*/

externintx,y; /*声明全局变量*/

voidswap()

{inttemp;

temp=x; /*开始交换全局变量x和y的值*/

x=y;

y=temp;

printf("在函数swap()中:x=%d,y=%d\n",x,y);

return;

}

在主函数所在的文件file1.c中定义了全局变量x和y,在函数swap()所在的文件file2.c中将x和y声明为外部变量,这样在函数swap()中就可以使用文件file1.c中定义的全局变量x和y了。

特别需要说明的是,同一源程序文件中不能同时定义相同的全局变量。例如,下面的程序是错误的。

#include<stdio.h>

voidswap(intx,inty); /*声明函数*/

intx,y; /*定义全局变量*/

main()

{

x=7; /*给全局变量x赋初值*/

y=14; /*给全局变量y赋初值*/

printf("数值交换\n\n");

printf("未交换前:x=%d,b=%d\n",x,y);

swap();

printf("调用函数swap()后:x=%d,y=%d\n",x,y);

}

intx,y;/*重复定义全局变量x和y,与前面的定义相冲突*/

voidswap()

{inttemp;

temp=x;/*开始交换全局变量x和y的值*/

x=y;

y=temp;

printf("在函数swap()中:x=%d,y=%d\n",x,y);

}

如果想使在文件中定义的全局变量不被其他文件使用,就可以在全局变量定义时,将其定义成static型外部变量。例如,下面程序就是错误的。

/*file1.c文件*/

#include<stdio.h>

#include"file2.c" /*将源程序文件file2.c包含到当前文件中来*/

staticintx,y; /*定义静态外部变量,仅限文件file1.c中使用 */

main()

{x=7; /*给全局变量x赋初值*/

y=14; /*给全局变量y赋初值*/

printf("数值交换\n\n");

printf("未交换前:x=%d,b=%d\n",x,y);

swap();

printf("调用函数swap()后:x=%d,y=%d\n",x,y);

}

/*file2.c文件*/

externintx,y; /*声明全局变量,程序编译时,会提示变量x 和y没有定义*/

voidswap()

{inttemp;

temp=x; /*开始交换全局变量x和y的值*/

x=y;

y=temp;

printf("在函数swap()中:x=%d,y=%d\n",x,y);

return;

}

上述程序在编译连接时会提示“变量x,y没有定义”的错误,这是因为,在主函数所在的文件file1.c中定义了外部变量x和y,但它们只能在文件file1.c中使用,而不能将其作用域扩展到文件file2.c中。8.3.4内部函数和外部函数

定义函数就是为了被其他函数调用,因此,在本质上函数是全局的,是能够被不同文件中的函数所调用。但是,也可以指定函数不能被其他文件中的函数调用,只能被本文件中的函数调用。根据函数能否被其他源文件中的函数调用,将函数分为内部函数和外部函数。

1.内部函数

只能被本文件中其他函数调用的函数称为内部函数,内部函数又称为静态函数。

内部函数的定义形式如下:

static类型标识符函数名(形参表)

例如,staticintfunc(n)就是将函数func()定义成一个内部函数,它只能被与它同在一个文件中的其他函数调用,而其他文件中的函数不能调用它。

在多人合作的模块化程序设计中,如果在不同的文件中使用了相同名称的内部函数,在程序联调时,不会相互干扰,这样每个人在编写函数时,就不必担心所用函数是否会与其他源文件中的函数同名。

2.外部函数

能被其他文件中函数调用的函数称为外部函数。

外部函数的定义形式如下:

[extern]类型标识符函数名(形参表)

例如,externintfunc(n)或intfunc(n)中,extern说明符可以省略,因此,如果省略extern说明符,则默认函数是外部函数,本节之前我们所定义的函数都是外部函数。

外部函数在一个源文件中定义,而其调用范围可以通过在其他源文件中用extern声明,而将其调用范围扩展到其他源文件中。例如,下面的程序可以说明在不同文件之间调用函数。

/*file.c文件*/

main()

{externinput_str(chars[80]);

externdel_str(chars[],charc);

externoutput_str(chars[]);

/*上面三行声明在本函数中将要调用在其他文件中定义的三个函数*/

charstr[80],ch;

input_str(str);

scanf("%c",&ch);

del_str(str,ch);

output_str(str);

}

/*file1.c文件*/

#include<stdio.h>

input_str(chars[80]) /*定义外部函数input_str()*/

{gets(s); /*输入字符串*/

}

/*file2.c文件*/

del_str(chars[],charc) /*定义外部函数del_str()*/

{inti,j;

for(i=j=0;s[i]!='\0';i++)

if(s[i]!=c)s[j++]=s[i];

s[j]=’\0’;

}

/*file3.c文件*/

#include<stdio.h>

output_str(chars[]) /*定义外部函数output_str()*/

{puts(s);} /*输入字符串*/

上述程序包含4个文件,在file.c中要调用其他3个文件中定义的外部函数,因此必须在主函数中对要调用的外部函数进行声明。

1.函数嵌套

在函数调用时,允许在函数中调用另一个已声明的函数,这种在函数中调用另一个函数的用法称为函数的嵌套。C语言中允许函数进行嵌套调用,但不允许函数的嵌套定义。

下面的程序段显示出了函数嵌套调用。

intx(); /*声明x()函数*/

inty(); /*声明y()函数*/

main()

{8.4函数的嵌套与递归调用

m=x(); /*调用函数x()*/

}

intx()

{

k=y(); /*嵌套调用函数y()*/

}

inty()

{

}

上面的程序结构中,函数的执行与调用过程如图8.1所示。

图8.1函数嵌套调用的执行过程在程序执行时,总是从主函数main()开始,当执行到语句“m=x()”时调用函数x()。

在函数x()中执行到语句“k=y()”时调用函数y()。

当y()函数执行完,就会返回到其主调函数(即x()函数)的调用位置“k=y()”,并将返回值赋值给变量k,再继续执行函数x()中“k=y()”后面的语句。

当x()函数执行完,就会返回到其主调函数main()的调用位置“m=z()”,并将返回值赋值给变量m,再继续执行main函数中的“m=x()”后面的语句,直到程序结束。可以看出,每一层函数的调用均只对调用它的函数是可见的,在函数返回时,将回到调用它的语句,然后再执行调用语句后面的其他语句。

2.函数的递归调用

既然一个函数可以调用另一个函数,那么这个函数能不能调用自身呢?这个答案是肯定的,这种调用自身的情况称为递归调用。

确切地说,函数的递归调用就是指一个函数直接或间接调用自身。前者称为直接递归,后者称为间接递归,如图8.2所示。

我们将递归调用的函数称为递归函数。由于递归非常符合人们的思维习惯,而且许多数学的函数及许多算法或数据结构都是递归定义的,因此递归调用很有实用价值。

图8.2函数递归调用从图8.2可以看出,这两种递归构成了循环调用,如果没有结束的条件,则会进行无终止地自身调用。显然,在程序中不应该出现这种无终止的递归调用,而只应出现有限次数的、有终止的递归调用,一般可以用if语句来控制,只有在某一条件成立时才继续执行递归调用,否则就不再继续。

例8.8

问年龄问题。

有5个人坐在一起,问第5个人多少岁,他说他比第4个人大3岁。问第4个人岁数,他说他比第3个人大3岁。问第3个人,又说比第2个人大3岁。问第2个人,说比第1个人大3岁。最后问第1个人,他说是10岁。请问第5个人多大?

问题分析:从题目可知,要想知道第5个人的年龄,就必须先知道第4个人的年龄,而第4个人的年龄也不知道,只有知道第3个人年龄才会知道第4个人的年龄,而第3个人的年龄又取决于第2个人的年龄,第2个人的年龄取决于第1个人的年龄。并且每个人的年龄都比其前一个人的年龄大3岁。如果用age(1)代表第1个人的年龄,则age(2)代表第2个人的年龄,……,age(n)代表第n个人的年龄。由此,我们可以得到:

age(n)=age(n-1)+3

age(n-1)=age(n-2)+3

age(5)=age(4)+3

age(4)=age(3)+3

age(3)=age(2)+3

age(2)=age(1)+3

age(1)=10

如果用数学公式可以表述成:

可以看出,当n>1时,求第n个人年龄的公式是相同的,可以用一个函数来表示上述关系。图8.3表示了求第5个人年龄的过程。该过程分为“递推过程”和“回代过程”。在“递推过程”过程中,从要知道的第5个人的年龄,去求第4个人的年龄,一直找下去,直到找到一个人的年龄是确切的值(第1个人的年龄为10岁)。这第1个人的年龄就是一个转折点,因此只要“回代”就可知道第2个人的年龄,再“回代”就可知道第3个人的年龄,依次递推,就可求出第5个人的年龄。

图8.3递归过程我们可以使用一个函数age()来实现递归过程。而递归的终止条件就是当n=1时,age(1)=10。

age(intn)

{intr;

if(n==1)r=10;

elser=age(n-1)+3;

return(r);

}

main()

{printf("第5个人的年龄为:%d",age(5));}

程序运行结果:

第5个人的年龄为:22

例8.9

编写n!

的递归调用程序。

n!

的递归定义如下:

当n>1时,n!=n*(n-1)!,例如:5!=5*4!,4!=4*3!,…

编写的程序如下:

longfunc(intn)

{longr;

if(n==0||n==1)r=1;

elser=n*func(n-1);

return(r);

}

main()

{intn;

printf("Pleaseinputn:");

scanf("%d",&n);

printf("\n%d!=%ld\n",n,func(n));

}根据本例,读者可以自己尝试用递归调用编写程序计算:s=1+2+3+…+n,求这个累加时我们也可以得出数学公式:

对于设计递归程序,一般可分为两个步骤:

(1)确定递归终止的条件。

(2)确定将一个问题转化成另一个问题的规律。即找到前后两项之间的规律,例如求n!,前后两项之间的规律就是n!=n*(n-1)!。

8.5.1用函数指针变量调用函数

C语言中,可以用指针变量指向整型变量、字符串、数组,也可以指向一个函数。一般来说,程序中的每一个函数经编译连接后,其目标代码在计算机内存中是连续存放的,该代码的首地址就是函数执行时的入口地址。函数名本身就代表该函数的入口地址。也可以用指针指向这个入口地址,这样的指针就称为函数的指针。因此,既然可以使用函数名调用函数,当然也可以用函数的指针调用函数。8.5*函 数 与 指 针指向函数的指针变量定义形式如下:

数据类型(*指针变量名)();

其中()不能省略。例如:

long(*p)();

说明:

(1)“数据类型”是指函数返回值的类型。

(2)函数的调用可以通过函数名调用,也可以通过函数指针调用(即用指向函数的指针变量调用)。

(3)(*p)()表示定义了一个指向函数的指针变量p,它不是固定指向哪一个函数,而只是表示定义了这样一个类型的变量,它是专门用来存放函数入口地址的。在程序中如果将哪一个函数的入口地址赋给它,它就指向哪一个函数。在一个程序中,一个指针变量可以先后指向返回值类型相同的不同函数。即在程序中,函数指针变量的值可以改变,但它所指向的函数返回值类型必须相同。

(4)给函数指针变量赋值时,只需给出函数名而不必给出参数,如:

p=func;

(5)用函数指针变量调用函数时,只需将(*p)代替函数名即可,在(*p)之后的括号中根据需要写上实参。如下面语句表示“调用由p指向的函数,实参为n,得到的函数值输出”。

printf("\n%d!=%ld\n",n,(*p)(n));

(6)对函数指针变量进行除指向运算之外的运算是没有意义的。若p为函数指针变量,则p=p+1、p++、p--运算都是没有意义的。

例如,如果我们将例8.9的函数调用改成使用函数指针变量调用,程序可以改成:

longfunc(intn)

{longr;

if(n==0||n==1)r=1;

elser=n*func(n-1);

return(r);

}

main()

{intn;

long(*p)(); /*定义函数指针变量p,返回值类型是长整型 */

p=func; /*给函数指针变量赋值*/

printf("Pleaseinputn:");

scanf("%d",&n);

printf("\n%d!=%ld\n",n,(*p)(n));

}8.5.2用指向函数的指针作函数参数

函数指针变量经常用作函数参数传递到其他函数。当函数指针作为某函数的参数时,可以实现将函数指针所指向的函数入口地址传递给该函数。这种情况下,当函数指针指向不同函数的入口地址时,在该函数中就可以调用不同的函数,且不需要对该函数体作任何修改。

到目前为止,函数的参数可以是变量、指向变量的指针变量、数组名、指向数组的指针变量,还有函数指针以及下一章要讲到的结构体指针。

下面通过一个例子介绍函数指针作为参数。

例8.10

用复化梯形公式计算下列三个定积分值:

可以使用函数指针作为函数参数来实现。

程序如下:

#include<stdio.h>

#include<math.h>

main()

{doublecomputeT(),f1(),f2(),f3(); /*声明函数*/

double(*p)(); /*声明指向函数的指针变量p*/

p=f1;printf("s1=%f\n",computeT(10,0.0,1.0,p)); /*输出第1个积 分值*/

p=f2;printf("s2=%f\n",computeT(10,-1.0,1.0,p)); /*输出第2个积 分值*/

p=f3;printf("s3=%f\n",computeT(10,0.0,1.0,p)); /*输出第3个积 分值*/

}

doublecomputeT(n,a,b,f) /*梯形法求积分值的函数*/

intn;

doublea,b,(*f)();

{intk;doubles,h;

h=(b-a)/n;

s=((*f)(a)+(*f)(b))/2;

for(k=1;k<n;k++)s=s+(*f)(a+k*h);

s=s*h;

return(s);

}

doublef1(doublex)

{return(exp(-x*x));}

d

温馨提示

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

评论

0/150

提交评论