C语言入门系列之7.函数的定义、参数、调用和存储类别

一、概述

1.函数基本概念

一个较大的程序可分为若干个程序模块,每一个模块用来实现一个特定的功能。
在高级语言中用子程序实现模块的功能,子程序由函数来完成。
一个C程序可由一个主函数和若干个其他函数构成。

函数间的调用关系可能如下:
函数间的调用关系
由主函数调用其他函数,其他函数也可以互相调用,同一个函数可以被一个或多个函数调用任意多次。

简单测试如下:

#include <stdio.h>

int main(){
	void printstar();
	void print_mseeage();
	
	printstar();
	print_message();
	printstar();
	
	return 0;
}

void printstar(){
	printf("*******************\n");
}

void print_message(){
	printf("Hello World!!!\n");
}

打印:

*******************
Hello World!!!
*******************

2.函数说明

(1)一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。
对较大的程序,一般不希望把所有内容全放在一个文件中,而是将它们分别放在若干个源文件中,再由若干源程序文件组成一个C程序,这样便于分别编写、分别编译,提高调试效率。
一个源程序文件可以为多个C程序共用。

(2)一个源程序文件由一个或多个函数以及其他有关内容(如命令行、数据定义等)组成。
一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。

(3)C程序的执行是从main函数开始的,在main函数中调用其他函数,在调用后流程返回到main函数,最终在main函数中结束整个程序的运行。

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

(5)从用户使用的角度看,函数有两种:

  • 标准函数
    即库函数,是由系统提供的,用户不必自己定义这些函数,可以直接使用它们。
    不同的C系统提供的库函数的数量和功能会有一些不同,许多基本的函数是共同的。
  • 用户自定义函数
    用以解决开发者的特定需要。

(6)从函数的形式看,函数分两类:

  • 无参函数
    如前面的printstar和print_message是无参函数。
    在调用无参函数时,主调函数不向被调用函数传递数据。
    无参函数一般用来执行指定的一组操作,例如前面的printstar函数。
  • 有参函数
    在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据,一般情况下,执行被调用函数时会得到一个函数值,供主调函数使用。

二、函数定义的一般形式

1.定义无参函数

定义无参函数的一般形式为:

类型标识符 函数名()
{
	声明部分
	语句部分
}

在定义函数时要用类型标识符指定函数值的类型,即函数带回的值的类型。
如前面例子的printstar和print_message函数为void类型,表示不需要带回函数值。

2.定义有参函数

定义有参函数的一般形式为:

类型标识符 函数名(形式参数表列)
{
	声明部分
  	语句部分
}

例如:

int max(int x, int y){
	int z;
	z = x > y ? x : y;
	return z;
}

3.定义空函数

定义空函数的一般形式为:

类型标识符 函数名()
{}

例如:

dummy()
{}

调用此函数时,什么工作也不做,没有任何实际作用;
在主调函数中写上dummy()表明这里要调用一个函数,而现在这个函数没有起作用,等以后扩充函数功能时补充上。

空函数时默认类型是int。

三、函数参数和函数的值

1.形参与实参概念

形式参数和实际参数:
在有参函数中,在定义函数时函数名后面括号中的变量名称为形式参数(简称形参);
主调函数中调用一个函数时,函数名后面括号中的参数(可以是一个表达式)称为实际参数(简称实参)。

大多数情况下,主调函数和被调用函数之间有数据传递的关系。

return后面的(括号中的)值作为函数带回的值,称为函数返回值。

在不同的函数之间传递数据,可以使用的方法:

  • 参数
    通过形式参数和实际参数。
  • 返回值
    用return语句返回计算结果。
  • 全局变量
    外部变量。

2.形参与实参的说明

(1) 在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。只在发生了函数调用时,函数max中的形参才被分配内存单元。在调用结束后,形参所占的内存单元也被释放。

(2) 实参可以是常量、变量或表达式,如max(3, a + b),但要求它们有确定的值,在调用时将实参的值赋给形参。

调用函数时的数据传递练习如下:

#include <stdio.h>

int main(){
	int max(int x, int y);
	int a, b, c;
	scanf("%d %d", &a, &b);
	c = max(a, b);
	printf("Max is %d", c);
	
	return 0;
}

int max(int x, int y){
	int z = x > y ? x : y;
	return z;
}

打印:

23 32
Max is 32

运行原理如下:
调用函数时的数据传递

通过函数调用,使两个函数中的数据产生联系。

(3) 在被定义的函数中,必须指定形参的类型。

(4) 实参与形参的类型应相同或赋值兼容。
前面的例子中实参和形参都是整型。
如果实参为整型而形参为实型,或者相反,则按不同类型数值的赋值规则进行转换。
例如实参值a为3.5,而形参b为整型,则将实数3.5转换成整数3,然后送到形参b;
字符型与整型可以互相通用。

(5) 在C语言中,实参向对形参的数据传递是值传递(相当于COPY),单向传递,只由实参传给形参,而不能由形参传回来给实参,如下:
形参与实参说明5(1)

在内存中,实参单元与形参单元是不同的单元。
在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参,调用结束后,形参单元被释放,实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数的实参的值。
例如,若在执行函数过程中x和y的值变为10和15,而a和b仍为2和3,如下:
形参与实参说明5(2)

3.函数的返回值

通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数的返回值。
例如,在前面的例子中,max(2, 3)的值是3,max(5, 2)的值是5,赋值语句将这个函数值赋给变量c。

函数的返回值是通过函数中的return语句获得的:
return语句将被调用函数中的一个确定值带回主调函数中去;
如果需要从被调用函数带回一个函数值供主调函数使用,被调用函数中必须包含return语句;
如果不需要从被调用函数带回函数值可以不要return语句。

一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个语句起作用。
return语句后面的值可以有括号,也可以没有括号,如return z;等价于return (z);

return后面的值可以是一个表达式,如:

int max(int x, int y){
	return (x > y) ? x : y;
}

4.函数返回值的注意点

(1)函数的返回值应当属于某一个确定的类型,在定义函数时指定函数返回值的类型。
例如,下面3个函数的首行即指定了函数返回值的类型:

int max(float x, float y);     		/* 函数值为整型 */
char letter(char c1, char c2);   	/* 函数值为字符型 */ 
double min(int x, int y);      		/* 函数值为双精度型 */

在C语言中,凡不加类型说明的函数,自动按整型处理,如前面的max函数首行的函数类型int可以省写,但是建议在定义时对所有函数都指定函数类型。

(2)在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致。
如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准,即函数类型决定返回值的类型;
对数值型数据,可以自动进行类型转换。

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

返回值类型与函数类型不同的测试:

#include <stdio.h>

int main(){
	int max(float x, float y);
	float a, b;
	int c;
	scanf("%f %f", &a, &b);
	c = max(a, b);
	printf("Max is %d", c);
	
	return 0;
}

int max(float x, float y){
	return x > y ? x : y;
}

打印:

2.3 3.2
Max is 3

显然,此时输入浮点数,却返回了整数,发生了强制类型转换,损失了精度,可以进行改进如下:

#include <stdio.h>

int main(){
	float max(float x, float y);
	float a, b, c;
	scanf("%f %f", &a, &b);
	c = max(a, b);
	printf("Max is %f", c);
	
	return 0;
}

float max(float x, float y){
	return x > y ? x : y;
}

打印:

2.3 3.2
Max is 3.200000

显然,此时可以正常输出,不会发生精度的损失。

四、函数的调用

1.函数调用的一般形式

函数调用的一般形式为:

函数名(实参表列)

如果是调用无参函数,则实参表列可以没有,但括号不能省略
如果实参表列包含多个实参,则各参数间用逗号隔开;
实参与形参的个数应相等,类型应匹配;
实参与形参按顺序对应,一一传递数据。

如果实参表列包括多个实参,对实参求值的顺序并不是确定的,有的系统按自左至右顺序求实参的值,有的系统则按自右至左顺序。

许多C版本是按自右而左的顺序求值,例如Tubro C++。

实参求值的顺序测试如下:

#include <stdio.h>

int main(){
	int f(int a, int b);
	int i = 2, p;
	p = f(i, ++i);
	printf("%d\n", p);
	
	return 0;
}

int f(int a, int b){
	int c;
	if(a > b){
		c = 1;
	}
	else if(a == b){
		c = 0;
	}
	else{
		c = -1;
	}
	return c;
}

对于该程序,两种调用方式的分析:

int i = 2, p;
p = f(i, ++i);

如果按自左至右顺序求实参的值,则函数调用相当于f(2, 3);
如果按自右至左顺序求实参的值,则函数调用相当于f(3, 3);
运行后打印,打印:

0

显然,说明本系统的执行顺序是自右向左的。

2.函数调用的方式

按函数在程序中出现的位置来分,有三种函数调用方式:

  • 函数语句
    把函数调用作为一个语句。
    如前面的printstar(),这时不要求函数带回值,只要求函数完成一定的操作。
  • 函数表达式
    函数出现在一个表达式中,这种表达式称为函数表达式,这时要求函数带回一个确定的值以参加表达式的运算。
    例如c = 2 * max(a, b);
  • 函数参数
    函数调用作为一个函数的实参。
    例如m = max(a, max(b, c));max(b, c)是一次函数调用,它的值作为max另一次调用的实参,m的值是a、b、c三者中的最大者;
    又如printf("%d", max(a,b));也是把max(a, b)作为printf函数的一个参数。
    函数调用作为函数的参数,实质上也是函数表达式形式调用的一种,因为函数的参数本来就要求是表达式形式。

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

在一个函数中调用另一函数(即被调用函数)需要具备以下条件:
(1)首先被调用的函数必须是已经存在的函数(是库函数或用户自己定义的函数)。

(2)如果使用库函数,应该在本文件开头用#include命令将调用有关库函数时所需用到的信息包含到本文件中。

(3)如果使用自己定义的函数,而该函数的位置在调用它的函数(即主调函数)的后面(在同一个文件中),应该在主调函数中对被调用的函数作声明。

声明一词的英文是declaration,作用是把函数名、函数参数的个数和参数类型等信息通知编译系统,以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法(例如函数名是否正确,实参与形参的类型和个数是否一致)。

函数的定义声明不是一回事:
函数的定义是指对函数功能的确立,包括指定函数名、函数值类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位,是占内存的;
函数的声明的作用则是把函数名、函数类型、形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查,是不占内存的。

在声明函数时,可以不加形参名,即int f(int, int);也是合法的,但是建议还是要加上形参名。

对被调用的函数作声明:

#include <stdio.h>

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

float add(float x, float y){
	return x + y;
}

打印:

2.3 3.2
sum is 5.500000

如果被调用函数的定义出现在主调函数之前,可以不必加以声明,因为编译系统已经先知道了已定义函数的有关情况,会根据函数首部提供的信息对函数的调用作正确性检查。

例如,在上面的程序中,如果将add()函数定义在main()函数前面,可以不用在main()函数中声明add()函数,如下:

#include <stdio.h>

float add(float x, float y){
	return x + y;
}

int main(){
	float a, b, c;
	scanf("%f %f", &a, &b);
	c = add(a, b);
	printf("sum is %f\n", c);
	
	return 0;
}


但是建议还是采用第一种方式,书写更规范。

练习:
实现pow()函数。
代码如下:

#include <stdio.h>

int main(){
	double power(double a, double b);
	double a, b;
	scanf("%lf %lf", &a, &b);
	double result = power(a, b);
	printf("Result = %lf", result);
	
	return 0;
}

double power(double a, double b){
	int i;
	double result = 1.0;
	for(i = 1; i <= b; i++){
		result *= a;
	}
	return result;
}

打印:

2.0 3.0
Result = 8.000000

除了用for循环,还可以用while循环,如下:

#include <stdio.h>

int main(){
	double power(double a, double b);
	double a, b;
	scanf("%lf %lf", &a, &b);
	double result = power(a, b);
	printf("Result = %lf", result);
	
	return 0;
}

double power(double a, double b){
	int i;
	double result = 1.0;
	while(b--){
		result *= a;
	}
	return result;
}

效果一样。

练习:
对整型实现sqrt()函数。
代码如下:

#include <stdio.h>

int main(){
	int sqroot(int n);
	int a;
	scanf("%d", &a);
	int result = sqroot(a);
	if(result < 0){
		printf("Error, %d has no int root!!", a);
	}
	else{
		printf("Result = %d", result);
	}
	
	return 0;
}

int sqroot(int n){
	int temp = n / 2;
	while(temp){
		if(temp * temp == n){
			return temp;
		}
		temp--;
	}	
	return -1;
}

打印:

36
Result = 6

练习:
编写一个用来统计输入的各个数字、空白符(空格、制表符、换行符)以及所以其他字符出现次数的程序。
代码如下:

#include<stdio.h>

int c, i, nspace, other;

int ndigit[10];
	
int main(){
	void count(char c);
	nspace = other = 0;
	for (i = 0; i < 10; ++i){
		ndigit[i] = 0;
	}
	
	while (c = getchar()){
		count(c);
		if(c == '\n'){
			break;
		}
	}	
	
	printf("digits =");
	for (i = 0; i < 10; i++){
		printf(" %d", ndigit[i]);
	}	
	printf("\nspace = %d\nother = %d\n", nspace, other);
	
	return 0;
}

void count(char c){
	if(c >= '0' && c <= '9'){
		ndigit[c - '0']++;
	}	
	else if(c == ' ' || c == '\t'){
		nspace++;
	}
	else if(c == '\n'){
		nspace++;
	}	
	else{
		other++;
	}
}

打印:

zxcvb12345      !@#$% QWERT
digits = 0 1 1 1 1 1 0 0 0 0
space = 8
other = 15

五、函数的嵌套调用

嵌套定义是在定义一个函数时,其函数体内又包含另一个函数的完整定义。
C语言不能嵌套定义函数,但可以嵌套调用函数,也就是说,在调用一个函数的过程中,又调用另一个函数。
如下:

main(){
	a();
}

a(){
	b();
}

b(){
	return;
}

该程序的执行过程如下:
嵌套函数执行过程

练习:
计算s = 2²! + 3²!(!即阶乘,4! = 4 * 3 * 2 * 1)。
实现思路:
可编写两个函数,一个是用来计算平方值的函数sqr,另一个是用来计算阶乘值的函数fact。
主函数先调sqr计算出平方值,再在sqr中以平方值为实参,调用fact计算其阶乘值,然后返回sqr,再返回主函数,在循环程序中计算累加和。
代码如下:

#include <stdio.h>

int main(){
	int sqr(int m);
	int i;
	long s = 0;
	for(i = 2; i <= 3; i++){
		s += sqr(i);
	}
	printf("result = %ld\n", s);
	
	return 0;
}

int sqr(int m){
	long fact(int n);
	return fact(m * m);
}

long fact(int n){
	int result = 1;
	while(n){
		result *= n;
		n--;
	}
	return result;
}

打印:

result = 362904

六、函数递归

在调用一个函数的过程中又直接或间接地调用该函数本身,称为函数的递归调用,C语言的特点之一就在于允许函数的递归调用。
例如:

int f(int x){
	int y, z;
	z = f(y);
	return 2 * z;
}

递归必须要有一个退出的条件,否则会一直调用自己,不能退出、陷入死循环。

练习:
用递归的方法求n!。
实现思路:
用递归方法,即5!等于4!*5,而4!等于3!*4=…,n!可用下面的递归公式表示:

1 (n=0、1);
n*(n-1)! (n > 1)。

程序中给出递归函数,主函数调用该函数后即递归执行;
如果n<0、n==0或n=1时都将结束函数的执行,否则就递归调用函数自身;
由于每次递归调用的实参为n-1,即把n-1的值赋予形参n,最后当n-1的值为1时再作递归调用,形参n的值也为1,将使递归终止,然后可逐层退回。
代码如下:

#include <stdio.h>

int main(){
	long fact(int n);
	int n;
	long result;
	printf("Input a integer number:\n");
	scanf("%d", &n);
	result = fact(n);
	if(result){
		printf("Result = %d\n", result);
	}
	else{
		printf("Input Error!!\n");
	}	
	
	return 0;
}

long fact(int n){
	long result;
	if(n < 0){
		result = 0;
	}
	else if(n == 1 || n == 0){
		result = 1;
	}
	else{
		result = n * fact( n - 1);
	}
	return result;
}

打印:

Input a integer number:
5
Result = 120

前面也用了迭代法来实现阶乘,即从1开始乘以2,再乘以3…直到n,递推法比递归法更容易理解和实现。
递归算法是效率低下的算法,但是有些问题则只能用递归算法才能实现。

练习:
Hanoi(汉诺)塔问题:
古代有一个梵塔,塔内有3个座A、B、C,开始时A座上有64个盘子,盘子大小不等,大的在下,小的在上,如下图:
汉诺塔问题示意

有一个老和尚想把这64个盘子从A座移到C座,但每次只允许移动一个盘,且在移动过程中在3个座上都始终保持大盘在下,小盘在上。
在移动过程中可以利用B座,要求编程序打印出移动的步骤。
实现思路:
为便于理解,我们先分析将A座上3个盘子移到C座上的过程:
(1) 将A座上2个盘子移到B座上(借助C);
(2) 将A座上1个盘子移到C座上;
(3) 将B座上2个盘子移到C座上(借助A)。
其中第(2)步可以直接实现,第1步又可用递归方法分解为:
1.1 将A上1个盘子从A移到C;
1.2 将A上1个盘子从A移到B;
1.3 将C上1个盘子从C移到B。
第(3)步可以分解为:
3.1 将B上1个盘子从B移到A上;
3.2 将B上1个盘子从B移到C上;
3.3 将A上1个盘子从A移到C上。

将以上综合起来,可得到移动3个盘子的步骤为:
A→C,A→B,C→B,A→C,B→A,B→C,A→C。

由上面的分析可知:将n个盘子从A座移到C座可以分解为以下3个步骤:
(1) 将A上n-1个盘借助C座先移到B座上。
(2) 把A座上剩下的一个盘移到C座上。
(3) 将n-1个盘从B座借助于A座移到C座上。

代码如下:

#include <stdio.h>

int main(){
	void hanoi(int n, char start, char mid, char end);
	int n;
	printf("Input the number of disks:\n");
	scanf("%d", &n);
	hanoi(n, 'A', 'B', 'C');	
	
	return 0;
}

void hanoi(int n, char start, char mid, char end){
	void move(char x, char y);
	if(n == 1){
		move(start, end);
	}
	else{
		hanoi(n - 1, start, end, mid);
		move(start, end);
		hanoi(n - 1, mid, start, end);
	}
}

void move(char x, char y){
	printf("%c -> %c\n", x, y);	
}

打印:

Input the number of disks:
5
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
A -> B
C -> B
C -> A
B -> A
C -> B
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
B -> A
C -> B
C -> A
B -> A
B -> C
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

显然,输出了移动的步骤。

七、数组作为函数参数

数组可以作为函数的参数使用,进行数据传递,数组作函数参数有两种形式:

  • 把数组元素(下标变量)作为实参使用;
  • 把数组名作为函数的形参和实参使用。

1.数组元素作为函数实参

数组元素就是下标变量,它与普通变量并无区别,因此它作为函数实参使用与普通变量是完全相同的,在发生函数调用时,把作为实参的数组元素的值传送给形参,实现单向的值传递。

练习:
判别一个整数数组a[10] = {1, 2, 3, 4, -4, -3, -2, -1, 5, -5}中各元素的值,若大于0则输出该值,若小于等于0则输出0值。
代码如下:

#include <stdio.h>

int main(){
	void arrayprint(int n);
	int a[10] = {1, 2, 3, 4, -4, -3, -2, -1, 5, -5};
	int i;
	for(i = 0; i< 10; i++){
		arrayprint(a[i]);
	}
	
	return 0;
}

void arrayprint(int n){
	if(n > 0){
		printf("%d ", n);
	}
	else{
		printf("%d ", 0);
	}
}

打印:

1 2 3 4 0 0 0 0 5 0

2.数组名作函数参数

用数组名作函数参数与用数组元素作实参有几点不同:
(1)用数组元素作实参时,只要数组类型和函数的形参变量的类型一致,那么作为下标变量的数组元素的类型也和函数形参变量的类型是一致的。因此,并不要求函数的形参也是下标变量。换句话说,对数组元素的处理是按普通变量对待的。
用数组名作函数参数时,则要求形参和相对应的实参都必须是类型相同的数组,都必须有明确的数组说明。当形参和实参二者不一致时,就会发生错误。
(2)在普通变量或下标变量作函数参数时,形参变量和实参变量是由编译系统分配的两个不同的内存单元。在函数调用时发生的值传送是把实参变量的值赋予形参变量。
在用数组名作函数参数时,不是进行值的传送,即不是把实参数组的每一个元素的值都赋予形参数组的各个元素,因为实际上形参数组并不存在,编译系统不为形参数组分配内存。因为数组名就是数组的首地址,因此在数组名作函数参数时所进行的传送只是地址的传送,也就是说把实参数组的首地址赋予形参数组名。形参数组名取得该首地址之后,也就等于有了实在的数组。实际上是形参数组和实参数组为同一数组,共同拥有一段内存空间,所以形参数组可以不指定数组大小,实参数组的大小就是形参数组的大小。

测试如下:

#include <stdio.h>

int main(){
	void arraytest(int b[10]);
	int a[10] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
	arraytest(a);
	putchar('\n');
	
	return 0;
}

void arraytest(int b[10]){
	int i = 0;
	for(;i < 5;i++){
		printf("%4d", b[i]);
	}
}

打印:

   2   4   6   8  10

内存数组的存储示意如下:
内存数组的存储

练习:
有一个一维数组score,存放10个学生的成绩,求平均成绩(写一个average函数求平均成绩)。
代码如下:

#include <stdio.h>

int main(){
	double average(double scores[10]);
	double scores[10] = {85, 92, 65, 77, 99, 56, 78, 90, 56, 83};
	double avg = average(scores);
	printf("Average score is %4.2lf", avg);
	putchar('\n');
	
	return 0;
}

double average(double scores[10]){
	int i = 0;
	double sum = 0;
	for(;i < 10;i++){
		sum += scores[i];
	}
	return sum / 10;
}

打印:

Average score is 78.10

形参数组不指定大小测试:

#include <stdio.h>

int main(){
	double average(double scores[10]);
	double scores[10] = {85, 92, 65, 77, 99, 56, 78, 90, 56, 83};
	double avg = average(scores);
	printf("Average score is %4.2lf", avg);
	putchar('\n');
	
	return 0;
}

double average(double scores[]){
	int i = 0;
	double sum = 0;
	for(;i < 10;i++){
		sum += scores[i];
	}
	return sum / 10;
}

运行结果与之前相同。

八、局部变量和全局变量

1.局部变量

在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的,这称为局部变量
如下:

float f1( int a)           	/* 函数f1 */
{
	int b,c;/* a、b、c有效 */
} 

char f2(int x,int y)       	/* 函数f2 */
{
int i,j;                 	/* x、y、i、j有效 */
}

void main()               	/* 主函数 */
{
	int m,n;/* m、n有效 */
}  

局部变量的注意点:

(1) 主函数中定义的变量也只在主函数中有效,不会因为是在主函数中定义而在整个文件或程序中有效,主函数也不能使用其他函数中定义的变量。
如上面的m、n只在主函数中有效。

(2) 不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干扰。
例如,上面在f1函数中定义了变量b和c,倘若在f2函数中也定义变量b和c,它们在内存中占不同的单元,互不混淆。

(3) 形式参数也是局部变量。
例如上面f1函数中的形参a,也只在f1函数中有效,其他函数可以调用f1函数,但不能引用f1函数的形参a。

(4) 在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也称为分程序程序块
如下:

void main()
{
	int a,b;{
		int c;
    	c=a+b;  /* c在此范围内有效,a、b在此范围内有效 */}}  

2.全局变量

在函数内定义的变量是局部变量,而在函数之外定义的变量称为外部变量,外部变量是全局变量(也称全程变量)。
全局变量可以为本文件中其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。
如下:

int p=1,q=5;                /* 外部变量,以下全局变量p、q的作用范围 */
float f1(int a)            	/* 定义函数f1 */
{
	int b,c;}

char c1,c2;                 /* 外部变量,以下是全局变量c1、c2的作用范围 */
char f2 (int x, int y)      /* 定义函数f2 */
{
	int i,j;}

void main ( )               /* 主函数 */
{
	int m,n;}   

练习:
输入正方体的长宽高l、w、h,求体积及三个面的面积x*y、x*z、y*z。
代码如下:

#include <stdio.h>

int s1, s2, s3;

int main(){
	int va(int a, int b, int c);
	int l, w, h, v;
	printf("Input length, width, height:\n");
	scanf("%d %d %d", &l, &w, &h);
	v = va(l, w, h);
	printf("volume = %d, s1 = %d, s2 = %d, s3 = %d", v, s1, s2, s3);
	
	return 0;
}

int va(int a, int b, int c){
	s1 = a * b;
	s2 = b * c;
	s3 = a * c;
	return a * b * c;  
}

打印:

Input length, width, height:
3 4 5
volume = 60, s1 = 12, s2 = 20, s3 = 15

练习:
有一个一维数组,内放10个学生成绩,写一个函数,求出平均分、最高分和最低分。
代码如下:

#include <stdio.h>

int Min, Max;

int main(){
	float average(float array[], int n);
	float avg, scores[10];
	int i;
	for(i = 0; i < 10; i++){
		scanf("%f", &scores[i]);
	}
	avg = average(scores, 10);
	printf("Max = %d, Min = %d, Average = %f", Max, Min, avg);
	
	return 0;
}

float average(float array[], int n){
	int i;
	float avg, sum = array[0];
	Max = Min = array[0];
	for(i = 1; i < n; i++){
		Max = array[i] > Max ? array[i] : Max;
		Min = array[i] < Min ? array[i] : Min;
		sum += array[i];
	}
	avg = sum / n;
	
	return avg;
}

打印:

76 93 82 67 54 99 83 72 91 78
Max = 99, Min = 54, Average = 79.500000

建议不在必要时不要使用全局变量,原因如下:
(1)全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。

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

(3)它使函数的通用性降低了,因为函数在执行时要依赖于其所在的外部变量。
如果将一个函数移到另一个文件中,还要将有关的外部变量及其值一起移过去;但若该外部变量与其他文件的变量同名时,就会出现问题,降低了程序的可靠性和通用性。
一般要求把C程序中的函数做成一个封闭体,除了可以通过实参——形参的渠道与外界发生联系外,没有其他渠道。

九、变量的存储类别

1.动态存储方式与静态存储方式

从变量的作用域(即从空间)角度来分,可以分为全局变量和局部变量;
从变量值存在的时间(即生存期)角度来分,又可以分为静态存储方式和动态存储方式。
静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式;
而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。

用户存储空间可以分为三部分:

  • 程序区
  • 静态存储区
  • 动态存储区

在C语言中每一个变量和函数有两个属性:

  • 数据类型;
  • 数据的存储类别。

对数据型(如整型、字符型等),存储类别指的是数据在内存中存储的方式。
存储方式分为两大类:

  • 静态存储类;
  • 动态存储类。

具体包含四种:

  • 自动的(auto)
  • 静态的(static)
  • 寄存器的(register)
  • 外部的(extern)

根据变量的存储类别,可以知道变量的作用域和生存期。

2.auto变量

函数中的局部变量,如不专门声明为static存储类别,都是动态地分配存储空间的(栈),数据存储在动态存储区中。
函数中的形参和在函数中定义的变量(包括在复合语句中定义的变量),都属此类,在调用该函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间,因此这类局部变量称为自动变量
自动变量用关键字auto作存储类别的声明。
例如:

int f(int a){
	auto int b, c = 3;		/* 定义b、c为自动变量 */
	……
}

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

3.用static声明局部变量

有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即其占用的存储单元不释放,在下一次该函数调用时,该变量已有值,就是上一次函数调用结束时的值,这时就应该指定该局部变量为静态局部变量,用关键字static进行声明。
静态局部变量使用测试如下:

#include <stdio.h>

int main(){
	int statictest(int a);
	int a = 2, i;
	for(i = 0; i < 3; i++){
		printf("%d\n", statictest(a));
	}
	
	return 0;
}

int statictest(int a){
	auto int b = 0;
	static int c = 3;
	b += 1;
	c += 1;
	
	return a + b + c; 
}

打印:

7
8
9

显然,因为statictest()函数中c声明为static,所以在一次函数执行结束后,c变量未被销毁,保留当前的值,下次调用函数时即使用保留在内存中的c值。

静态变量的注意点

(1)静态局部变量属于静态存储类别,在静态存储区内分配存储单元,在程序整个运行期间都不释放
而自动变量(即动态局部变量)属于动态存储类别,占动态存储区空间而不占静态存储区空间,函数调用结束后即释放

(2)对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值,以后每次调用函数时不再重新赋初值而是保留上次函数调用结束时的值;
而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。

(3)如在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符变量);
而对自动变量来说,如果不赋初值则它的值是一个不确定的值,这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。

(4)虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。

练习:
输出1到5的阶乘值。
代码如下:

#include <stdio.h>

int main(){
	int fact(int n);
	int i;
	for(i = 1;i <= 5;i++){
		printf("%d! = %d\n", i, fact(i));
	}
	
	return 0;
}

int fact(int n){
	static int f = 1;
	f *= n;
	return f;
}

打印:

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120

4.register变量(寄存器变量)

一般情况下,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的,当程序中用到哪一个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中,经过运算器进行运算,如果需要存数,再从运算器将数据送到内存存放,如下:
内存与运算器

如果有一些变量使用频繁(例如在一个函数中执行10000次循环,每次循环中都要引用某局部变量),则为存取变量的值要花费不少时间。
为提高执行效率,C语言允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。
由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率,这种变量叫做寄存器变量,用关键字register声明

对前面的代码改进如下:

#include <stdio.h>

int main(){
	long fact(int n);
	int i;
	for(i = 1;i <= 10;i++){
		printf("%d! = %d\n", i, fact(i));
	}
	
	return 0;
}

long fact(int n){
	register long i, f = 1;
	for(i = 1; i <= n; i++){
		f *= i;
	}
	
	return f;
}

打印:

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800

与auto类型相比,可以看到运行速度明显地加快。

5.用extern声明外部变量

外部变量即全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾。
在此作用域内,全局变量可以为程序中各个函数所引用。编译时将外部变量分配在静态存储区。
可以用extern来声明外部变量,以扩展外部变量的作用城。

练习如下:

#include <stdio.h>

int main(){
	int max(int a, int b);
	extern A, B;
	printf("Max = %d", max(A, B));
	
	return 0;
}

int max(int a, int b){
	return a > b ? a : b;
}

int A = 12, B = 20;

打印:

Max = 20

显然,因为A、B变量的声明和初始化是在使用这两个变量之后,如果在主函数中不声明extern,在编译时就找不到这两个变量,就会报错。

扩展--在多文件程序中声明外部变量

在同一目录下创建testsub.c和testmain.c文件,testsub.c如下:

extern A;	//声明A为已定义的外部变量 

int power(int n){
	int i, y = 1;	
	for(i = 1; i<= n; i++){
		y *= A;
	}
	
	return y;
}

testmain.c如下:

#include <stdio.h>
#include "test1.c"

int A;
int main(){
	int power(int);
	int b = 3, c, d, n;
	printf("Input number A and n:\n");
	scanf("%d %d", &A, &n);
	c = A * b;
	printf("%d * %d = %d\n", A, b, c);
	d = power(n);
	printf("%d ^ %d = %d\n", A, n, d);
	
	return 0;
}

运行testmain.c即主函数,打印如下:

Input number A and n:
12 3
12 * 3 = 36
12 ^ 3 = 1728

显然,要想调用其他文件中的函数,只需要在主文件开头导入头文件即可。

6.用static声明外部变量

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

static int A;                   
void main()
{} 

file2.c:

extern int A;
void fun (int n){
	A=A*n;
}

运行后会报错。

练习:
改变maintest.c如下:

#include <stdio.h>
#include "test1.c"

static int A;
int main(){
	int power(int);
	int b = 3, c, d, n;
	printf("Input number A and n:\n");
	scanf("%d %d", &A, &n);
	c = A * b;
	printf("%d * %d = %d\n", A, b, c);
	d = power(n);
	printf("%d ^ %d = %d\n", A, n, d);
	
	return 0;
}

再运行,会报错:

4	12	XXX\testmain.c	[Error] static declaration of 'A' follows non-static declaration
2	0	XXX\testmain.c
1	8	XXX\testsub.c	[Note] previous declaration of 'A' was here

显然,因为在外部声明了static,所以A不能被其他文件引用,所以在编译testsub.c时会报错。

7.变量的声明和定义

对变量而言,声明与定义的关系稍微复杂一些。
在声明部分出现的变量有两种情况:一种是需要建立存储空间的,如int a;,另一种是不需要建立存储空间的,如extern a;,前者称为定义性声明(defining declaration) ,或简称定义(definition),后者称为引用性声明(referencing declaration)。
广义地说,声明包括定义,但并非所有的声明都是定义,对int a;而言,它既是声明、又是定义,而对extern a;而言,它是声明而不是定义。
一般为了叙述方便,把建立存储空间的声明称定义,而把不需要建立存储空间的声明称为声明,这里指的声明是狭义的,即非定义性声明。
例如:

void main()
{
	extern A;
}
int A; 

这里是声明不是定义,声明A是一个已定义的外部变量。

8.内部函数和外部函数

函数本质上是全局的,因为一个函数要被另外的函数调用,但是也可以指定函数不能被其他文件调用。
根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。

内部函数

如果一个函数只能被本文件中其他函数所调用,它称为内部函数。
在定义内部函数时,在函数名和函数类型的前面加static,即static 类型标识符 函数名(形参表),如static int fun(int a, int b)

外部函数

(1) 在定义函数时,如果在函数首部的最左端加关键字extern,则表示此函数是外部函数,可供其他文件调用。
如函数首部可以写为extern int fun(int a, int b),这样,函数fun就可以为其他文件调用。
C语言规定,如果在定义函数时省略extern,则隐含为外部函数。
在需要调用此函数的文件中,用extern对函数作声明,表示该函数是在其他文件中定义的外部函数。

主程序miantest.c如下:

#include <stdio.h>
#include "file1.c"
#include "file2.c"
#include "file3.c"

int A;
int main(){
	extern void enter_string(char str[]);
	extern void delete_string(char str[], char ch);
	extern void print_string(char str[]);
	
	char c;
	char str[80];
	enter_string(str);
	scanf("%c", &c);
	delete_string(str, c);
	print_string(str);
	
	return 0;
}

file1.c如下:

#include <stdio.h>

void enter_string(char str[80]){
	gets(str);
}

file2.c如下:

void delete_string(char str[], char ch){
	int i, j;
	for(i = j = 0; str[i] != '\0'; i++){
		if(str[i] != ch){
			str[j++] = str[i];
		}
	}
	str[j] = '\0';
}

file3.c如下:

#include <stdio.h>

void print_string(char str[]){
	printf("%s\n", str);
}

运行主程序,打印:

Hello World!!!
o
Hell Wrld!!!

显然,o字符被删除。

展开阅读全文
©️2020 CSDN 皮肤主题: 黑客帝国 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值