第九章 初窥天机之模块化程序设计

哲理:

C语言共分为两类,一类是用户自定义函数,一类是库函数。用户自定义函数是程序员在开发时根据需要,自己开发的函数。我们将会在本章进行详细的讲解。而库函数就是别人已经写好的函数库,我们只需要拿过来用就行,比如printf函数,scanf函数,以及我们在上一章讲解的和字符串处理相关的函数。

 

9.1 函数的概述

9.1.1 什么是函数

说到函数,很多不了解编程语言的人在脑中会立刻浮现出数学方面或物理方面的函数,输入变量x输出变量y。事实上,C语言函数并不是这样。“函数”是从function翻译过来的,function的英文含义就是“函数”或“功能”。函数的本质就是完成一定功能的代码。

每一个函数实现一个特定的功能,函数的名字反应其代表的功能。通过执行这些函数,实现预期的结果。如果函数是用来实现数学运算的,那么该函数就是一个数学函数。

9.1.2 为什么使用函数

假如我们要制造一部智能手机,要事先生产各种部件,如CPU,电池,摄像机,屏幕,外壳等。而在组装时,只用根据需要什么器件直接取出安装就行,而不是需要时临时再去制造。这就是采用模块化程序设计的思路。

在程序设计时,特别是在写应用软件时,所做项目往往都是一个大项目,那么这就需要,把它分成若干个子程序模块,每个模块包含一个或多个功能函数,每个函数实现一个特定的功能。当每个子模块实现的功能集合就是该项目的功能。如果把一个项目比喻成智能手机,那么各个子模块就是对应CPU,电池等。最后把这些部件组合成电脑。如图9.1所示,一个C语言程序包含一个main函数和多个其他函数。在main函数里面调用其他函数,其他函数之间是可以相互调用的,并且调用次数不限。

 

图9.1 函数调用示意图

 

程序设计中使用函数的目的就是把一个难以解决的大程序划分成若干个独立的子程序模块,每个模块解决一个或多个问题,把这些模块组合起来,形成我们软件。

 

9.2 函数的定义

9.2.1 函数原型

函数原型又称函数声明,是由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但是不必对形参命名。这三个元素被称为函数原型,函数原型描述了函数的接口。文字总是表示的不形象,下面让我们换种形式,如下:

函数返回类型 函数名(参数类型 参数,参数类型 参数,...)

{

    函数体,实现我们想要的功能;

    [return 函数返回类型];

}

这个就是函数的整体框架,任何函数都是基于这样一个框架来写的。其中,‘[]’表示可选项,就是说如果返回值类型为void,那么就不需要返回值。函数返回值类型,参数类型等可以是任意类型,比如int,float,double,char等。下面我们首先就举一个不带返回值的实例来说明一下。如下所示:

void fun(int a, int b)

{

    printf(“%d\n”, a+b);

}

该函数的功能就是输出a+b的结果。并不把结果返回去。下面我们看看带返回结果的函数。如下所示:

int fun(int a, int b)

{

    return (a+b);

}

这个函数就是带返回值的函数。

现在大家是不是对函数已经有了直观的了解呢?那么下面让我们真正的去欣赏函数的魅力吧!

 

注意:

需要注意的是函数返回值类型和return返回的类型要求一致。

 

9.2.2 函数定义的方法及分类

上一节我们知道了函数定义也是有多种形式的,比如有无返回值。那么函数到底都还有那些定义方式呢?我们接着看。

  1. 定义无返回值,有参数的函数。如9.2.1所示的是无返回值,有参数的函数定义。此处将不再举例。
  2. 定义无返回值,无参数的函数。其一般形式如下:

void 函数名()

{

    函数体;

}

看伪代码不如看真正的代码来的给力。下面是一个具体可应用的实例。

void print()

{

    printf(“Hello C Program\n”);

}

  1. 定义有返回值,无参数的函数。其一般形式如下:

函数返回值 函数名()

{

    函数体;

    return 函数返回值;

}

对于返回值,我们前面已经说了,可以是任何类型,比如:char,int,double,float等。那么具体的实例如下:

int random()

{

    return rand()%10;

}

这个实例中,我们用int作为返回值。

4. 定义有返回值,有参数的函数。其一般形式如下:

函数返回值 函数名(参数类型1 参数1,参数类型2 参数2,...)

{

    函数体;

    return 函数返回值;

}

在这个形式中,多了参数,同样我们也已经说了参数也可以是各种类型,不同的参数可以相同也可以不同。

double div(double num1,double num2,...)

{

    return num1/num2;

}

此处我们用double型参数表示。

5. 定义无返回值,有参数的函数。其一般形式如下:

void 函数名(参数类型1 参数1,参数类型2 参数2,...)

{

    函数体;

}

同样我们还是举一个例子,

void printChar(char c)

{

    printf(“%d\n”, c);

}

这个函数的功能就是输出字符的ASCII值。

 

这五个是典型的函数具体形式,接下来我们看看如何调用函数吧!

 

9.3 函数的调用

9.3.1 函数声明

首先,我们什么都不说直接上一个程序来直观的进行函数声明的讲解。

例9.1】简单函数调用实例。

#include <stdio.h>
void printMessage();//函数声明
int main()
{
	printMessage();//main函数调用printMessage函数
	return 0;
}
void printMessage()//函数
{
	printf("----------------------\n");
	printf("         *            \n");
	printf("        * *           \n");
	printf("       *   *          \n");
	printf("        * *           \n");
	printf("         *            \n");
	printf("----------------------\n");
}

这个程序就是一个函数调用的实例。在这里我们就要讲讲什么是函数声明了。

在C语言中编译系统是由上而下进行编译的,如果被调用函数放在调用函数的后面,则需要对被调用函数在调用函数前面进行说明。否则编译器无法识别函数,并且出现调用失败。

比如我们上面例9.1所示,函数printMessage为被调用函数,main函数为调用函数。现在观察发现被调用函数printMessage放在了调用函数main函数的后面,所以我们需要在第二行进行声明(第二行是函数声明),否则会出现编译错误。大家可以尝试把第二行去掉去编译一下,多多尝试,就会收获多多。尝试吧,少年!!!

那么写函数就一定需要函数声明吗?答案是:否定,滴!你只需要把被调用函数放到调用函数之前,C语言在自上而下编译时,就会默认读到还没有调用的函数时,就会默认声明该函数。

聪明的你一定会发现在这个程序中,第2行函数声明,和函数第8~17行中的第8行除了最后多了一个分号外,是完全一模一样啊!不错!这就是函数的声明,当你把一个函数模块写好,只需要把该模块的第一行,复制一份放到头文件下面,再加一个“;”,那么你的函数声明就完成了,是不是很简单啊!其实程序就是这么简单。

注意:

1. 建议无论被调用函数放到哪里,尽量都进行函声明。

2. 函数可以声明一次,但可以多次调用。

 

9.3.2 函数调用

其实,上一节实例9.1中我们就编写函数的调用程序。在程序第五行调用printMessage函数。我们这一节会更加详细的讲解函数的调用。

首先我们此处写一个main函数三个被调函数,第一个函数输出一行“+”号,第二个函数输出一个“hello world”字符串,第三个函数输出数字。

第一个函数,输出“+”号。

void printAdd()

{

    printf(“+++++++++++++++++++\n”);

}

第二个函数,输出“hello world”。

int printStr()

{

    int a,b;

    a=2;

    b=3;

    printf(“   a*b=%d\n”, a*b);

    return a*b;

}

第三个函数,输出数字。

void ifEven(int num)

{

    if(num%2 == 0)

    {

        printf(“%d是偶数!\n”, num);

    }

    else

    {

    printf(“%d是奇数!\n”, num);

    }

}

上面是三个被调函数的编写,下面让我们编写主函数。

int main()

{

}

现在函数都已经写好,我们也知道函数的执行是从main函数开始的,也就是说现在我们需要把上面两个函数模块写到下面的main函数中去。

1. 首先我们写成如下程序:

int main()

{

    printfAdd();

    return 0;

}

运行程序发现输出的结果是一个字符串,如下:

++++++++++++++++++++++

Press any key to continue

2. 接着我们改成程序成如下形式

int main()

{

    int a;

    a = printStr();

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

    return 0;

}

运行结果如下

   a*b=6

   a*b=6

Press any key to continue

  1. 最后我们把第三个函数写出来,以便后面的对比。

int main()

{

    int d=5;

    ifEven(d);

    return 0;

}

运行结果如下

5是奇数!

Press any key to continue

 

从程序1中我们发现,对于无参数,无返回值的函数,我们只需要把函数名写到main函数中即可实现调用,当然后面的“()”不要忘了。

从程序2中我们发现,对于返回值,无参数的函数,我们就需要定义一个和函数返回值类型相同的变量接收返回值。正如程序2中,函数的返回值类型是int型,那么我们在main函数中定义一个int型变量a,接收函数的返回值。当然变量也可以不接收,或者定义成其他类型变量,不过此处我们暂时不考虑,后文会有涉及。大家现在只需按规定来就行。

从程序3中我们发现函数无返回值,有参数,我们只需要定义一个与参数类型相同的变量传递到函数中去即可。其中d为5这个值就相当如赋值给函数ifEven中的num值,这个点我们将会在后面讲到。

上面三个程序,就是我们讲解的函数调用,相信大家通过对比三个程序的结果,就会对函数调用有更详细的了解了,不过这些都是基础,大家还需要努力哟!!!

关于函数的返回值的讲解我们将会在下节给大家进行详细的介绍,而带参数的函数,我们也会在后面做详细的讲解。大家到时候就可以详细的了解这些知识了。

此处,我们把上面的函数做一个简单的整理,把代码详细的列出来,一遍大家能够参考。

【例9.2】函数调用程序。

#include <stdio.h>
void printAdd();
int printStr();
void ifEven(int num);
void printAdd()
{
	printf("++++++++++++++++++++++\n");
}
int printStr()
{
	int a,b;
	a=2;
	b=3;
	printf("      a*b=%d\n", a*b);
	return a*b;
}
void ifEven(int num)
{
	if(num%2 == 0)
	{
		printf("      %d是偶数!\n", num);
	}
	else
	{
		printf("      %d是奇数!\n", num);
	}
}
int main()
{
	int c,d=5;
	printAdd();
	c = printStr();
	printf("      a*b=%d\n", c);
	ifEven(d);
	printAdd();
	return 0;
}

运行结果:

++++++++++++++++++++++

      a*b=6

      a*b=6

      5是奇数!

++++++++++++++++++++++

Press any key to continue

 

 

9.3.3函数返回值

上一节我们介绍了函数的整数返回值,大家已经对函数的返回值有了一个大概的理解了,这一节我们还将继续讲解函数的返回值。

首先,我们会不会有一个问题,那就是为什么要使用函数的返回值呢

此处写有关函数返回值的定义

 

函数的返回值有多种类型,几乎所有的类型都可以作为返回值返回。比如void,int,double,float,枚举类型,结构体类型,指针类型,甚至自定义类型等等。

那么函数的返回值需要注意什么呢?我们做一下四点说明。

(1) 函数的返回值是通过函数中的return语句获得。如例9.2第32行所示,变量c就是从函数printStr()的return中获取。那么我们接着观察这个程序的15和36行,我们发现15行返回的是一个表达式,36行返回的是一个整数0,那么这就说明return既可以返回数值,也可以返回一个表达式。

 

  • 函数值的类型。在上一节我们举过例子int类型的,说明返回值可以是int类型,当然也可以是float,double等任何数据类型,这就说明,如果要返回某个数据或某些数据,需要这些数据有确定的数据类型。比如以下四个函数所示:

int length(char str[]);

int max(int x, int y);

double min(double x, double y);

void print(char str[]);

 

  • 函数的返回值尽量要与定义的函数类型保持一致。这样不会造成数据值的改变,丢失或错误。比如如下一段函数:

int length(char str[])

{

    int i=0;

    while(str[i] != ‘\0’)

    {

        i++;

    }

    return i;

}

这个函数的返回值为int型,函数的定义类型也是int型,这就是函数我们所说的“函数的返回值尽量要与定义的函数类型保持一致”。假如我们return一个double类型的变量,而函数的定义类型是int型,那么这就会造成数据精度的缺失,会把高精度的double型数据变量默认转化为低精度的int型数据。

 

事实上,在程序9.2中,ifEven函数已经实现了函数的数据传递,该函数传递的是整型变量。我们也9.4 函数调用中的数据传递

可以传递其他类型的变量。那么数组可以传递吗?聪明的你已经猜出来,不错的确可以。形如int length(char str[]),其中的“char str[]”就是传递的字符型的一维数组。同理二维,三维也是完全可以的。我们将会在下面做详细的介绍,不过在介绍之前,我们应该先介绍一下关于函数传递的基础知识了。

 

9.4.1 形式参数和实际参数

什么是形式参数?什么又是实际参数?此处卖个关子,我们先来看一段程序。

【例9.3】通过函数传参显示数组长度。

#include <stdio.h>
int length(char str[])
{
	int i=0;
	while (str[i] != '\0')
	{
		i++;
	}
	return i;
}
int main()
{
	char s[]="I Love C Program";
	int len = 0;
	len = length(s);
	printf("数组长度:%d\n", len);
	return 0;
}

运行结果:

数组长度:16

Press any key to continue

 

观察这个程序,首先第二行:int length(char str[]),这个函数中,str就是形式参数,又称为“形参”。char则为形参的类型。其实,形式参数和实际参数是对应的,那么调用函数中对应的数据就是实际参数了。程度第15行中的s就是实际参数。相当于把实际参数s赋值给形式参数str。实际参数又称为“实参”。

通过这个程序我们知道调用函数中的参数为实际参数,而被调函数中的参数为形式参数。我们把函数模块换一种形式书写会更加清楚了:

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

{

    函数体

}

 

说明:

  1. 实际参数可以是常量,变量,甚至是表达式。在之前的例子中我们已经讲过,此处不再细说。
  2. 实参和形参的数据类型应该相同或者兼容。比如我们定义一个字符型数据类型,用一个整型变量接受你的字符型数据。如下:

char getChar()

{

    return ‘A’:

}

int a = getChar();

 

 

9.4.2 函数值传递

我们主要从使用角度介绍C语言的函数传值。我们介绍的多为应用而非理论,以求大家能够通过实例掌握C语言。废话不多说,我们接着说,理论!嗨!呜呜!

函数数据传递方式分为两类:值传递引用传递

值传递:数据只能从实参单向传递给形参,成为“按值”传递。当基本类型变量作为实参时,在函数调用过程中,形参和实参占据不同的存储空间,形参的改变对实参的值不产生影响。本节我们主要讲函数的值传递。

引用传递:使实参和形参共用一个地址,即所谓“引用传递”。这种传递方式,无论对那个变量进行修改,都是对同一地址的内容进行修改,实参变量与它的引用变量,总是具有相同的值。函数的引用传递我们将会在下一节进行讲解。

在前几节关于函数值传递我们已经有所涉及,比如例9.2就是传递了一个整数,然后判断是奇数还是偶数,并输出结果。这个是普通的值传递。

例9.4】实现两个数的加减乘除运算。要求用函数调用实现。

解题思路:首先,我们写四个函数,分别是:加法函数,减法函数,乘法函数,除法函数。然后,用switch做一个可选项,选择要进行那种计算。最后,把结果值返回主函数,并输出结果。需注意,在做除法运算时,被除数不能为0。

编写程序:

#include <stdio.h>
void print();
double add(double a, double b);
double sub(double a, double b);
double mult(double a, double b);
double div(double a, double b);
void print()
{
	printf("****        四则运算        ****\n");
	printf("--------------------------------\n");
	printf("            1. 加法             \n");
	printf("            2. 减法             \n");
	printf("            3. 乘法             \n");
	printf("            4. 除法             \n");
	printf("--------------------------------\n");
	printf(">>>");
}
double add(double a, double b)
{
	return (a+b);
}
double sub(double a, double b)
{
	return (a-b);
}
double mult(double a, double b)
{
	return (a*b);
}
double div(double a, double b)
{
	return (a/b);
}
int main()
{
	int choose;
	double a, b, result;
	print();
	scanf("%d", &choose);
	switch(choose)
	{
	case 1:
		{
			printf("请输入a, b值,以逗号分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			result = add(a, b);
			printf("%lf + %lf=%lf\n", a, b, result);
		}
		break;
	case 2:
		{
			printf("请输入a, b值,以逗号分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			result = sub(a, b);
			printf("%lf - %lf=%lf\n", a, b, result);
		}
		break;
	case 3:
		{
			printf("请输入a, b值,以逗号分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			result = mult(a, b);
			printf("%lf * %lf=%lf\n", a, b, result);
		}
		break;
	case 4:
		{
			printf("请输入a, b值,以逗号分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			if (b<1e-6 && b>-1e-6)
			{
				printf("b值不能为0。\n");
				return 0;
			}
			result = div(a, b);
			printf("%lf / %lf=%lf\n", a, b, result);
		}
		break;
	default:printf("输入选项错误,请重新输入。\n");
	}
	return 0;
}

运行结果:

****      四则运算     ****

----------------------------------------

          1. 加法

          2. 减法

          3. 乘法

          4. 除法

----------------------------------------

>>>2

请输入a, b值,以逗号分隔(比如1.0,3.0):8.0,5.0

8.000000 - 5.000000=3.000000

Press any key to continue

 

程序分析:我们把加减乘除写成函数调用的形式。方便大家对本节函数值传递的理解。

 

9.4.3 函数引用传递之指针变量

什么是指针变量?简单的说,就是在定义定义变量时,在变量名前面添加一个“*”。指针变量的定义方式如下:

变量名 *指针类型;

比如:int *p或者double *pt。这就是指针变量的定义。了解这些基本的知识我们就可以讲解关于指针变量的引用传递了。

如果函数的形式为指针类型时,对应的实参类型必须与形参的基类型相同。我们以下面这个实例进行说明。

例9.5】通过指针变量的引用传递带回函数中的变量值。

解题思路:首先定义一个交换变量的函数,函数中变量定义为指针变量,在调用函数中用传地址的方式,传递变量值。并把函数中要交换的两个变量带回主函数,输出结果。

编写程序:

#include <stdio.h>
void swap(int *a, int *b);
void swap(int *a, int *b)
{
	int temp=0;
	temp = *a;
	*a = *b;
	*b = temp;
}
int main()
{
	int a=6, b=5;
	printf("a = %d, b = %d\n", a, b);
	swap(&a, &b);
	printf("a = %d, b = %d\n", a, b);
	return 0;
}

运行结果:

a = 6, b = 5

a = 5, b = 6

Press any key to continue

 

程序分析:观察程序第3行和第14行,它们分别为被调函数和调用函数。我们首先看第14行,如果调用函数中,如果要采用引用的方式,那么被调函数需要用指针变量的形式定义形参。那么就是第3行的定义方式。程序第13行和15行分别是执行被调函数前和被调函数后的结果。从结果中我们看出,如果采用指针传递的形式,会把被调用函数中的变量带回主函数中。这就出现了程序的运行结果部分。

函数之间值的传递是单向传递,也就是说函数只能通过实参把值传递给形参,若形参值改变,对实参不会产生影响;把数据从被调函数返回到调用函数的唯一途径就是通过return语句,且只能返回一个数据。若采用传递地址值的方式,即可以在被调函数中对调用函数中的变量进行引用,也可以把被调函数中改变的值传回给调用函数。因此,通过改变形参的值,而让实参的值也发生相应的改变,这样就可以把多个数据从被调函数中返回调用函数(主函数)中。

 

9.4.4 函数引用传递之一维数组

我们从前面知道,在函数调用时,实际参数可以是常量、变量或表达式。其实,数组也是一种变量,那么数组也是可以被调用的,用法与变量相同。另外,数组名既可以做实参,又可以做形参。在传递的是数组的第一个元素的地址。例9.3使用了一维数组调用。事实上,数组作为函数参数传递时分为两种方式:一种是数组元素作为函数实参传递,一种是函数名作为参数传递。下面我们将对这两种进行详细说明。

 

一、 数组元素作为函数实参传递

在主函数中把数组元素作为实参传递传递给被调函数的形参时,称为“值传递”。另外,数组元素只能作为实参,而不能当做形参。因为数组在内存中是连续的一段存储单元,不可能为一个数组元素单独分配存储单元。

【例9.6】输入一组学生成绩,输出最高成绩。

解题思路:首先,输入一组成绩,保存到数组中。然后,通过两两比较找出最大的那个成绩,并输出结果。

编写程序:

#include <stdio.h>
double maxGrade(double x, double y);
double maxGrade(double x, double y)
{
	return (x>y?x:y);
}
int main()
{
	double grade[10];
	double maxValue;
	int i;
	printf("请输入10个学生的成绩。\n");
	for ( i=0 ; i<10 ; i++ )
	{
		scanf("%lf", &grade[i]);
	}
	for (i=1, maxValue=grade[0]; i<10; i++)
	{
		maxValue = maxGrade(maxValue, grade[i]);		
	}
	printf("最高成绩是:%0.1lf\n", maxValue);
	return 0;
}

 

运行结果:

请输入10个学生的成绩。

88.5 75 96 61 82 78.5 73 91 74.5 86

最高成绩是:96.0

Press any key to continue

 

程序分析:通过这个程序希望大家加深理解函数的数组元素的传递。程序第19行,传递的就是grade数组中的数组元素。这个程序中有一个小技巧,那就是我们每次把最大的值直接赋值给maxValue,这样每一次调用maxGrade函数后,获取的就是最大值,直至整个成绩数组比较完成。其中,初试默认grade[0]为最大值。

 

 

二、 一维数组名作为参数传递

之前我们讲到函数之间进行数据传递时,数组元素可以作为实参传递给形参,这时的数组元素与普通变量一样,这种传递本质就是值传递。除此之外,还可以用数组名作函数参数(包括实参和形参)。我们已知用数组元素作为实参时,对形参传递的是一个数值,而用数组名作为函数实参时,对形参传递的是数组首元素的地址(数组的起始地址)。

【例9.7】通过传递数组名,实现数组赋值。

解题思路:在主函数中定义一个一维数组,然后定义一个函数,把主函数中的数组通过把数组名作为实参的形式,传递给该函数,在该函数中实现数组初始化的功能。并在主函数中输出最终结果。

编写程序:

#include <stdio.h>
int main()
{
	void InitArr(int arr[], int count);
	int arr[5], i;
	InitArr(arr, 5);
	for (i=0; i<5; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}
void InitArr(int arr[], int count)
{
	int i;
	for (i=0; i<count; i++)
	{
		arr[i] = i+1;
	}
}

运行结果:

1 2 3 4 5

Press any key to continue

 

程序分析:在该程序中,我们发现第4行的函数声明与以往把函数声明写到头文件之下有所不同,这也是一种函数声明的方式,这种声明方式将不用考虑把函数模块写在main函数之前还是之后,或者说函数声明之前还是函数声明之后(注:另一种方式函数模块必须放到函数声明之后)。程序第6行以及第14行~21行中我们经过函数调用发现,如果要把一维数组作为实参传递给形参,那么在调用函数中我们只需要写入一维数组名,在被调函数中我们需要在给相同类型数组名定义时,数组名后面需添加“[]”,这样表示一维数组。如果是二维数组名传递,那么就需要添加两个“[][]”,并在最后一个“[]”中写上对应的列数,比如a[5][5],那么形参就写成“a[][5]”。

 

【例9.8】通过函数传递实现计算学生的平均成绩。

解题思路:与例9.7的解题思路类似,也是先定义一个函数用于成绩的输入,再定义一个函数实现计算成绩平均值,最后在主函数中输出成绩的平均值。

编写程序:

#include <stdio.h>
int main()
{
	void InitArr(double arr[], int count);
	double AverageScore(double arr[], int count);
	double grade[100], result;
	int num;
	printf("你要计算几个学生的数学平均成绩:");
	scanf("%d", &num);
	printf("请分别输入这%d个学生的成绩:", num);
	InitArr(grade, num);
	printf("平均成绩计算中...\n");
	result = AverageScore(grade, 5);
	printf("这%d个学生的平均成绩为:%.1lf\n", num, result);
	return 0;
}
void InitArr(double arr[], int count)
{
	int i;
	for (i=0; i<count; i++)
	{
		scanf("%lf", &arr[i]);
	}	
}
double AverageScore(double arr[], int count)
{
	int i;
	double sum=0;
	for (i=0; i<count; i++)
	{
		sum = sum + arr[i];
	}
	return (sum/count);
}

运行结果:

你要计算几个学生的数学平均成绩:5

请分别输入这5个学生的成绩:66 78 83.5 90 89.5

平均成绩计算中...

这5个学生的平均成绩为:81.4

Press any key to continue

 

程序分析:对于这个程序大家是不是有一种小系统的感觉呢?继续努力我们将会写出更大的系统。但是,不积跬步无以至千里。所以大家要学好类似的小程序才行啊。该程序的函数声明还是和例9.7类似。第一个函数模块实现了输入学生的初试成绩的功能。第二个函数模块实现了计算学生平均成绩的功能。程序第20~23行实现依次输入学生的成绩。程序第29~32行实现把所有学生的成绩相加至sum中。程序第33行,返回成绩的平均值,即成绩和除以总人数。

 

在数组名作为函数调用的过程中需要注意一下几点:

  1. 实参和形参类型尽量保持一致。
  2. 实参数组和形参数组的大小保持一致。
  3. 如果是一维数组,形参可以不指定大小,但是一定要在形参名后添加“[]”。
  4. 一维数组传递的是第一个数组元素的地址。

 

9.4.5 函数引用传递之二维数组

无论一位数组元素还是一维数组名都可以作为函数参数,那么多维数组可以吗?毫无疑问,当然可以。接下来我们主要以二维数组为例,进行讲解多维数组作为函数参数的使用。  

要讲解二维数值作为实参传递给给形参时,我们需要先看看二维数组长什么样子,以方便我们理解二维数组的传递。

比如,我们定义一个4*5的二维数组作为实参。

    int array[4][5];

那么,把该二维数组传递给形参时,形参是如何定义的呢?形参可以定义为如下两种形式:

    int array[4][5] 或者 int array[][5]。

但是,不能定义为:

    int array[][] 或者 int array[4][]。

那是因为二维数组是由若干一位数组组成,在内存中数组是按行存放的。因此,在定义二维数组时,必须指定列数,即每一行中包含多少个元素,并且实参和形参数据类型相同,所以它们是由具有相同长度的一维数组所组成。

【例9.9】二维数组名作为实参进行初始化

解题思路:经过上面的讲解,我们该程序就会使用上面两种合法的定义形式来进行讲解。一种形式是int array[4][5],另一种形式是int array[][5]。该程序主要通过函数定义与输出函数来对应上面的两种形式的定义。

编写程序:

#include <stdio.h>
int main()
{
	void InitArr(int arr[][5], int count);
	void Print(int arr[5][5], int count);
	int arr[5][5];
	InitArr(arr, 5);
	Print(arr, 5);
	return 0;
}
void InitArr(int arr[][5], int count)
{
	int i, j;
	for (i=0; i<count; i++)
	{
		for (j=0; j<count; j++)
		{
			arr[i][j] = i*count+j+1;
		}
	}
}
void Print(int arr[5][5], int count)
{
	for (int i=0; i<count; i++)
	{
		for (int j=0; j<count; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

运行结果:

1 2 3 4 5

6 7 8 9 10

11 12 13 14 15

16 17 18 19 20

21 22 23 24 25

Press any key to continue

 

程序分析:程序第11~21行和程序第22~32行就是两个数组调用表现形式,它们的声明分别为第3行和第4行。首先,程序第11~21行表示第一种形参的接受形式,即int arr[][5]。由于程序是按照行存放的,所以当确定列数时,行数自然就确定了。然后,程序第22~32行表示第二种形参的接受形式,即int arr[5][5]。这个形式直接就确定了接受的行数和列数,所以不需要去特殊的讲解。

 

9.5函数的嵌套调用

C语言中不允许做嵌套的函数定义。因此各函数之间是平行的,不存在上一级函数和下一级函数的问题。但是C语言允许在一个函数的定义中出现对另一个函数的调用。这样也就出现了函数的嵌套调用,即在被调用函数中有调用其他函数。这与其他语言的子程序嵌套的情形是类似的。

图9.2 多层函数模块嵌套调用

 

如图9.2所示多层嵌套,其执行过程如下:

  1. 执行main函数至调用函数1。
  2. 执行调用函数1至调用函数2。
  3. 这样依次执行,,,
  4. 一直执行到函数N。
  5. 由函数N返回上一层调用函数N-1。
  6. 依次返回至函数2。
  7. 由函数2返回函数1。
  8. 继续执行main函数的剩余部分直到结束。

 

【例9.10】计算之和。

解题思路:首先,主函数我们只需要输入要求这四个数据。然后,在第一个调用函数中我们计算“+”两边的和。接着,计算每个数次方的值。最后,依次返回上一级函数,直至返回带主函数中输出结果。

编写程序:

#include <stdio.h>
int calc_sum(const int, const int, const int, const int);
int calc_fact(const int base,const int num);
int calc_sum(const int a,const int num1, const int b, const int num2)
{
	return (calc_fact(a, num1)+calc_fact(b,num2));
}
int calc_fact(const int base,const int num)
{
	if (num<=0)
	{
		return 1;
	}
	return base*calc_fact(base, num-1);
}
int main()
{
	int a,b,num1,num2, result=0;
	printf("请输入两个数值及对应的阶乘:");
	scanf("%d%d%d%d", &a, &b, &num1, &num2);
	result = calc_sum(a,num1, b, num2);
	printf("result:%d\n", result);
	return 0;
}

运行结果:

请输入两个数值及对应的阶乘:2 3 3 2

result:17

Press any key to continue

 

程序分析:程序第2~3行是两个函数的声明。在main函数中接受要输入的数据,如程序20行。然后调用函数calc_sum()函数计算立方与平方的和,就是8和9。接着进入calc_sum()函数,调用calc_fact()函数计算每个数据的值,就是计算2的立方和3的平方。通过递归调用该函数,计算出想要的结果,具体递归调用我们将会在下一节进行讲解。最后,把获取的值依次返回到主函数中,输出结果。

图9.3 calc_fact()函数流程图

以2的立方为例,我们看一下具体的calc_fact()函数的流程图。当把底数2和指数3传递到该函数后,判断3是不是小于等于0,显然3大于0,程序执行到2*calc_fact(2,3-1),此时calc_fact(2,3) = 2*calc_fact(2,3-1),同时函数第二个参数减1,变成calc_fact(2,2)。然后,函数calc_fact()接受变量2,判断2是不是小于等于0,显然不是,接着是执行到2*calc_fact(2,2-1),此时calc_fact(2,2)=2*calc_fact(2,2-1),同时第二个参数减1,变成calc_fact(2,1)。接着,函数calc_fact()接受变量1,判断1是不是小于等于0,显然不是,接着执行到2*calc_fact(2,1-1),此时calc_fact(2,1)=2*calc_fact(2,1-1),同时第二个参数减1,变成calc_fact(2,0),显然0小于等于0。那么calc_fact(2,0)=1。依次放回到原来的数据:

calc_fact(2,0)=1;

calc_fact(2,1)=2*calc_fact(2,0)=2*1=2;

calc_fact(2,2)=2*calc_fact(2,1)=2*2=4;

calc_fact(2,3)=2*calc_fact(2,2)=2*4=8;

 

最后把calc_fact(2,3)返回到函数calc_sum()中,就是程序的第6行:

    return (calc_fact(a,num1)+calc_fact(b,num2));

也就是:

    return (calc_fact(2,3)+calc_fact(3,2));

也就是:

    return (8+calc_fact(3,2));

用相同的方法计算calc_fact(3,2),获得的值为9。

那么:

    return (8+9);

就会主函数中获得calc_sum()的返回值,就是程序第21行,result值就是17。最后输出结果。

 

9.6 函数递归调用

一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归调用。执行递归调用将反复调用其自身,每调用一次就进入新的一层。

在上一节函数嵌套调用中的实例程序9.10,函数calc_fact()就是递归函数。如果把图9.2中的函数1,函数2,…,函数N,看做是同一个函数的话,那就是函数的递归调用。

函数调用的大概过程:

  1. 将调用函数的上下文入栈;
  2. 调用被调用的函数;
  3. 被调函数执行;
  4. 调用函数上下文出栈,继续执行后继指令。

所以在函数调用过程中调用函数是不会退出的,被调函数的内存只有在返回调用函数后才会释放。

 

【例9.11】用递归求出n!。

解题思路:首先n!=1•2•3…n,那么使用递推的方法:

1!=1;

2!=2•1;

3!=3•2•1;

n!=n•(n-1)•(n-2)…2•1;

因此,基于数学的方法我们总结出递归公式:

编写程序:

#include <stdio.h>
int main()
{
	int fact(int);
	int num = 0, n;
	printf("请输入一个整数:");
	scanf("%d", &n);
	num = fact(n);
	printf("%d!=%d\n", n, num);
	return 0;
}
int fact(int n)
{
	if(n<=1)
	{
		return 1;
	}
	return n*fact(n-1);
}

运行结果:

请输入一个整数:5

5!=120

Press any key to continue

 

程序分析:程序第4行为递归函数的声明,程序第8行调用递归函数。本程序的核心就是第12~19行的递归函数,如图9.4所示的递归流程图,执行顺序为1,2,3,...,10。main函数中第一次传递到递归函数,此时n是5,然后开始执行递归。n=5不满足n<=1,执行n*fact(n-1),即5*fact(4)。由于要返回5*fact(4)的值,可是fact是函数,不是确定值,所以要继续执行该函数。此时n=4不满足n<=1,执行n*fact(n-1),即4*fact(3)。同样fact是函数,继续执行fact函数。此时n=3不满足n<=1,执行n*fact(n-1),即3*fact(2)。同理,直到执行到n=1时,满足n<=1,执行fact(1),返回整数1,以此返回上一级,2*1,3*2*1,4*3*2*1,5*4*3*2*1,最后获取结果120。

如下为递归程序的执行流程:

图9.4 fact函数递归流程图

递归的基本原理:

  1. 每次函数调用都会有返回值,当程序执行到某一级递归的结尾处时,它会转移到前一级递归的下一条命令继续执行。
  2. 递归函数中,位于递归调用前的语句和各级被调函数具有相同的顺序。
  3. 每一级函数调用都有自己的私有变量。
  4. 递归函数中,位于递归调用语句后的语句的执行顺序和各个被调用函数的顺序相反。
  5. 虽然每一级递归有自己的变量,但是函数代码并不会得到复制。
  6. 递归函数中必须包含可以终止递归调用的语句。

 

9.7 变量的作用域和生存周期

9.7.1 变量属性

变量也有属性,变量的属性共分为六种,它们分别是:名称、地址/左值、值/右值、存储类型、作用域、生存周期。下面我们将详细介绍这几种方式。

 

  • 名称

名称定义类似于标识符的定义,当多个名字访问同一存储地址时,就称这些名字为别名。但是如果使用别名过多不利于程序的可读性,然而却存在于任何一门语言中。

例如:

int a = 1;

int b = a;

 

其中,b作为a的别名,都指向同一个地址,值为1。当a的值发生改变后,b却不会改变。我们会发现很多时候多个函数有相同的参数名。

 

  • 地址/左值

计算机中所有的数据都是存放在存储器中的,一般把存储器中的一个字节称为一个存储单元。为了能够正确的访问这些存储单元,需要为每个存储单元编号,根据编号就可以准确的找到该内存单元。内存单元的编号称为地址。

比如上面的a就是地址,地址a指向的存储空间存放的就是1。

 

  • 值/右值

值即变量的值,表示与该变量相关联的存储单元的内容。变量的值有时候也称为变量的右值,因为变量的值常被用于赋值语句的右边。比如:a=1,表示左边变量a接收右边变量的值1的赋值。

 

  • 存储类型

变量的存储类型指系统针对变量存储方式的规定。根据系统的存储方式可以分为两类。一类是静态存储方式。一类是动态存储方式。

  1. 静态存储方式:指在程序运行期间,系统对变量固定地分配存储空间。即一旦分配,不在变化直到整个程序运行结束。比如:int a[5],表示静态的为变量a申请5个int型的存储空间长度。
  2. 动态存储方式:指在程序运行期间,系统对变量动态地分配存储空间。即程序运行期间,可以根据程序需求,动态分配。比如:通过malloc等函数动态申请内存空间。

 

存储类型既说明了变量的存储单元,有说明了变量的生存的时间和作用域。对于存储类型有四种限定符。在此之前,我们需要了解一个事实:在某一个程序文件中定义的全局变量和函数均默认为外部的,即跨文件的。

  1. 自动变量auto:指不加说明的局部变量。变量生存周期结束由系统自动释放其存储空间,所以称为自动。
  2. 寄存器变量register:为提高程序执行效率,允许将局部变量的值存放于寄存器中(注意不是内存中),因为寄存器有限,所以不提倡把所有的变量都存放与寄存器中。事实上,如果存放过多数据到寄存器中,执行效率不会提高,程序还是会自动把大部分变量放到内存中。
  3. static变量:可作用于局部变量和全局变量,故可分为:局部静态和全局静态。静态说的是:生存周期。而局部或全局说的是:作用域。
  4. 以extern声明的变量:指全局变量,若要在其他文件中使用,需要加以声明,方法:使用前用extern作外部声明即可。通常放于文件开头,并且对于函数而言,通常省略关键字extern。

 

  • 作用域

变量的作用域是指变量的有效范围,它从空间角度体现变量的特性。变量的作用域细分可分为六种:全局变量作用域、局部变量作用域、语句作用域、类作用域、命名空间作用域、文件作用域。

常用的就是全局变量作用域和局部变量作用域。

 

  • 生存周期

变量的生存周期指从变量创建到删除所经历的时间段,它是从时间角度衡量变量的特性。其生存周期共分为三类。

  1. 动态生存期:指存放在“堆区”中的数据。创建、删除均有程序员自己完成。
  2. 局部生存期:指存放在“栈区”中的数据。
  3. 静态生存期:指存放在“数据区”中的数据。程序一运行,它们就存在;程序一结束,它们由系统自动释放。

 

9.7.2 局部变量

本文通过程序来说明什么是局部变量。

【例9.12】局部变量之计算正方形,长方形,梯形的面积。

解题思路:通过定义三个函数以及主函数来说明什么是局部变量。

编写程序:

#include <stdio.h>
double square(double edge)
{
	double area = edge*edge;
	return area;
}
double rectangle(double length, double width)
{
	double area = length*width;
	return area;
}
double trapezoid(double upper, double bottom, double height)
{
	double area = (upper+bottom)*height/2;
	return area;
}
int main()
{
	double a=2.4, b=3.6, c=4.8;
	double squareArea,rectangleArea,trapezoidArea;
	squareArea = square(a);
	printf("Square area:%lf\n", squareArea);
	rectangleArea = rectangle(a, b);
	printf("Rectangle area:%lf\n", rectangleArea);
	trapezoidArea = trapezoid(a, b, c);
	printf("Trapezoid area:%lf\n", trapezoidArea);
	return 0;
}

运行结果:

Square area:5.760000

Rectangle area:8.640000

Trapezoid area:14.400000

Press any key to continue

 

程序分析:首先在main函数中定义double型变量的有:a,b,c,squareArea,rectangleArea,trapezoidArea六个局部变量。它们的作用范围就在main函数中,其他函数中无法使用这六个变量。当main函数结束时,这六个变量自动释放。同样在square函数中,局部变量为形参edge和area,它们随着square函数的调用而产生,结束而释放,作用范围就在square函数中,其他函数中无法使用这两个变量。针对rectangle函数和trapezoid函数也是同理。

通过上面程序的说明我们发现,在一个函数内部定义的变量只在该函数范围内有效,即只能在该函数中被使用,其他函数不能使用这些变量,如上例中所示,这些变量称为局部变量。还有一种局部变量定义的方式就是复合语句内定义。在复合语句内定义的变量只能在复合语句内使用,在复合语句外不能使用这些变量,这些变量也称为局部变量。

比如:

void func()

{

    int a;

    ┇

    {

        int b;

        ┇

    }

}

其中,变量b就是复合语句内的变量,即复合语句内的局部变量。变量b只在复合语句中有效。

 

此处有几点说明:

  1. 不同函数中可以使用同名的变量,它们互不影响。例如上面程序中的变量area。
  2. 形参也是局部变量。例如上面程序中的edge,length,width等变量也是局部变量。
  3. 函数内部的复合语句内也可以定义局部变量,这些变量的使用范围就在复合语句内。

 

9.7.3 全局变量

在上一节中,我们已经介绍了局部变量,局部变量就是在函数内部定义的变量称为局部变量,而在函数外部定义的变量则称为全局变量,或者外部变量。全局变量作用范围是从定义变量开始到该源文件结束,即全局变量可以被本文件中其他函数共同使用。

【例9.13】全局变量之计算正方形,长方形,梯形的面积。

解题思路:通过定义三个函数以及主函数来说明什么全局变量。

编写程序:

#include <stdio.h>
double a=2.4, b=3.6, c=4.8;
double squareArea,rectangleArea,trapezoidArea;
double area;
void square(double edge)
{
	area = edge*edge;
}
void rectangle(double length, double width)
{
	area = length*width;
}
void trapezoid(double upper, double bottom, double height)
{
	area = (upper+bottom)*height/2;
}
int main()
{
	square(a);
	printf("Square area:%lf\n", area);
	rectangle(a, b);
	printf("Rectangle area:%lf\n", area);
	trapezoid(a, b, c);
	printf("Trapezoid area:%lf\n", area);
	return 0;
}

运行结果:

Square area:5.760000

Rectangle area:8.640000

Trapezoid area:14.400000

Press any key to continue

 

程序分析:该实例程序中定义几个double型的全局变量:a,b,c,squareArea,rectangleArea,trapezoidArea,area。在改程序中它们的作用范围是从定义变量开始直到整个程序结束,即程序结束时,这七个全局变量自动释放。那么在square函数中的形参edge和area仍然是局部变量,它们随着square函数的调用而产生,结束而释放,作用范围就在square函数中,其他函数中无法使用这两个变量。针对rectangle函数和trapezoid函数也是同理。但是这三个函数中的area却是全局变量,我们不仅可以在这三个函数中使用,也可以在main函数中使用。比如square函数中把计算的结果赋值给area,然后在main函数中输出area的值,这就是全局变量的使用。当然也可以在main函数中计算area的值,然后在被调函数中输出结果,这就形成了常用的“显示函数”。

针对什么是局部变量?什么是全局变量?可以用一句话概括:在函数内部定义的变量叫做局部变量,在函数外定义的变量叫做全局变量。在一个函数中可以同时存在局部变量和全局变量。

建议在不明白的情况下,不要过多的使用全局变量,它会使程序的可读性、清晰性、通用性等会大大降低。因为如果在一个源程序中针对某一处的全局变量的值进行改变,那么整个程序中该全局变量的值对于另一处的结果造成不利的影响。准确说例9.13是一个很不严谨的程序,就是因为全局变量的不当使用。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值