7.1为什么要用函数
模块化程序设计:实现编写好一批常用的函数,需要使用时可直接调用,而不必重复在写,减少了程序的冗余,使得程序变得更加精炼,编写一次,就可以多次调用。
函数声明的作用:吧有关函数的信息(函数名、函数类型、函数参数的个数与类型)通知编译系统,以便在编译系统对程序进行编译时,在进行到Main函数中调用其它函数时,知道它们是定义的函数而不是变量或其他对象。
说明:
(1) 一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。对于较大的程序,一般不希望把所有的内容全放在一个文件中,而是将它们分别编写成若干个源文件中,由若干个源程序文件组成一个C程序。
(2) 一个源程序文件由一个胡多个函数以及其他有关内容(如指令、数据声明与定义)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
(3) C程序的执行是从main函数开始的,如果在main函数中调用它其他函数,在调用后流程返回到main函数,在main函数找那个结束整个程序的运行。
(4) 所有函数都是平行的,即在定义函数时是分别进行的,是相互独立的;一个函数并不从属于另一个函数,即函数不能嵌套定义。
(5) 从用户使用的角度看,函数有两种形式
① 库函数
② 用户自己定义的函数
(6) 从函数的角度看,函数有两种形式
① 无参函数
② 有参函数
7.2 怎样定义函数
7.2.1 为什么要定义函数
定义函数包含以下内容:
(1) 指定函数名字,使后续可按名字调用
(2) 指定函数的类型,即函数返回值的类型
(3) 指定函数的参数的名字和类型,以便在调用函数时向它们传递数据。无参参数该项不需要指定
(4) 指定函数应当完成什么操作,即函数的功能
说明:对于C编译系统提供的库函数,是由编译系统事先定义好的,库文件中包括了个函数的定义。我们不必自己定义,只须用#include指令把有关的头文件中包含到本文件模块中即可。形式:#include <stdio.h>
7.2.2定义函数的方法
1.定义无参函数
定义无参函数的一般形式为:
①类型名 函数名()
{
函数体
}
或者
②类型名 函数名(void)
{
函数体
}
函数名后面括号内的void表示“空”,即函数没有参数。
函数体包括声明部分和语句部分
在定义函数时要用“类型标识符”(类型名int float double等)指定函数值的类型,换一个说法就是指定函数返回值的类型
2.定义有参函数
定义有参函数的形式:
类型名 函数名(形式参数列表)
{
函数体
}
int function(int x,int y)
{
Int z;
Z=x>y?x:y;
Return z;
}
有参构造函数的特点:有返回值类型typeof(如int float duble 等),也有返回值语句return.可通过这两个标志识别该函数是有参函数还是无参函数。
3.定义空函数
定义空函数的形式:
类型名 函数名()
{
}
如:
Void dummy()
{}
空函数是函数体是空的函数。即什么都不做,没有任何实际作用。
那为什么还要定义函数空函数呢?若当前不清楚具体实现的功能,或者当前没有实现该功能的能力;将空函数放在特定的位置,日后再来实现也是可以的,也可以在扩充程序功能时用一个编写好的函数替代空函数,这样程序的结构非常清楚,可读性好,便于扩展新功能。
7.3调用函数
7.3.1 函数调用的形式
函数调用的一般形式为:
函数名(实参表列)
1.函数调用语句
把函数调用语句单独作为一个语句
2.函数表达式
函数调用出现在另一个表达式中。如“c=max(a,b)”
3.函数参数
函数调用作为另一个函数调用时的实参,如:
M=max(a,max(b,c));
换一个说话就是函数嵌套调用,函数里调用函数
说明:调用函数并不一定要求包括分号;只有作为函数调用语句才需要有分号。如果作为函数表达式或函数参数,函数调用时不必有分号的。如:
Printf(“%d”,max(a,b));
该max函数后面不能跟“;”分号
Max(a,b);
该max函数作为函数语句调用时,必须后面跟着“;”
7.3.2函数调用时的数据传递
1.形式参数和实际参数
在调用有参函数时,主调函数和被调用函数之间有数据的传递关系。
在定义函数时函数名后面括号中的变量名称为“形式参数”(简称“形参”),在主调函数中调用一个函数时,函数名后面括号中传入的函数称为“实际参数”(简称“实参”)。实际参数可以是常量、变量或表达式
2.实参和形参间的数据传递
在调用函数过程中,系统会把实参的值传递给被调用函数的形参。
7.3.3函数调用的过程
(1)在定义函数中指定的形参,在未出现函数调用时,并不占用内存中的存储单元。只有在发生函数调用时,函数max的形参被临时分配内存单元。
(2)将实参对应的值传递给形参。
(3)在执行函数期间,由于形参已经有值,就可以利用形参进行有关的运算
(4)通过return语句将在调用函数内得到的结果带回到主函数。
如果函数不需要返回值,则不需要return语句。这时函数的类型应定义为void类型
(5)调用结束,形参单元被释放。注意:实参单元仍保留并维持原值,没有改变。即调用函数的过程中,形参会发生改变,而主调函数的实参不会发生改变。
注意:实参向形参的数据传递是“值传递”,单向传递,只能由实参传给形参,而不能由形参传给实参。换一个说法就是,形参的改变并不能影响到实参,而实参的改变会影响到形参。
7.3.4 函数的返回值
(1)函数的返回值是通过函数中的return语句获得的。
一个函数中可以有多个return,但是只能执行到一个return
(2)函数值的类型。函数值有返回值,那么这个返回值应当属于一种确定的类型,应当在定义函数时指定函数值的类型。
注意:在定义函数时要指定函数的类型
(3)在定义函数时指定指定的函数类型一般应该和return语句中的表达式类型一致。说明:如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换,即函数类型决定返回值的类型
7.4对被调用函数的声明和函数原型
在一个函数中调用另一个函数(即被调用函数)需要具备如下条件:
(1)首先被调用的函数必须是已经定义的函数(库函数或者用户自己定义的函数)。
(2)如果使用库函数,应该在本文件开头中用#include指令将调用有关库函数时所需用到的信息“包含”到本文件中来
#include <stdio.h> 中.h是头文件所用的后缀,表示是头文件(header file)
(3)如果使用用户自己定义的函数,而该函数的位置在调用它的函数(即主调函数)的后面(在同一个文件中),应该对被调用的函数做声明(declaration)。声明的作用是把函数名、函数参数的个数和参数类型等信息通过编译系统,以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法。
说明:在运行阶段发现错误并重新调试程序,是比较麻烦的,工作量也较大。故应在编译阶段尽可能多地发现错误,随之纠正错误。
函数声明的一般形式:
(1)函数类型 函数名(参数类型1 参数名1,参数类型2 参数名2,……);
(2)函数类型 函数名(参数类型1,参数类型2 ,……);
注意:函数声明是方面带有分号“;”,函数定义是后面无分号。如定义的格式为:
(1)函数类型 函数名(参数类型1 参数名1,参数类型2 参数名2,……)
{
}
(2)函数类型 函数名(参数类型1,参数类型2 ,……)
{
}
7.5函数的嵌套调用(迭代、递归)
在定义函数时,一个函数能不能在定义另一个函数。即函数不能嵌套定义。但是可以嵌套调用函数,也就是说在调用一个函数的过程中,又调用另一个函数。
7.6 函数的递归调用
在调用一个函数过程中又出现直接或间接地调用该函数本身,称为函数的递归调用
7.7数组作为函数参数
7.7.1 数组元素作函数实参(值传递)
数据元素可以用作函数实参,不能用作形参。在用数组元素作函数实参时,把实参的值传给形参,是“值传递”方式。数据传递的方向是从实参传到形参,单向传递。
7.7.2 数组名作函数参数(址传递)
用数组元素作实参时,向形参变量传递的是数组元素的值,而用数组名作函数实参时,向形参(数组名或指针变量)传递但是数组首元素的地址。
7.7.3多维数组作函数参数(低维决定数组结构,故低维不能省略)
可用多维数组作为函数的实参和形参,在被调用函数中对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小(对于二维数组来说);如:
int array[3][10];
或
int array[][10];
但是下面这个定义时不合法的
int array[][];
7.8局部变量和全局变量
我们首先提出一个问题:在一个函数中定义的变量,在其他函数中能否被引用?在不同位置定义的变量,在什么范围内有效?
以上的问题就是该节讨论的变量的作用域的问题。每一个变量都有一个作用域问题,即它们在什么范围内有效。
7.8.1 局部变量
定义变量可能有3种情况:
(1)在函数的开头定义
(2)在函数内的符合语句内定义
(3)在函数的外部定义
在一个函数内部定义的变量只在本函数范围内有效,也就是说只有在本函数内才能引用它们,在此函数以外是不能使用这些变量的。在复合语句内定义的变量只在本复合语句范围内有效,只有在本复合语句内才能引用它们。在该复合语句以外是不能使用这些变量的,以上这些称为“局部变量”
说明:
(1)主函数中定义的变量(如m,n)也只能在主函数中有效,并不因为在主函数中定义而在整个文件或程序中有效。主函数不能使用其他函数中定义的变量,如f1中的b,c;f2中的m,n都不能使用
(2)不同函数中可以使用同名的变量,它们代表不同的对象,互不干扰。如f1中定义的b,c也可在f2中定义
(3)形式参数也是局部变量
(4)在一个函数内部,可以在复合语句中定义变量,这些变量只在复合语句中有效,这些复合语句也称为“分程序”或“程序块”
7.8.2 全局变量
程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。
在函数内定义的变量是局部变量,而在函数之外定义的变量称为外部变量,外部变量是全局变量。全局变量可以为本文件中其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。
注意:在函数内定义的变量是局部变量,在函数外定义的变量是全局变量。
P,q,c1,c2都是全局变量,它们的作用范围不同,在main函数和f2函数中可以使用全局变量p,q,c1,c2,但在函数f1中只能使用p,q而不能使用c1,c2.
说明:设置全局变量的作用是增加了函数间数据联系的渠道。由于同一个文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,就能影响到其他函数中全局变量的值。相当于各个函数间有直接的传递通道。由于函数的调用只能待会一个函数返回值,一次有时可以利用全局变量来对增加函数建的联系渠道,通过函数调用能得到一个以上的值。
为了便于区别全局变量和局部变量,在C程序设计中有一个习惯(并非规定)。将全局变量名的第1个字母用大写表示。
说明:建议不再必要时不要使用全局变量,理由如下:
(1)全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。故消耗内存单元
(2)它使函数的通用性降低了,如果在函数中引用了全局变量,那么执行情况会受到有关的外部变量影响,如果有一个函数移到另一个文件中,还要考虑把有关的外部变量及其值一起转移过去。但若该外部变量与其他文件的变量同名时,就会出现问题。这就降低了程序的可靠性和通用性。在程序设计中,在划分模块时要求模块的“内聚性”强,与其他模块的“耦合性”弱。即模块的功能要单一,不相互影响或者影响较小。
(3)使用全局变量过多,会降低程序的清晰性,难以判断瞬间各个外部变量的值。由于在各个函数执行过程中都可能改变外部变量的值,故程序容易出错。
注意:如果在同一个源文件中,全局变量与局部变量同名,这时会出现什么情况呢?
答案是,在局部变量的作用范围内,局部变量有效,全局变量被“屏蔽”,即全局变量不起作用。简单说,就是局部变量在该范围内覆盖了全局变量。
7.9 变量的存储方式和生存期
7.9.1 动态存储方式与静态存储方式
从变量的作用域(即从空间)的角度来观察,变量可以分为全局变量和局部变量
从变量的生存周期(即变量存在的时间、生存期)的角度来观察,变量可分为静态存储方式和动态存储方式。
静态存储方式:指在程序运行期间由系统分配固定的存储空间的方式
动态存储方式:在程序运行期间格局需要进行动态的分配存储空间的方式
存储空间分为3部分
(1)程序区
(2)静态存储区
(3)动态存储区
数据分别存储在静态存储区和动态存储区中。全局变量全部存放在静态存储区中,即在程序执行过程中占据固定的存储单元,而不是动态地进行分配和释放。
动态存储区中存放以下数据:
(1)函数形式参数。在调用函数时给形参分配存储空间。
(2)函数中定义的没有用关键字static声明,即自动变量(auto)
(3)函数调用时的现场保护和返回地址等
以上数据,在函数调用开始时分配动态存储空间,函数结束时释放这些空间。在一个程序中两次调用同一函数,而在此函数中定义了局部变量,在两次调用时分配给这些局部变量的存储空间的地址可能是不相同的。
在C语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类别。数据类型是int double short char double float等类型。存储类别指的是数据在内存中存储的方式(静态存储和动态存储)。
在定义和声明变量和函数时,一般应同时指定数据类型和存储类别,也可以采用默认方式指定(即如果用户不指定,系统会隐含地指定为某一种存储类别)auto
C的存储类别有4种:自动的(auto)、静态的(static)、寄存器的(register)、外部的(extern)。我们可以根据存储类别,知道变量的作用域和生存周期。
7.9.2 局部变量的存储类别
1.自动变量(auto变量)
函数中的局部变量,如果不专门声明为static(静态)存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。
函数中的形参和在函数中定义的局部变量,都属于自动变量。
在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字auto做存储类别的声明;如:
int f(int a)
{
auto int b,c; //定义b,c为自动变量
....
}
其中,a是形参,b、c是自动变量。执行完f函数后,自动释放a,b,c所占的存储单元。
实际上,关键字”auto”可以省略,不写auto则隐含指定为“自动存储类别”,它属于动态存储方式。程序中大多变量属于自动变量。
在函数定义的变量都没有声明为auto,起始都隐含指定为自动变量。
Int a,b;
和 auto int a,b;
等价。
2.静态局部变量(static局部变量)
有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即占用的存储单元不释放,在下一次在调用该函数时,改变了的值为上一次的值。这时需要将该局部变量指定为“静态局部变量”,用关键字static进行声明。
范例:考察静态局部变量的值
1 #include <stdio.h> 2 int main() 3 { int f(int); //函数声明 4 int a=2,i; //自动局部变量 5 for(i=0;i<3;i++) 6 printf("%d ",f(a)); //输出f(a)的值 7 return 0; 8 } 9 10 int f(int a) 11 { auto int b=0; //自动局部变量 12 static int c=3; //静态局部变量 13 b=b+1; 14 c=c+1; 15 return(a+b+c); 16 }
说明:
(1)静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在程序整个执行期间都不会释放。而自动变量(即动态局部变量)属于动态存储类别,分配在动态存储区空间而不再静态存储区空间,函数调用结束后即释放
(2)对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是再编译时进行的,而是在函数调用时进行的,每调用一次函数重新赋一次初值。相当于执行一次赋值语句。
(3)如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(当然这是对于数值型变量而言)或空字符’ ’(对于字符变量而言)。而对自动变量来说,它的值是一个不确定的值,这是由于每次函数调用结束后存储单元已释放,下次调用时有重新另分配存储单元,而所分配的单元中的内容是不可预知的。
(4)虽然静态局部变量在函数调用结束后仍然存在,但其他函数时不能引用它的。因为它是局部变量,只能被本函数引用,而不能被其他函数引用。
3.寄存器变量(register变量)
一般情况下,变量(包括静态、动态存储方式)的值是存放在内存中的,当程序用到那个变量时,将该变量从内存中送到运算器中运算。
如果有一些变量使用频繁,则为存取变量的值要花费不少时间,为提高执行效率,允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种变量叫做寄存器变量。
Register int f; //定义f为寄存器变量
以上3中局部变量存储的位置是不同的,自动变量存储在动态存储区中;静态局部变量存储在静态存储区中;寄存器存储杂技CPU中的寄存器中。
7.9.3 全局变量的存储类别
一般来说,外部变量是在函数外部定义的全局变量,它的作用域是从变量定义的位置开始,到本程序文件的末尾。在此作用域内,全局变量可以为程序中各个函数所引用。有时希望能够扩展外部变量的作用域,有以下几种情况可扩展作用域:
1.在一个文件内扩展外部变量的作用域
如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义该变量前的函数不能引用该外部变量。如果想要在定义该变量前引用该变量,就需要用关键字extern对该变量做“外部变量声明”,表示把该外部变量的作用域扩展到此位置。
注意:提倡将外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个extern声明。
用extern声明外部变量时,类型名可以写也可以不写。如:
“extern int A,B,C;”也可以写成“extern A,B,C;”。因为它不是定义变量,可以不指定类型,只须写出外部变量名即可。
2.将外部变量的作用域扩展到其他文件
如果程序由多个源程序文件组成,那么在一个文件中想引用另一个文件中已定义的外部变量(假设为var),有什么办法?
解决方法:在任一个文件中定义外部变量,而在另一个文件中用extern对该变量(var)做“外部变量声明”,即extern Num;
在编译和连接时,系统会由此知道该变量(var)有“外部链接”,可从别处找到已定义的外部变量(var),并将在另一个文件中定义的外部变量(var)的作用域扩展到本文件,在本文件中可以合法地引用外部变量(var).
3.将外部变量的作用域限制在本文件中
有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个static声明。
如:
在file1.c中定义了一个全局变量Var,但用了static声明,因此只能用于本文件(file1.c),故在file2.c中使用会出错
这种加上static的声明,只能用于本文件的外部变量称为静态外部变量
说明:不要误认为对外部变量加static声明后才采取静态存储方式(存放在静态存储区中),而不加static的是采用动态存储方式(存放在动态存储区中)。声明局部变量的存储类型和声明全局变量的存储类型的含义是不同的。
对于局部变量来说,声明存储类型的作用是指定变量存储的区域(静态存储区或动态存储区)以及由此产生的生存周期的问题。
对于全局变量来说,声明存储类型的作用是指定变量作用域扩展的问题,而不是在编译时分配内存静态、动态存储区的问题,因为全局变量都存放在静态存储区。
用static声明一个变量的作用是:
(1)对局部变量用static声明,是将该变量分配在静态存储区中,该变量在整个程序执行期间不释放,其所分配的空间在程序结束前都存在。
(2)对全局变量用static声明,是将该变量的作用域限定在一定的范围内;作用是将该变量的作用域只限于本文件模块中(即被声明的文件中)。
注意:用auto、register和static声明变量时,是在定义变量的基础上加上这些关键字,而不能单独使用。
Int a;
Static a;
这是错误的,只能在定义时加上以上关键字,厚泽编译时会被认为“重新定义”。
static int a;//这是正确的定义的方式
7.9.5存储类别小结
对一个数据的定义,需要指定两种属性:数据类型和存储类别。
下面从不同角度做些归纳:
(1)从作用域角度分,有局部变量和全局变量。它们采用的存储类别如下:
(2)从变量存在的实际(生存周期)来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储是在调用函数时临时分配单元。
(3)从变量值存放的位置来区分,可分为
(4)关于作用域和生存周期的概念,对于变量的属性来分析,一是变量的作用域,另一是变量值存在时间的长短,即生存周期。前者是从空间的角度,后者是从时间的角度。
7.10 关于变量的声明和定义
一个函数由两部分组成:声明部分和执行语句
声明部分的作用:对有关的标识符(变量、函数、结构体、共用体)的属性进行声明。
函数的声明是函数的原型,而函数的定义是对函数功能的定义。
对于变量而言,声明与定义的关系复杂一些。不过总结出来就是,建立存储空间的声明称定义,而把不需要建立存储空间的声明称为声明。
7.11 内部函数和外部函数
函数本质上是全局的,因为定义一个函数的目的就是要被另外的函数调用。如果不加声明的话,一个文件中的函数既可以被本文件中其他函数调用,也可以被其他文件中的函数调用。但是,凡事都有特例,也可指定某些函数不能被其他文件调用。
根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。
7.11.1内部函数
如果一个函数只能被本文件中其他函数锁调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加static,即
static 类型名 函数名(形参表);
如:
static int fun(int vara,int varb);
表示fun是一个内部函数,不能被其他文件调用
内部函数又称为静态函数,因为它是用static声明的。被static声明的函数作用域只局限于所在的文件。
通常把只能由本文件使用的函数和外部变量放在文件的开头,前面都冠以static使之局部化,其他文件不能引用。这就提高了程序的可靠性
7.11.2 外部函数
如果在定义函数时,在函数首部的最左端加关键字extern,则此函数是外部函数,可供其他文件调用,即
static 类型名 函数名(形参表);
如:
extern int function(int vara,int varb);
表示function是一个外部函数,可被其他文件调用。
C语言规定,如果在定义函数时省略extern,则默认为外部函数。
说明:在本章中接触到一些重要的概念和方法,这些对于日后程序的工作来说,是非常重要的,是必须了解和掌握的。尽可能把概念给记下来,多看多复习。