目录
1.1函数概述
c语言都是由很多程序模块构成的,一个程序模块完整一个特定的功能,通过这些模块的配合,可以实现程序所要达成的目标。而这些所谓的模块,实际上可以理解为函数。
1.1.1函数的概念
在程序开发过程中,程序员需要完成的代码量非常大,所以将代码合理地划分为不同的模块是很有必要的,每一个模块都具有相对独立的功能,并为其他程序提供对外调用的参数和返回值,这样由多个模块组成的程序才会让程序阅读者更方便地理解程序设计的理念,并且更容易调试和维护。而这种一步一步地把一个复杂地问题分解为多个小问题,自顶向下,逐步求精,分别解决,直到完成最终的程序的设计方法称为模块化程序设计。
函数是c语言模块种化程序设计的最小单位,既可以把每个函数都看作一个模块,也可以将若干相关的函数合并成一个模块。可以通过函数将功能封装起来,使得一个功能可以在不同的情况下被其他功能调用,函数的概念就是这样产生的。如果把程序设计比作制造机器,那么函数就好比是它的零部件,可以先将这些“零部件”单独设计、调试、测试好,用的时候拿出来装配,并进行总体调试。这些“零部件”可以是自己设计的,也可以是别人设计好的,也可以是现成的标准产品。
1.1.2函数的分类
1)按照函数定义的角度进行分类,可以将函数分为库函数(标准函数)和用户自定义函数两种。
2)c语言的函数兼有其他语言中的函数和过程两种功能,按照这个角度来看,又可把函数划分为有返回值函数和无返回值函数两种。
3)按照主调函数和被调函数之间有无数据传送的角度来看,函数又可分为无参函数和有参函数两种。
1.2函数的定义、调用和声明
1.2.1函数的定义
1.函数定义的一般形式
在c语言中,对一个函数进行定义,目的是使编译器知道该函数所完成的功能,一个函数在定义的时候需要包含函数头与函数体两部分。
函数类型 函数名(形式参数列表) //函数头
{
声明部分 //函数体
执行部分
}
1)函数头由函数类型、函数名、形式参数列表3部分构成。
a.函数类型:指的是函数返回值的数据类型。
对于有返回值的函数必须在函数定义中明确指定返回值的数据类型。例如:
double add(double x,double y) //函数返回值为double型
int min(int x,int y) //函数返回值为整形
对于无返回值函数的函数类型用void表示。
b.函数名:函数的标识符,在命名时需要符合标识符的命名规则。
c.形式参数列表:列表中的变量称为形参。
对于有参函数:在函数被调用时主调函数应按照形参的数据要求向被调函数传递数据。
对于无参函数:形式参数列表为空,函数名后的“()”一定不能缺省。在函数调用时,主调函数无数据传送给被调函数。
2)函数体是函数的主要部分,是一个语句块,是用一对花括号括起来的语句序列。一般包括声明部分和执行部分,它描述了函数实现一个功能的过程。声明部分包括对函数内使用的变量进行定义行声明或引用行声明,以及对被调函数的原型声明。执行部分是实现函数功能的语句序列。
还有一种特殊的函数称为“空函数”,它的形式为:
函数类型 函数名()
{ }
空函数在函数体中没有任何处理语句或操作,也就是说,该函数没有任何实际作用。空函数的存在只是为了“占位”,就是说该函数所在的位置是需要一个函数完成相应的功能,但是由于程序员没有编写好代码,所以暂时放置一个空函数来“占位”,等待后续的处理。
同时,c语言规定不能在一个函数的内部再定义函数,即不允许函数嵌套定义。
2.return语句
函数不仅能够完成用户所指定的特殊功能,有时还能根据具体需要使用返回语句向主调函数返回一个确定值,这个值就是函数的返回值。
当程序执行到返回语句时能够马上从所在的函数中退出,返回到主调函数中,并且根据需要可以通过返回语句返回一个确定的值。
1)函数的返回值
a.在一个带有返回值的函数中,通常使用return语句将被调函数中的某个确定的值返回到主调函数中。
return语句的一般形式是:
return(表达式);
或
return 表达式;
a.有返回值的函数只能返回一个值,即使一个函数体中有多个return语句,也只能通过一个return语句执行返回操作。
使用return语句时,返回的可以是一个具体的变量值,同样也可以是一个表达式。
有返回值的函数头部中的“函数类型”就是函数返回值的类型,可以是基本类型和指针类型,若缺省函数类型,则系统默认为函数类型为int类型。
如果返回值变量与函数类型不一致,则以函数类型为准自动进行类型转换,即函数类型决定返回值的类型。
返回值为指针类型
若函数的返回值是指针类型,那么这种函数就是指针函数,其定义的一般形式为:
函数类型 *函数名(形式参数表列)
例如,编写函数,实现字符串的连接,并返回连接后字符串的起始地址,主函数中调用该函数连接字符串:
#include <stdio.h>
char *str_concat(char *s1,char *s2)
{
char *p=s1,*q=s2;
while(*P!='\0')
p++;
while((*p=*q)!='\0')
{
p++;
q++;
}
return s1;
}
int main()
{
char str1[81],str2[81];
printf("Input two string:\n");
gets(str1);
gets(str2);
printf("str1:%s\n",str_concat(str1,str2));
return 0;
}
分析上面的程序,在主函数中,printf("str1"%s\n",str_concat(str1,str2));这条语句是要以“%s”的格式输出,而根据前面的介绍,此时输出项列表中应该是地址或者是能表示地址的内容。在这条语句中,输出项列表是一个函数,这时就要求这个函数的返回值是一个地址,而能返回地址的函数就得是指针函数。因此,在这个程序中就定义了一个指针函数str_concat(),其return语句中的变量s1就是个指针变量,而指针变量s1中存放的就是数组str1的地址
b.无返回值的函数
没有返回值的函数,用void定义其函数类型,否则,函数即使不用return语句返回值,函数仍将返回一个不确定的值,这时执行到函数体中的最后一条语句后,自动退出被调函数。
如果return语句中没有返回值,语句格式为:
return;
2)函数的结束标志
若c语言的一个函数中没有return语句,则将该函数体中的语句一条一条执行完毕后哦到达“}”,该函数执行结束。而若是函数中使用了返回语句return,那么当执行到函数体中的语句时,只要执行到return语句就会返回到主调函数中,该函数执行结束,后续的语句将不会再执行。
如果一个函数体中有多个return语句,函数执行到其中任何一个return语句,就结束对该函数的调用。
1.2.2函数调用
1.函数调用的一般形式
用户自定义一旦被定义后,使用的方法与库函数一样,是通过调用表达式实现的。调用表达式的形式为:
函数名(实际参数列表)
调用表达式可以作为其他表达式的一部分参与运算。
说明:
1)调用函数时提供的参数为实参。实参是有确定值的表达式(包括常量、变量、数组元素值、函数值,以及由这些值构成的表达式)。如果实参列表包含多个实参,则各实参间用逗号隔开。
2)函数的参数列表是主调函数和被调函数之间数据传递的一种约定。定义函数时,形式参数列表约定了参数的类型、个数和顺序;调用函数时,应按约定的类型、个数和顺序提供数据。如果数据类型不一致,系统将按照自动转换规则进行转换。若被调函数是无参函数,则实参表列为空,但括弧不能省略,表示被调函数不从主调函数接收数据。
2.函数的调用过程
1)为主调函数中定义的变量分配内存单元。
2)执行调用语句。将实参表达式的值一次赋值给对应的形式参数,并为形式参数分配内存单元;若是无参函数,则无参数传递过程。
3)执行被调函数的函数体。先为函数体中定义的变量分配内存单元,再执行函数体中的语句。
4)执行到return语句时,计算返回值,释放为函数体中变量和形参变量分配的内存空间,返回到主调函数。
5)主调函数继续执行后续语句。
函数调用是一个单向传递的过程,只能将主调函数实参的值传递给被调函数的形参,返回值由被调函数传递给主调函数。
3.函数调用的方式
按被调函数在主调函数出现的位置来分,可以有以下3种函数调用方式。
1)作为函数语句调用
这种情况下,函数一般不带返回值,或者返回的值用户不关心,只要求函数完成一定的操作。例如,“scanf("%d",&b);”,又如“fun();”。
2)作为函数表达式调用
这种方式要求函数带回一个确定的值以参加表达式的运算,例如“z=2*min(x,y);”。
3)作为函数参数调用
这时要求函数有一个确定的返回值,并把函数调用得到的值作为其他函数的实参,如“y=min(1,min(2,3))"。
1.2.3函数的声明
在前面的程序中,函数的定义都是位于主调函数之前。但是,c语言并没有要求函数的定义必须在主调函数之前。
例如:
#include <stdio.h>
int main()
{
float a,b,c;
scanf("%f,%f",&a,&b);
c=min(a,b);
printf("min is %f\n",c);
return 0;
}
float min(float x,float y)
{
if(x<y)
return(x);
else
return(y);
}
程序运行结果是错误的。调试过程中发现,虽然在编译过程中没有报错,但是有警告。通过查看警告信息得知。编译器不知道min函数有几个形式参数,形参分别是什么数据类型,也不知道min函数返回的值是什么类型。但是,编译器不会给出出错信息,而是默认min函数的返回值类型为int类型。当编译器在后面遇到min函数的定义时,会发现返回值类型是float类型,而不是默认的int类型,所以结果出错。
为了避免在定义之前就进行调用的问题,一种方法就是使每个函数的定义都位于其调用之前。但是有时候无法进行这样的安排。
c语言提供了另外一种更好的解决方法,在调用前进行声明。函数声明是对已存在的被调函数进行声明,将其返回值的类型、参数个数及参数的类型、顺序通知编译系统,让编译系统可以在编译阶段对调用进行合法性检查,判断形参与实参的类型及个数是否一致,从而保证程序能正确运行。
1.函数原型
为了能正确地进行函数的调用,系统需要知道下列信息。
1)函数类型
2)函数名
3)函数的参数(个数、类型及顺序)
这些信息可以组成函数的模型,也称函数原型,它描述了函数的用户界面。
C语言要求在某函数被调用之前,应当让编译器知道该函数原型,以便让编译器利用函数原型提供的信息去检查调用的合法性,保证参数的正确传递。那么,编译器从何处得到函数原型呢?
1)当函数定义在前、调用在后,编译器在调用函数前,能从其定义中抽取函数原型。
2)当函数调用在前、定义在后,要求程序员在调用之前用函数原型对函数进行声明,以便编译器从中得到函数原型所提供的有关信息。
2.函数声明的形式
函数定义只能一次,函数声明可以多次(不同的函数调用同一函数时,可以分别声明)
一般采用函数原型进行声明:
函数类型 函数名(形参类型1,形参类型2,...);
或
函数类型 函数名(形式参数列表);
这种形式就是把定义函数时的函数头搬过来加一个分号即可。
例如:
char letter(char c1,char c2);
或
char letter(char,char);
函数声明中的形参名对编译器没有意义。但是,有时为了强调各参数的”角色“,通常还是给出参数名。
3.函数声明的条件
1)被调用的函数必须是已经存在的函数(或是库函数,或是用户自定义的函数)。
2)对于库函数,只要在主调函数所在文件开头用#include命令包含相应的头文件即可。
3)对于用户自定义的函数,调用在前、定义在后,要求在调用前进行函数声明。
4.函数声明的位置
函数声明语句的位置应该在函数定义之前。
1)库函数的声明系统都集中存储在一些扩展名为.h的文件中。所以,使用库函数时,只需将这些文件用预处理命令#include包含即可,用户不必再做声明。
2)文件的开头处。将函数声明集中放在整个程序的开头,所有函数定义之前,即在所有函数的外部,这样就不必在之后的每个主调函数中对被调函数进行声明了。
3)主调函数内。即将函数声明放在主调函数内的声明部分,如在main函数内进行声明,则只有在main函数内才能识别该函数。
1.3函数中的参数
1.3.1实际参数与形式参数
实际参数,简称实参,可以是常量、变量、表达式、函数等。
无论实参是何种形式,在进行函数调用时,它们都必须具有确定的值,以便把这些值传递给形参。
形式参数,简称形参,是在定义函数和在函数体中使用的参数,目的是用来接收调用该函数时实参传递过来的值,形参只能是变量或者是数组名。
1.3.2参数的传递方式
1.值传递
所谓值传递就是函数调用时,系统先计算表达式的值,然后将值传递给形参变量,参数的类型是基本类型。此时,在被调函数中不能修改或引用主调函数中的变量。如果主调函数只是想把一些值传递给被调函数,对应的参数通常使用值传递方式。
例如:
#include <stdio.h>
void swap(int x,int y)
{
int z=0;
printf("swap1:x=%d,y=%d\n",x,y);
z=x;x=y;y=z;
printf("swap2:x=%d,y=%d\n",x,y);
}
int main()
{
int a,b;
printf("\nplease enter date(a,b):");
scanf("%d%d",&a,&b);
printf("main1:a=%d,b=%d\n",a,b);
swap(a,b);
printf("main2:a=%d,b=%d\n",a,b);
return 0;
}
程序的运行结构为:
please enter date(a,b):10 20
main1:a=10,b=20
swap1:x=10,y=20
swap2:x=20,y=10
main2:a=10,b=20
这个程序演示了将实参的值传递给形参的过程,但是,被调函数没能把主函数中a和b的值交换,也就是说实参在形参交换值后并没有发生任何改变。这就说明,在值传递方式中,形参的改变不会影响到实参,实参变量与形参变量是两个不同的变量。
2.地址传递方式
地址传递也就是将实参表达式的地址值赋值给形参变量,但它是一种特殊的值传递方式,它传递的不是普通的数值,而是地址,参数的类型是指针类型。这样的话,赋值后形参变量就指向了实参表达式所指的变量,也就是说形参和实参指向了同一个地址的变量。在被调函数中,通过该形参指针变量可以修改或引用主调函数中的变量,这种方式使函数间的数据传递更加灵活。
如果希望将主调函数中变量的地址传递给被调函数,那么定义被调函数时,其形参变量应定义为相应的指针类型。
在函数调用时,首先由实参向形参赋值,这时形参和实参的值是相同的,即实参和形参指向了相同的地址单元,所以在函数调用过程中,改变形参所指向的单元的内容,也就相当于改变了实参所指向单元的内容,这正是地址传递方式的特点。
1.4函数的嵌套调用和递归调用
1.4.1函数的嵌套调用
所谓嵌套调用,就是在调用一个函数的过程中,被调用的函数又调用其他函数。例如:
#include <stdio.h>
f1(int a,int b)
{
int c;
a+=a;b+=b;
c=f2(a,b);
return (c*c);
}
f2(int a,int b)
{
int c;
c=a*b%3;
return (c);
}
int main()
{
int x=11,y=19;
printf("The final result is:%d\n",f1(x,y);
return 0;
}
1.4.2函数的递归调用
在C语言中,若是一个函数在调用的时候,直接或简介地调用了自身,那么就称为函数的递归调用,该函数就是递归函数。
C语言允许函数递归调用,执行递归时函数将反复调用其自身,每调用一次就进入新的一层。递归调用和调用其他函数一样需要重新开辟空间,并不共用同一内存空间,因此调用自身和调用其他函数的调用过程没有区别。
递归函数又分为直接递归和间接递归
1.直接递归调用
void fun()
{
printf("*");
fun();
}
如果在主函数中直接调用该函数,程序将不断打印星号*,无止休地调用其自身。
2.间接递归调用
void fun2();
void fun1()
{
printf("function fun1 begin. \n");
fun2();
printf("function fun1 end. \n");
}
void fun2()
{
printf("function fun2 begin. \n");
fun1();
printf("function fun2 end. \n");
}
可以看到,这两种递归调用都是无终止的自身调用。显然,程序中不应出现这种现象,这可以用if语句来控制,只有在某一条件达成时才继续执行递归调用,否则就不再继续。
采用递归方法来解决问题时,必须符合以下3个条件。
1)可以把要解决的问题转化为一个解决方法相同的新问题,而新问题的规模要比原始问题小。
2)可以应用这个转化过程使问题得到解决。
3)必定要有一个明确的递归约束条件,称为递归出口。
例如,用递归方法输出n个星号“*”:
#include <stdio.h>
void f(int n)
{
if(n!=0)
{
printf("*");
f(n-1);
}
else
return;
}
int main()
{
f(10);
return 0;
}
1.5.3函数程序应用设计举例
在C语言中,数组元素作为实参的使用方法和同类型的普通变量一样。若将数组元素的地址作为实参,则可以在被调函数中通过指针修改数组元素的值。那么数组名作为函数的参数时,情况如何呢?
数组名实际上表示的是整个数组的首地址。如果实参是数组类型,形参也应该是数组类型,主调函数函数与被调函数存取的将是相同的一组空间,原因是实参传给形参的是一组空间的首地址,并不是将所有数组元素的值都传递给形参数组。系统只需为形参开辟一个指针型的存储空间,用来存放实参传递过来的数组起始地址。因而,指向一段连续空间的首地址的指针变量也可以作为函数参数,其意义与数组名作为参数的意义相同。
数组名作函数的参数时,要求形参和实参必须均为数组类型 或与其同类型的指针。形参与实参的对应关系可以有4种组合。
1.形参和实参都用数组名
例如,设计一个程序,将数组的第一个元素放到最后一个位置,将第二个元素放到倒数第二个位置......直至最后一个元素放到第一个位置。
#include <stdio.h>
void invert(int x[],int n)
{
int i,j,t;
for(i=0,j=n/2;i<=j;i++)
{
t=x[i];
x[i]=x[n-i-1];
x[n-i-1]=t;
}
}
int main()
{
int a[10],i;
printf("please enter the array:");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
invert(a,10);
printf("now the array is:");
for("i=0;i<10;i++)
printf("%3d",a[i]);
return 0;
}
2.形参用数组名,实参用指针
#include <stdio.h>
void invert(int x[],int n)
{
int i,j,t;
for(i=0,j=n/2;i<=j;i++)
{
t=x[i];
x[i]=x[n-i-1];
x[n-i-1]=t;
}
}
int main()
{
int a[10],i,*p=a;
printf("please enter the array:");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
invert(p,10);
printf("now the array is:");
for("i=0;i<10;i++)
printf("%3d",a[i]);
return 0;
}
3.形参用指针,实参用数组名
#include <stdio.h>
void invert(int *x,int n)
{
int *i,*j,t;
for(i=x,j=x+n-1;i<=j;i++,j--)
{
t=*i;
*i=*j;
*j=t;
}
}
int main()
{
int a[10],i;
printf("please enter the array:");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
invert(a,10);
printf("now the array is:");
for("i=0;i<10;i++)
printf("%3d",a[i]);
return 0;
}
4.形参和实参都用指针
#include <stdio.h>
void invert(int *x,int n)
{
int *i,*j,t;
for(i=x,j=x+n-1;i<=j;i++,j--)
{
t=*i;
*i=*j;
*j=t;
}
}
int main()
{
int a[10],i,*p=a;
printf("please enter the array:");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
invert(p,10);
printf("now the array is:");
for("i=0;i<10;i++)
printf("%3d",a[i]);
return 0;
}
1.5内部函数和外部函数
1.5.1内部函数
如果在一个源文件中所定义的函数,只能通过本文件中的函数进行调用,而不能被同一程序其他文件中的函数来调用,则诸如此类的函数称为内部函数。
内部函数又称为静态函数,在定义内部函数的时候,需要在函数类型前加上“static”关键字进行声明。
内部函数的定义语法如下:
static 函数类型 函数名(参数列表)
例如:
static char fun(char c)
{
if(c>='A'&&c<='Z')
c=c+32;
return (c);
}
由于该函数使用关键字“static”进行声明,所以为内部函数。
使用内部函数的好处就是不同的人在编写不同的函数时,不用去担心自己所定义的函数会与其他文件中的函数有同名的现象。即使在不同的文件中有同名的内部函数,也不会互相干扰。
1.5.2外部函数
如果在一个源文件中所定义的某个函数,能够被同一个程序的其他文件中的函数进行调用,那么这样的函数就被称为外部函数。
在 定义一个函数时,若该函数的函数类型前没有关键字“static”进行声明,或是加了“extern”关键字进行声明,说明此函数是外部函数。
外部函数的定义语法如下:
[extern] 函数类型 函数名(参数列表)
其中,关键字“extern”可以省略不写,默认为外部函数。
1.6变量的作用域
变量除了有数据类型的属性外,还有存储属性。存储类型包括作用域与生存期两个方面。
在C语言中,一个有参函数的形参变量在该函数被调用时才去分配内存,在该函数调用结束后就会立即将内存释放掉。这说明了形参变量的作用域是有限的,只能够在函数的内部使用,当离开函数之后就变成无效的了。
这里说的“作用域”,就是指得变量的有效范围,不仅仅形参变量拥有有效范围,其他任何变量在使用时,都是有自己的作用范围的。而按照变量所定义的位置不同,可以将它们分为局部变量和全局变量。
1.6.1局部变量
在函数内部定义的变量称为局部变量或内部变量。局部变量只在本函数范围内有效,也就是说局部变量的作用域是变量定义所在的函数内。例如:
int f1(int a)
{
int b,c; //在此函数内a、b、c有效,它们不是主函数中的a、b、c
...
}
int main()
{
int a,b; //在主函数内变量a、b一直有效
{
int c; //变量c只在复合语句内有效
c=a+b;
...
}
...
return 0;
}
说明:
1)在主函数main中定义的变量a、b也只在主函数中有效,而不因为在主函数中定义就在整个文件或程序中有效。
2)在不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干扰。它们在内存中占不同的单元,互不混淆。如上面f1函数中的a、b、c和main函数中的a、b、c都是局部变量,代表不同的对象。
3)形式参数也是局部变量,如上面f1函数中的形参a,也只在f1函数中有效,其他函数中不能使用。
4)可以在复合语句中定义变量,这些变量只在本复合语句中有效,离开本复合语句该变量所占内存单元将释放。
1.6.2全局变量
在函数外部定义的变量称为全局变量或外部变量,全局变量可以为本文件中其他函数所共用,其作用域是从定义或声明的位置开始,直至它所在源程序文件的结束。
说明:
1)全局变量的使用增加了函数之间传递数据的途径。由于同一文件中的所有函数都能引用全局变量的指,因此,如果在一个函数中改变了全局变量的值,就能影响到其他函数,相当于各个函数间有直接的传递通道。由于函数的调用只能带回一个返回值,因此如果一个函数需要返回两个或两个以上的计算结果时,可以使用全局变量传递数据。
2)如果外部变量在文件开头定义,则在整个文件范围内都可以使用该外部变量。如果不在文件开头定义,则其作用范围只限于定义点到文件终了。若定义点之前的函数想引用该外部变量,则应在函数中用关键字extern作“外部变量声明”即可,表示这些变量是在该函数后面定义的外部变量。
例如,全局变量的引用:
#include <stdio.h>
void fa(int a)
{
extern int j; //声明此函数引用全局变量j
int i=1;
a=a+a+i+j;
printf("fa:a=%d,i=%d,j=%d\n",a,i,j);
}
int j=200; //定义全局变量j
void fb(int a)
{
int i=100;
a=a+i+j;
printf("fb:1=%d,i=%d,j=%d\n",a,i,j);
}
int main()
{
int i=100;
fa(i);
fb(j);
printf("main:i=%d,j=%d\n",i,j);
return 0;
}
3)在同一个源文件中,若局部变量和全局变量同名,则在局部变量的作用域内,同名的全局变量不起作用(即全局变量被屏蔽)。
作用域不同的同名变量有时会出现作用域的部分重叠,这时系统通常都选择作用域范围小的变量有效。
4)何时需要定义全局变量?如果变量的类型固定,并只有很有限的几个地方需要修改它的值,而且这个变量的值经常被程序中的多个函数使用,大多地方只是读取它的值,而不修改它的值,那么这时就比较适合将这个变量定义为全局变量。
5)建议非必要时不要使用全局变量。
a.占用存储空间。
b.降低了程序的可靠性和通用性。
c.降低了程序的清晰性。
1.7变量的存储类型
变量从作用域(即空间)的角度来看,可以分为局部变量和全局变量。换个角度还可以从生存期(即存在时间)将变量分为动态存储变量和静态存储变量。
变量的生存期指变量在程序执行过程中什么时间存在。一般来说,变量的生存期也分为永久和暂时两大类。永久变量是存在于程序运行的全过程的变量,即程序一开始就已经生成,一直存在到程序运行结束。暂时的生存期指变量只在程序执行到某个局部时才生成,并在该程序局部结束时被撤销。
变量的生存期与变量的存储类型有关。
1.7.1动态存储和静态存储
1.存储类型
变量的存储类型是指变量使用计算机存储资源(内存和寄存器)的方式。根据变量在程序运行期间是否需要占用固定的存储单元,变量的存储类型可分为动态存储和静态存储两类,具体包含动态变量、静态变量、寄存器变量、外部变量4种。
2.存储区域
就内存资源而言,C程序运行时可供用户使用的内存空间通常分为程序区、静态存储区和动态存储区3部分。就变量而言,分为静态和动态两个区。
3.存储类型与存储区域的关系
动态存储类型:程序运行期间不需要长期占用存储单元。动态存储类型的变量有auto(自动)类型和register(寄存器)类型。动态存储类型的变量可存放在动态存储区和寄存器两个地方。
存储在动态存储区中的变量,在程序运行过程中被动态创建,在程序运行完成所在域即被撤销,相对于程序的生命而言,其生命是动态的。也就是存放在动态存储区中的数据,是在函数调用开始时才动态分配存储空间,函数结束时释放这些存储空间,即所谓的动态存储方式。
静态存储方式:静态存储类型的变量在编译时被分配空间,在整个程序运行期间一直占用固定的内存空间,程序运行结束才释放内存空间。可以用static、extern定义和声明静态存储类型的变量。静态存储类型的变量只能存放于静态存储区中。
存储在静态存储区中的变量,在编译时即被创建,分配了固定的存储空间,在程序运行结束时才被撤销,在程序执行过程中它们占据固定的存储单元,而不是动态地分配和释放,即所谓的静态存储方式。
4.变量的存储属性
显然,变量的作用域和生存期都与变量的存储类型有关。
C语言中的变量和函数都有存储类型和数据类型这两种属性,定义变量时既要说明数据类型。也要说明存储类型。因此,完整的变量定义形式应为:
存储类型 数据类型 变量列表;
例如:
auto int a,b,c; //定义自动整形变量a,b,c
static int x,y,z; //定义静态整形变量x,y,z
register int i,j,k; //定义寄存器变量i,j,k
1.7.2自动变量
自动变量的标准定义格式如下:
auto 数据类型 变量列表;
其中“auto”可以不写。auto存储类型是最常用的一种存储类型。此前,在函数内部或复合语句内部定义的局部变量即函数的形参变量如不做专门的声明,则默认为自动变量,属于动态存储类型,是动态分配存储空间的,对它们分配和释放存储空间的工作是由系统自动进行的,因此这类局部变量称为局部动态变量或自动变量。
自动变量在其定义所在的函数开始执行时才分得内存空间并初始化,在该函数执行期间占用内存空间,在函数执行结束时其所占用的空间被系统收回(此后,这些变量不存在)。
自动变量的作用域是其所在的函数内或复合语句内,离开这个范围,自动变量消失。
自动变量被声明在“内部”,是一种使用最频繁的变量。它有如下两种性质。
1)自动变量的生存期局限于所在的程序块。所在的程序块执行结束时,所占用的存储空间即被释放,从而可以节省存储空间。由于它与所在的程序块共存亡,所以函数的量词调用之间,在函数中声明的变量及参数的值不能保存。
2)自动变量由于生存期是局部的,其作用域一定局限于其声明语句所出现的程序块。从声明语句开始到该程序块结束的程序段内都可以引用该变量。
1.7.3静态变量
在C语言中,局部变量和全局变量都可以定义成具有静态存储类型的变量。
1.静态局部变量
其语法形式如下:
static 数据类型 变量列表;
static存储类型也是比较常用的一种存储类型。在编译时,编译系统就为静态变量分配好内存空间(并初始化 ,初始化只进行一次)。程序运行时,静态变量始终占用内存空间,直到程序运行结束。
有时候希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即其占用的存储单元不是放,在下一次调用该函数时,该变量已有上一次函数调用结束时保留下来的值。谓词,应该指定该局部变量为“静态局部变量”,用static加以声明。
自动变量和静态局部变量的不同
1)静态局部变量属于静态类型存储,在静态存储区内分配存储单元。在程序整个运行期间占用固定的内存单元而不释放。而自动变量属于动态存储类型,占用动态存储区空间,函数调用结束后立即释放。
2)对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有值,以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行的,每调用一次函数重新赋一次初值。
3)如在定义局部变量时不赋初值,则度静态变量来说,编译时自动赋初值0或空字符'\0';而对自动变量来说,定义时不会自动初始化,它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新分配另一个存储单元,因此所分配的单元中的值是不确定的。
4)虽然静态局部变量在函数调用结束后、整个程序运行期间都是存在的,仍然占用存储单元,但由于该变量是局部变量,其作用域仍然是定义该变量的函数体内部。在它的作用域外,仍是不可被引用的。
2.静态全局变量
静态全局变量与静态局部变量的定义形式是一样的,只是静态全局变量定义在函数外面。
如:
static int sum;
int main()
{
...
}
静态全局变量有以下存储特点
1)静态全局变量的存储空间是在静态存储区进行分配的,并且它在编译时分配内存空间,在程序执行过程中静态全局变量始终存在。
2)静态全局变量的作用域仅为文件作用域,即从定义点开始到所在的程序文件结束。因此,静态全局变量不能够被其他文件中的函数进行访问,体现了模块间低耦合性的思想。
1.7.4寄存器变量
为了提高程序的执行效率,减少使用变量时存取内存的时间,C语言允许将局部变量的值放在CPU的寄存器中,使用时,不需要访问内存而直接从寄存器取出,使程序的运行速度快得多。这种变量叫“寄存器变量”,用关键字register进行声明。其定义格式如下:
register 数据类型 变量列表;
寄存器变量具有以下特点
1)寄存器变量说明符只适用于局部变量和形式参数,其他不适合使用,因此寄存器变量不能被同一文件中的其他函数访问。在调用一个函数时,若定义了寄存器变量,则会占用一些寄存器以存放寄存器变量的值,函数调用结束就释放寄存器。通常将频繁使用的变量放在寄存器中,以提高程序的执行速度。
2)寄存器变量只能是整形变量。如果没有足够的寄存器来存放指定的变量,将自动按自动变量来处理。
3)静态局部变量不能定义为寄存器变量。
4)不能对寄存器变量进行求地址运算,因为寄存器变量的值不是存放在内存中的,寄存器是没有地址的。
1.7.5外部变量
外部变量指定义全局变量时没有用关键字“static”,该变量的作用域可以扩展到其他文件中的函数。
用关键字“extern”声明的变量称为外部变量。外部变量有定义性声明与引用行声明两种声明形式。定义性声明是为了建立实体,引用性声明是为了建立标识符与实体之间的联系。
其定义性声明的一般形式为:
[extern] 数据类型 变量列表[=初始化表达式];
通常,只要是定义在函数外部的,就认为是外部变量,extern可以省略。
外部变量具有如下特性。
1)外部变量常驻程序的内存静态存储区,程序运行期间一直占用固定的内存空间,与程序共存亡。即程序开始运行时就被创建,直到程序终止执行。
2)外部变量只能被初始化一次,初始化是在编译时进行的。当定义性声明语句中没有显示地初始化时,系统将其初始化为0.
3)外部变量具有全程作用域。外部变量除了可以被本文件的函数访问之外,还可以被程序的其他源文件访问。该外部变量在被源程序的其他源文件访问之前需要进行引用性声明:
extern 数据类型 变量列表;