[C程序设计] 07 - 函数

若程序功能较多,规模较大,将所有的程序写在一个 main 主函数中,会使得主函数庞杂,阅读和维护困难大。于是提出模块化程序设计

在设计一个较大的程序时,往往将它分为若干个程序模块,每个模块包括一个或多个函数(function),每个函数实现一个特定的功能。
  • 一个 C 程序可由一个 main 函数和若干个其他函数构成,程序的执行是从 main 函数开始的。
  • 函数间可以相互调用,但其他函数不能调用 mian 函数,一个函数可以被一个或多个函数调用多次。
  • 所有函数都是平行的、相互独立的,函数不能嵌套定义。

从用户使用的角度看,函数分为:库函数和用户自定义的函数。

从函数形式上看,函数分为:无参函数和有参函数。

一、定义函数

C 语言要求程序中所有用到的函数,必须先定义后使用。定义函数应包括以下几个内容:

  1. 函数的名字。
  2. 函数的类型,即函数返回值的类型。
  3. 函数的参数名和参数类型(无参数函数不需要)。
  4. 函数的功能。

对于 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. 声明调用函数

在一个函数中调用另一个函数需要具备以下条件:

  1. 被调用的函数必须是已经定义的。
  2. 如果使用库函数需要在文件开头使用 #include 指令引入有关文件。
  3. 如果自定义的函数在调用它的函数(主调函数)之后,应该在主调函数中对被调用函数作声明(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. 在函数开头定义,只能在本函数内使用。
  2. 在函数内的复合语句中定义,只能在本复合语句中使用。
  3. 在函数外部定义,可以从定义之处起为本文件中所有函数调用。

前两种情况定义的是局部变量,最后一种情况定义的是全局变量。为了区分局部变量和全局变量,编写程序时通常将全局变量名的首字母大写(习惯,非规定)。
建议非必要不使用全局变量,因为:

  • 全局变量在程序执行的全程中占用存储单元。
  • 使函数的通用性降低。因为如果在函数中引用了全局变量,函数的执行情况会受到有关外部变量的影响,也不便于函数移植到其他程序文件。程序设计划分模块时,要求模块的内聚性强、与其他模块的耦合性弱,即模块功能单一、与其他模块的相互影响小,而使用全局变量不符合这一原则。
  • 使用全局变量过多,会降低程序的清晰度,难以清楚判读程序执行过程中外部变量的值。

四、变量的存储方式和生存期

1. 动态存储与静态存储

从变量作用域(空间)的角度,可以把变量分为局部变量和全局变量。而从变量值生存期(时间)的角度,有的变量在程序运行的这个过程都存在,有点变了只有调用其所在函数时才会被临时分配存储单元。因此,变量的存储方式有两种:

  1. 静态存储:在程序运行期间由系统分配固定的存储空间。
  2. 动态存储:在程序运行期间根据需要动态分配存储空间。

在内存中,供用户使用的存储空间分为 3 个部分:

  1. 程序区。
  2. 静态存储区:存放全局变量。
  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程序设计(第五版)》

你可能感兴趣的:([C程序设计] 07 - 函数)