若程序功能较多,规模较大,将所有的程序写在一个 main
主函数中,会使得主函数庞杂,阅读和维护困难大。于是提出模块化程序设计:
在设计一个较大的程序时,往往将它分为若干个程序模块,每个模块包括一个或多个函数(function),每个函数实现一个特定的功能。
- 一个 C 程序可由一个
main
函数和若干个其他函数构成,程序的执行是从main
函数开始的。 - 函数间可以相互调用,但其他函数不能调用
mian
函数,一个函数可以被一个或多个函数调用多次。 - 所有函数都是平行的、相互独立的,函数不能嵌套定义。
从用户使用的角度看,函数分为:库函数和用户自定义的函数。
从函数形式上看,函数分为:无参函数和有参函数。
一、定义函数
C 语言要求程序中所有用到的函数,必须先定义后使用。定义函数应包括以下几个内容:
- 函数的名字。
- 函数的类型,即函数返回值的类型。
- 函数的参数名和参数类型(无参数函数不需要)。
- 函数的功能。
对于 C 编译系统提供的库函数,是由编译系统事先定义好的,只需使用 #include
指令引入到程序文件中即可使用。
1. 定义无参函数
定义无参函数的一般形式有两种:
// 形式1
类型名 函数名()
{
函数体
}
// 形式2
类型名 函数名(void)
{
函数体
}
形式 2 中括号内 void
表示“空”,即函数没有参数。
定义函数时,如果函数无类型,即无函数值,可以使用 void
代替类型名。这时,执行函数后,不会把任何值带回 main
函数中。
2. 定义有参函数
定义有参函数的方法为:
类型名 函数名(形式参数表列)
{
函数体
}
例如,定义一个求两个数中最大值的函数:
int max(int x, int y)
{
int z;
z = x > y ? x : y;
return(z);
}
数组元素的作用与变量相当,因此,数组元素也可以作为函数实参,其用法与变量相同。此外,数组名也可以作为实参和形参。形参数组可以不指定数组大小(高维数组只能忽略第一维)。
float average(float array[], int n)
{
float aver, sum = array[0];
int i;
for(i = 1; i < n; i++)
sum = sum + array[i];
aver = sum / n;
return aver;
}
需要注意,使用数组名作为函数实参时,不是吧数组元素的值传递给形参,传递的是数组的第一个元素的地址,这样两个数组就占用同一段内存单元,当形参数组中各元素值发生变化,实参数组元素值也会发生变化。这与变量作函数参数的情况不同。
3. 定义空函数
所谓空函数,即函数体是空的:
类型名 函数名()
{}
在程序设计中,往往根据需要定义若干模块,分别由一些函数来实现。而在第一阶段只设计最基本的模块,其他一些功能需要后续补上。因此,在程序开始的最初阶段,可以在将来准备扩充的地方使用空函数。
二、调用函数
1. 函数调用的形式
函数调用的一般形式为:
函数名(实际参数表列)
如果调用的是无参函数,实参表列可以省略,但括号不能省略。函数可以单独调用,也可以出现在一个表达式中,这时要求函数返回一个确定的值以参加表达式的运算。同时,函数调用也可以作为另一个函数的实参:
m = max(a, max(b, c);
2. 数据传递
函数定义时函数名后面括号内的变量名称为形式参数,简称形参。函数调用时,函数名后面括号内的参数为实际参数,简称实参。实参可以时常量、变量或表达式。
在函数调用过程中,系统会把实参的值传递给形参。定义函数时指定的形参不会占据存储单元,只有函数被调用时,形参才会被分配临时内存单元。由于形参和实参在内存中占据不同的存储单元,调用结束后,形参单元被释放,实参单元保留原值。
3. 函数的返回值
函数的返回值通过 return
语句获得。return
语句后面的括号可以不要,return(x);
余 return x;
等价。return
语句中表达式的类型应该与函数定义时指定的类型一致。如果函数类型与 return
语句中表达式的类型不一致,以函数类型为准,数值型数据可自行转化。即函数类型决定返回值类型。对于不带返回值的函数,应当定义函数为 void
类型。
4. 声明调用函数
在一个函数中调用另一个函数需要具备以下条件:
- 被调用的函数必须是已经定义的。
- 如果使用库函数需要在文件开头使用
#include
指令引入有关文件。 - 如果自定义的函数在调用它的函数(主调函数)之后,应该在主调函数中对被调用函数作声明(declaration)。声明的作用是把函数名、函数参数等学习通知编译系统,以便在调用函数时能正确识别并检查调用的合法性。
函数的声明和函数的第一行基本一致,也把函数的首行称为函数原型(function prototype)。
#include
int main()
{
float add(float x, float y); // 函数的声明
float a, b;
scanf("%f, %f", &a, &b);
printf("sum is %f", add(a, b));
return 0;
}
float add(float x, float y)
{
return (x + y);
}
实际上,编译系统只关心参数个数和参数类型,而不检查参数名。因此函数声明中可以省略形参名,只保留参数类型:
float add(float, float);
如果已经在文件的开头(即所有函数之前)对本文件中所调用的所有函数进行了声明,则在函数中不必对其所调用的函数再做声明。
5. 嵌套调用
C 语言函数定义时相互平行、互相独立的,不能嵌套定义,但可以嵌套调用。
#include
int main()
{
int max4(int, int, int, int);
int a, b, c, d;
scanf("%d %d %d %d", &a, &b, &c, &d);
printf("max is %d", max4(a, b, c, d));
return 0;
}
int max2(int a, int b)
{
return(a>=b ? a:b);
}
int max4(int a, int b, int c, int d)
{
int max2(int, int);
return max2(max2(max2(a, b), c), d);
}
6. 递归调用
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。
#include
int main()
{
int fac(int);
int n;
scanf("%d", &n);
printf("%d", fac(n));
return 0;
}
int fac(int n)
{
if (n == 1)
return 1;
else
return fac(n - 1) * n;
}
三、局部变量和全局变量
每个变量被定义之后都只能在一定的范围内使用才有效,这就是变量的作用域问题。定义变量有 3 种情况:
- 在函数开头定义,只能在本函数内使用。
- 在函数内的复合语句中定义,只能在本复合语句中使用。
- 在函数外部定义,可以从定义之处起为本文件中所有函数调用。
前两种情况定义的是局部变量,最后一种情况定义的是全局变量。为了区分局部变量和全局变量,编写程序时通常将全局变量名的首字母大写(习惯,非规定)。
建议非必要不使用全局变量,因为:
- 全局变量在程序执行的全程中占用存储单元。
- 使函数的通用性降低。因为如果在函数中引用了全局变量,函数的执行情况会受到有关外部变量的影响,也不便于函数移植到其他程序文件。程序设计划分模块时,要求模块的内聚性强、与其他模块的耦合性弱,即模块功能单一、与其他模块的相互影响小,而使用全局变量不符合这一原则。
- 使用全局变量过多,会降低程序的清晰度,难以清楚判读程序执行过程中外部变量的值。
四、变量的存储方式和生存期
1. 动态存储与静态存储
从变量作用域(空间)的角度,可以把变量分为局部变量和全局变量。而从变量值生存期(时间)的角度,有的变量在程序运行的这个过程都存在,有点变了只有调用其所在函数时才会被临时分配存储单元。因此,变量的存储方式有两种:
- 静态存储:在程序运行期间由系统分配固定的存储空间。
- 动态存储:在程序运行期间根据需要动态分配存储空间。
在内存中,供用户使用的存储空间分为 3 个部分:
- 程序区。
- 静态存储区:存放全局变量。
- 动态存储区:存放函数形式参数、函数中定义的没有使用关键字
static
声明的变量(即自动变量)、函数调用时的现场保护和返回地址等。
2. 局部变量的存储类型
在 C 语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类型。存储类型指的是数据在内存中的存储方式(如静态存储和动态存储)。在定义和声明变量与函数时,一般应同时指定其数据类型和存储类型,也可以采用默认方式,即用户不指定,系统自动指定。
C 语言的存储类型有 4 种:自动的(auto)、静态的(static)、寄存器的(register)、外部的(extern)。
自动变量 auto
函数中的局部变量和形参,如果不专门声明 static
存储类别,都是动态分配存储空间。这类局部变量称为自动变量,使用关键字 auto
作为存储类型说明:
int f(int a) // 定义f函数,a为形参
{
auto int b, c = 1; // 定义b和c为自动变量
...
}
程序中大部分变量都是自动变量,定义时关键字 auto
可以省略。
静态局部变量 static
有时需要函数种局部变量的值在函数调用结束之后继续保留,即其占用的存储单元不释放。这时应使用关键字 static
指定该局部变量为静态局部变量。
#include
int main()
{
int f(int);
int n, i, r;
scanf("%d", &n);
for (i = 2; i < n + 1; i++)
r = f(i);
printf("%d\n", r);
return 0;
}
int f(int n)
{
static int x = 1;
x = x * n;
return x;
}
静态局部变量在编译时赋初值,只赋值一次,以后每次调用函数都不再重新赋值,而保留上次函数调用结束后的值。如果不赋初值,系统自动为数值型静态局部变量赋初值为 0,为字符型静态局部变量赋初值为 \0
。虽然静态局部变量在函数结束后依旧保留在内存种,但毕竟时局部变量,只能由定义的函数使用,其他函数不能调用。
由于静态局部变量多占内存,且降低了程序的可读性,因此非必要不使用。
寄存器变量 register
一般情况下变量的值是放在内存中的,当程序运行需要时,由控制器发出指令将该变量的值传到运算器,运算后如果需要继续存储,再传回到内存。如果有一些变量频繁使用,为节省存取变量的值花费的时间,可以将局部变量的值放到 CPU 的寄存器中,需要时直接从寄存器中读取。由于寄存器的存取速度远高于内存,这样做可以提高程序执行效率。这种变量称为寄存器变量,使用关键字 register
定义。
register int a;
由于现在计算机的性能越来越好,优化后的编译系统能识别出使用频繁的变量,自动将这些变量存放在寄存器中,不需要设计程序时单独指定。因此实际上用 register
声明变量的必要性不大。
3. 全局变量的存储类别
一般来说,外部变量是在函数外部定义的全局变量,它的作用域是从定义处开始,到本程序结束。在此作用域内,全局变量可以被各个函数使用。但有时会希望扩展外部变量的作用域。
在一个文件内扩展外部变量的作用域
如果外部变量不在文件的开头定义,其有效范围只限定于定义处到文件结束。如果由于某种考虑,需要定义点之前的程序能引用该外部变量,应使用关键字 extern
在引用前对该变量作外部变量声明。
#include
int main()
{
int max();
extern A, B, C; // 扩展外部变量A,B,C的作用域到此处
scanf("%d %d %d", &A, &B, &C);
printf("max is %d", max());
return 0;
}
int A, B, C; // 定义外部变量A,B,C
int max()
{
int m = A > B ? A : B;
if (C > m)
m = C;
return m;
}
使用 extern
声明外部变量时可以省略变量类型。通常提倡将外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个 extern
声明。
将外部变量的作用域扩展到其他文件
如果程序是由多个源程序文件组成的,想在一个文件中引用了一个文件定义的外部变量,此时不能在两个文件中同时定义该变量,而应在一个文件中定义后,在另一个文件中使用 extern
对变量做外部声明。用法与在文件内扩展外部变量的作用域用法相同。
实际上,编译系统遇到 extern
时,会现在本文件中寻找外部变量的定义,如果找到,就在本文件中扩展外部变量的作用域,找不到时再从其他文件中寻找。
将外部变量的作用域限制在本文件中
如果希望某些外部变量只能被本文件引用,可以在定义外部变量时加一个 static
声明:
static int A;
这种只能用于本文件的外部变量称为静态外部变量。
五、内部函数与外部函数
变量由作用域,用局部和全局之分,函数也有类似的问题。函数本质上时全局的,因为定义一个函数的目的就是希望它能被其他函数调用。如果不加声明,一个函数可以被本文件和其他文件中的函数调用。也可以指定某些函数不能被其他函数调用。根据函数是否能被其他源文件调用,分为内部函数和外部函数。
1. 内部函数
如果一个函数只能被本文件中的其他函数调用,称为内部函数,又称为静态函数。定义时在函数类型前面加 static
关键字:
static int f(int x)
通常把只能本文件使用的静态外部变量和静态函数放在文件开头,提高程序可读性。
2. 外部函数
定义函数时在函数类型前加 extern
关键字,指定该函数为外部函数,可被其他文件调用。extern
可以省略:
extern int f(intx)
调用外部函数也要先声明,如果该函数来自其他文件,声明时也要加关键字 extern
。
Reference:
谭浩强《C程序设计(第五版)》