C语言程序设计-第7章 用函数实现模块化程序设计


模块化程序设计:事先编好一批常用的函数来实现不同的功能,需要使用时直接拿来用.而不是把所有程序代码都写在一个主函数里,用什么去写什么.

从本质意义上来说,函数就是用来完成一定功能的.

每个函数用来实现一个特定的功能.函数的名字应反映其代表的功能.

在设计一个较大的程序时,往往把它分成若干个程序模块,每一个模块包括一个或多个函数,每个函数实现一个特定的功能.一个C程序可由一个主函数和若干个其他函数构成.由主函数调用其他函数,其他函数也可以互相调用.同一个函数可以被一个或多个函数调用任意多次.

在定义函数时若函数前加void,则意为函数无类型,即无函数值,也就是说,执行这两个函数后不会把任何值带回main函数.

若自定义的函数在main函数后面,则在main函数的开头部分,应对调用的自定义函数进行声明.

所有函数都是平行的,即在定义函数时是分别进行的,是互相独立的.一个函数并不从属于另一个函数,即函数不能嵌套定义.函数间可以互相调用,但不能调用main函数.main函数是被操作系统调用的.

从用户角度看函数有两种:库函数和用户自定义的函数.

从函数形式看,函数分两类:无参函数和有参函数.

在调用无参函数时,主调函数不向被调用函数传递数据.无参函数可以带回或不带回函数值,但一般以不带回函数值居多.

在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据,一般情况下,执行被调用函数时会得到一个函数值,供主调函数使用.此时有参函数应定义为与返回值相同的类型.

定义函数应包括收下几个内容:

(1)指定函数的名字,以便以后按名调用.

(2)指定函数的类型,即函数返回值的类型.

(3)指定函数的参数的名字和类型,以便在调用函数时向它们传递数据.对无参函数不需要这项.

(4)指定函数应当完成什么操作,也就是函数是做什么的,即函数的功能.这是最重要的,是在函数体中解决的.

7.2.2定义函数的方法

1.定义无参函数

形式:

类型名 函数名()

{

函数体

}

类型名 函数名(void)

{

函数体

}

函数名后面括号内的void表示"空",即函数没有参数.

函数体包括声明部分和语句部分.

2.定义有参函数

函数名后面括号内有形式参数.

形式:

类型名 函数名(形式参数表列)

{

函数体

}

3.定义空函数

形式:

类型名 函数名()

{}

函数体是空的.调用此函数时,什么工作也不做,没有任何实际作用.

7.3 调用函数

函数调用的形式:

函数名(实参表列)

实参表列各参数间用逗号隔开.如果是调用无参函数,则括号里为空.

1.函数调用语句.2.函数表达式.3.函数参数

函数调用时的数据传递:在定义函数时函数名后面括号中的变量名称为"形式参数"(简称"形参")或"虚拟参数".在主调函数中调用一个函数时,函数名后面括号中的参数称为"实际参数"(简称"实参").实际参数可以是常量,变量或表达式.

在调用函数过程中,系统会把实参的值传递给被调用函数的形参.或者说,形参从实参得到一个值.该值在函数调用期间有效,可以参加函数中的运算.

在定义函数中的形参,在未出现函数调用时,它们并不占内存中的存储单元.在发生函数调用时,,函数的形参被临时分配内存单元.调用结束,形参单元被释放.实参单元仍保留并维持原值,没有改变.

函数的返回值是通过函数中的return语句获得的.

一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个return语句就起作用.

函数值的类型.既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型.

在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致.

如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准.对数值型数据,可以自动进行类型转换.即函数类型决定返回值的类型.

对于不带回值的函数,应当用定义函数为"void类型"(或称"空类型").这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值.此时在函数体中不得出现return语句.

7.4 对被调用函数的声明和函数原型

在一个函数中调用另一个函数,这个被调用的函数一定是之前已经有的,可以是库函数也可以是之前自己定义的函数,如果是库函数那么要在开头引入,如果是自己定义的函数且它在主调函数之后,那么要在开头对这个被调函数先声明一下,声明它的类型,函数参数的个数和参数类型等信息.

例7.4 输入两个实数,用一个函数求出它们的和.

#include <stdio.h>
#include <stdlib.h>


int main()
{
    float add(float x,float y);
    float a,b,c;
    printf("please enter a and b:");
    scanf("%f,%f",&a,&b);
    c=add(a,b);
    printf("sum is:%f\n",c);
    return 0;
}
float add(float x,float y)
{
    float z;
    z=x+y;
    return(z);
}

7.5 函数的嵌套调用

C语言的函数定义是想到平行,独立的,也就是说,在定义函数时,一个函数内不能再定义另一个函数,也就是不能嵌套定义,但可以嵌套调用函数,也就是说,在调用一个函数的过程中,又调用另一个函数.

例7.5 输入4个整数,找出其中最大的数.用函数的嵌套调用来处理.

#include <stdio.h>
#include <stdlib.h>


int main()
{
    int max4(int a,int b,int c,int d);
    int a,b,c,d,max;
    printf("please enter 4 interger numbers:");
    scanf("%d,%d,%d,%d",&a,&b,&c,&d);
    max=max4(a,b,c,d);
    printf("max=%d\n",max);
    return 0;
}
int max4(int a,int b,int c,int d)
{
    int max2(int a,int b);
    int m;
    m=max2(a,b);
    m=max2(m,c);
    m=max2(m,d);
    return(m);
}
int max2(int a,int b)
{
    if(a>b)
    {
        return a;
    }else{
        return b;
    }
}

程序改进:

#include <stdio.h>
#include <stdlib.h>


int main()
{
    int max4(int a,int b,int c,int d);
    int a,b,c,d,max;
    printf("please enter 4 interger numbers:");
    scanf("%d,%d,%d,%d",&a,&b,&c,&d);
    max=max4(a,b,c,d);
    printf("max=%d\n",max);
    return 0;
}
int max4(int a,int b,int c,int d)
{
    int max2(int a,int b);
    return max2(max2(max2(a,b),c),d);
}
int max2(int a,int b)
{
    return(a>b?a:b);
}

7.6 函数的递归调用

在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用.

例7.7 用递归方法求n!.

#include <stdio.h>
#include <stdlib.h>


int main()
{
    int fac(int n);
    int n;
    int y;
    printf("input an integer number:");
    scanf("%d",&n);
    y=fac(n);
    printf("%d=%d\n",n,y);
    return 0;
}
int fac(int n)
{
    int f;
    if(n<0)
        printf("n<0,data error!");
    else if(n==0||n==1)
        f=1;
    else f=fac(n-1)*n;
    return(f);
}

例7.8 Hanoi(汉诺)塔问题.这是一个古典的数学问题,是一个用递归方法解题的典型例子.问题是这样的:古代有一个梵塔,塔内有3个座A,B,C,开始时A座上有64个盘子,盘子大小不等,大的在下,小的在上. 有一个老和尚想把这64个盘子从A座移到C座,但规定每次只允许移动一个盘,且在移动过程中在3个座上都始终保持大盘在下,小盘在上.在移动过程中可以利用B座.要求编程序输出移动盘子的步骤.

解题思路:要把64个盘子从A座移到C座,需要移动大约264次盘子。把64个盘子从A座到C座,如果把上面63个盘子看成一个整体,那么就可以把这63个盘子移到B座,第64个盘子移到C座,然后再将63个盘子移到C座,这样问题就变成怎么把这63个盘子从A座移到B座。把这63个盘子从A座移到B座可以把上面62个盘子看成整体从A座移到C座,将第63个盘子移到B座,然后再将62个盘子从C座移到B座,这样问题就变成如何将62个盘子从A座移到C座…一次次递归下去,直到最后的递归结束条件是将一个盘子从一座移到另一座,否则递归一直进行下去。

由上面的分析可在,将n个盘子从A座移到C座可以分解为以下3个步骤:

(1)将A上n-1个盘借助C座先移到B座上;

(2)把A座上剩下一个盘移到C座上;

(3)将n-1个盘从B座借助A座移到C座上。

上面第(1)步到第(3)步,都是把n-1个盘从一个座移到另一个座上,采取的办法是一样的,只是座的名字不同而已。

上面3个步骤可以分成两类操作:

(1)将n-1个盘从一个座移到另一个座上(n>1)。这就是大和尚让小和尚做的工作,它是一个递归的过程,即和尚将任务层层下放,直到第64个和尚为止。

(2)将1个盘子从一个座上移到另一个座上。这是大和尚自己做的工作。

编写程序:分别用两个函数实现以上的两类操作,用hanoi函数实现上面第1类操作(即模拟小和尚的任务),用move函数实现上面第2类操作(模拟大和尚自己移盘),函数调用hanoi(n,one,two,three)表示盘子从“one”座移到“three”座的过程(借助“two”座)。函数调用move(x,y)表示将1个盘子从座移到y座的过程。x和y是代表A,B,C座之一,根据每次不同情况分别取A,B,C代入。


#include <stdio.h>

#include <stdlib.h>

int main()

{

void hanoi(int n,char one,char two,char three);

int m;

printf("input the number of diskes:");

scanf("%d",&m);

printf("The step to move %d diskes:\n",m);

hanoi(m,'A','B','C');

}

void hanoi(int n,char one,char two,char three)

{

void move(char x,char y);

if(n==1)

move(one,three);

else

{

hanoi(n-1,one,three,two);

move(one,three);

hanoi(n-1,two,one,three);

}

}

void move(char x,char y)

{

printf("%c-->%c\n",x,y);

}


7.7 数组作为函数参数

例 7.9 输出10个数,要求输出其中值最大的元素和该数是第几个数。

#include <stdio.h>

#include <stdlib.h>

int main()

{

int max(int x,int y);

int a[10],m,n,i;

printf("enter 10 integer numbers:");

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

{

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

}

printf("\n");

for(i=0,m=a[0],n=0;i<10;i++)

{

if(max(m,a[i])>m)

{

m=max(m,a[i]);

n=i;

}

}

printf("The largest number is %d\nit is the %dth number.\n",m,n+1);

}

int max(int x,int y)

{

return(x>y?x:y);

}


用数组元素作实参时,向形参变量传递的是数组元素的值,而用数组名作函数实参时,向形参(数组名或指针变量)传递的是数组首元素的地址。

例:7.10 有一个一维数组score,内放10个学生成绩,求平均成绩。

#include <stdio.h>

#include <stdlib.h>

int main()

{

float average(float array[10]);

float score[10],aver;

int i;

printf("input 10 scores:\n");

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

scanf("%f",&score[i]);

aver=average(score);

printf("average score is %5.2f\n",aver);

return 0;

}

float average(float array[10])

{

int i;

float aver,sum=array[0];

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

sum=sum+array[i];

aver=sum/10;

return(aver);

}


例7.11 有两个班级,分别有35名和30名学生,调用一个average函数,分别求这两个班的学生的平均成绩。

#include <stdio.h>

#include <stdlib.h>

int main()

{

float average(float array[],int n);

float score1[5]={98,93,92,96,95};

float score2[10]={77,99,33,98,34,35,87,68,32,15};

printf("The average of class A is %6.2f\n",average(score1,5));

printf("The average of class B is %6.2f\n",average(score2,10));

return 0;

}

float average(float array[],int n)

{

int i;

float aver,sum=array[0];

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

sum=sum+array[i];

aver=sum/n;

return(aver);

}


例 7.12 用选择法对数组中10个整数按由小到大排序。

#include <stdio.h>

#include <stdlib.h>

int main()

{

void sort(int array[], int n);

int a[10], i;

printf("enter array:\n");

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

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

sort(a, 10);

printf("The sorted array:\n");

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

printf("%d",a[i]);

printf("\n");

return 0;

}

void sort(int array[], int n)

{

int i, j, k, t;

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

{

k = i;

for (j = i + 1; j < n; j ++){

if (array[j] < array[k])

k = j;

}

t = array[k];

array[k] = array[i];

array[i] = t;

}

}


例 7.13 有一个 3*4的矩阵,求所有元素中的最大值。

#include <stdio.h>

#include <stdlib.h>

int main()

{

int max_value(int array[][4]);

int a[3][4] = {{1, 3, 5 ,7}, {2, 4, 6, 8}, {15, 17, 34, 12}};

printf("Max value is %d\n", max_value(a));

return 0;

}

int max_value(int array[][4])

{

int i, j, max;

max = array[0][0];

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

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

if (array[i][j] > max)

max = array[i][j];

return(max);

}


7.8 局部变量 和 全局变量

变量的作用域问题,每一个变量都有一个作用域问题,即它们在什么范围内有效。

7.8.1 局部变量

定义变量可能有3种情况:在函数的开头定义;在函数内的复合语句内定义;在函数的外部定义。

在一个函数内部定义的变量只在本函数范围内有效,也就是说只有在本函数内才能引用它们,在此函数以外是不能使用这些变量的。在复合语句内定义的变量只在本复合语句范围内有效,只有在本复合语句内才能引用它们。在该复合语句以外是不能使用这些变量的,以上这些称为“局部变量”。

主函数中定义的变量也只在主函数中有效,主函数也不能使用其他函数中定义的变量。

不同函数中可以使用同名的变量,它们代表不同的对象;互不干扰。

形式参数也是局部变量。

在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,离开该复合语句该变量就无效,系统会把它占用的内存单元释放,这种复合语句也称为“分程序”或“程序块”。

7.8.2 全局变量

程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量称为外部变量,外部变量是全局变量(也称为全程变量)。全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。

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

为了便于区别全局变量和局部变量,在C程序设计人员中有一个习惯(但非规定),将全局变量名的第1个字母用大写表示。

例 7.14 有一个一维数组,内放10个学生成绩,写一个函数,当主函数调用此函数后,能求出平均分,最高分和最低分。

解题思路:调用一个函数可以得到一个函数返回值,现在希望通过函数调用能得到3个结果。可以利用全局变量来达到此目的。

#include <stdio.h>

#include <stdlib.h>

float Max = 0, Min = 0;

int main()

{

float average(float array[], int n);

float ave, score[10];

int i;

printf("Please enter 10 scores: ");

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

scanf("%f", &score[i]);

ave = average(score, 10);

printf("max = %6.2f\nmin = %6.2f\naverage=%6.2f\n",Max,Min,ave);

return 0;

}

float average(float array[], int n)

{

int i;

float aver, sum = array[0];

Max = Min = array[0];

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

{

if (array[i] > Max) Max = array[i];

else if (array[i] < Min) Min = array[i];

sum = sum + array[i];

}

aver = sum / n;

return(aver);

}


建议不在必要时不要使用全局变量,因为:

1.全局变量在程序的全部执行过程中都占用存储单元,而还是仅在需要时才开辟单元。

2.这使函数的通用性降低了,因为如果在函数中引用了全局变量,那么执行情况会受到有关的外部变量的影响,如果将一个函数移到另一个文件中,还要考虑把有关的外部变量及其值一起移过去。但是若该外部变量与其他文件的变量同名时,就会出现问题。这就降低了程序的可靠性和通用性。

3.使用全局变量过多,会降低程序的清晰性,人们往往难以清楚地判断出每个瞬间各个外部变量的值。由于在各个函数执行时都可能改变变量的值,程序容易出错。因此,要限制使用全局变量。

例 7.15 若外部变量与局部变量同名,分析结果。

#include <stdio.h>

#include <stdlib.h>

int a = 3, b = 5;

int main()

{

int max(int a, int b);

int a = 8;

printf("max=%d\n",max(a, b));

return 0;

}

int max(int a, int b)

{

int c;

c = a > b ? a : b;

return(c);

}


7.9 变量的存储方式和生存期

7.9.1 动态存储方式和静态存储方式

从变量值的存在时间(生存期)观察变量,有的在程序运行的整个过程都是存在的,有的则在调用其所在的函数时才临时分配存储单元,而在函数调用结束后该存储单元就马上释放了,变量不存在了。

静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式,而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。

全局变量全部存放在表态存储区中,在程序执行过程中它们占据固定的存储单元,而还是动态地进行分配和释放。

在动态存储区中存放以下数据:

1 函数形式参数。在调用函数时给形参分配存储空间。

2 函数中定义的没有用关键字static声明的变量,即自动变量。

3 函数调用时的现场保护和返回地址等。

在C语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类别。

C的存储类型包括4种:自动的(auto),静态的(static),寄存器的(register),外部的(extern)。

7.9.2 局部变量的存储类别

关键字“auto”可以省略,不写auto则隐含指定为”自动存储类别“,它属于动态存储方式。

有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其战胜的存储单元不释放,在下一次南再调用该函数时,该变量已有值(就是上一次函数调用结束时的值)。这里就应该指定该局部变量为”静态局部变量“,用关键字static进行声明。

如果有一些变量使用频繁,为提高执行效率,允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种这是叫做寄存器变量,用关键字rgister作声明.

3种局部变量的存储位置是不同的:自动变量存储在动态存储区;静态局部变量存储在静态存储区;寄存器存储在CPU中的寄存器中。

如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。如果出于某种考虑,在定义占之前的函数需要引用该外部变量,则应该在引用之前用关键字extern对该变量作”外部变量声明“,表示把该外部变量的作用域扩展到此位置。有了此声明,就可以从”声明“处起,合法地使用该外部变量。

如果想在一个文件中引用另一个文件中已定义的外部变量Num,可以在作一个文件中定义外部变量Num,而在另一文件中用extern对该变量作”外部变量声明“,即”extern Num;“。在编译和连接时,系统会由此知道这个变量Num有”外部链接“,可以从别处找到已定义的外部变量Num,并将另一个文件中定义的外部变量Num的作用域扩展到本文件,在本文件中可以合法地引用外部变量Num.

在编译时遇到extern时,先在本文件中找外部变量的定义,如果找到,就在本文优缺点 扩展作用域;如果找不到,就在连接时从其他文件中找外部变量的定义。如果从其他文件中找到了,就将作用域扩展到本文件;如果再找不到,就按出错处理。

有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个static声明。

这种加上static声明,只能用于本文件的外部变量称为静态外部变量。

用static声明一个变量的作用是:

1 对局部变量作static声明,把它分配在表态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在。

2 对全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。

7.10 关于变量的声明和定义

有一个简单的结论,在函数中出现的对变量的声明(除了用extern声明的以外)都是定义。在函数中对其他函数的声明不是函数的定义。

7.11 内部函数和外部函数

根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。

内部函数又称为表态函数,因为它是用static声明的。使用内部函数,可以使函数的作用域只局限于所在文件。

通常把只能由本文件使用的函数和外部变量放在文件的开头,前面都冠以static使之局部化,其他文件不能引用。这就提高了程序的可靠性。

如果在定义函数时,在函数的首部的最左端加关键字extern,则此函数是外部函数,可供其他文件调用。

利用函数原型扩展函数作用域最常见的例子是#include指定的应用。