第5章 C语言函数

5.1 函数简述

在学生时代的数学课上,老师用y=f(x, a, …)来说明数学中的函数。C语言是函数式语言,C语言函数的名称其实也借鉴了数学中的函数。

函数是按照模块化设计思想,实现特殊控制流程的程序块。

函数在内存表现为内存中的一段二进制代码,可以被CPU执行的一段机器码。而程序的执行只不过是程序代码段的顺序执行和跳转执行而已。函数调用属于跳转执行代码,将程序代码段指针跳到函数起始地址处开始执行代码,函数执行完成后代码段指针指向调用函数程序的下一行。

1.函数概述

函数的基本思想为将一个大的程序按功能分割成一些小模块。

函数特点为各模块相对独立、功能单一、结构清晰、接口简单,控制了程序设计的复杂性,提高元件的可靠性,缩短开发周期,避免程序开发的重复劳动,易于维护和功能扩充。函数开发方法为自上向下、逐步分解、分而治之。

C是函数式语言,必须有且只能有一个名为main的主函数,C程序的执行总是从main函数开始,在main中结束,函数不能嵌套定义,可以嵌套调用。

函数分类从用户角度可分为标准函数(库函数)和用户自定义函数。从函数调用或被调用形式上可分为有参函数和无参函数。从返回值上可划分为有返回值函数和无返回值函数,无返回值函数需要在函数名前加void关键字。

有参函数在函数定义及函数说明时都有参数,这些参数称为形式参数(简称为形参)。在函数调用时也必须给出参数,这些参数称为实际参数(简称为实参)。进行函数调用时,主调函数将把实参的值传送给形参,供被调函数使用。

使用库函数时应注意函数功能、函数参数的数目和顺序、各参数的意义和类型、函数返回值意义和类型、需要包含的头文件。

函数的返回值形式为:return(表达式)、return表达式、return三种。返回的作用是使程序控制从被调用函数返回到调用函数中,同时把返回值带给调用函数。函数中可有多个return语句(但良好的设计应保持只有一个return语句),若无return语句,遇}时,自动返回调用函数,若函数类型与return语句中表达式值的类型不一致,按照函数类型为准进行自动转换。函数返回值类型默认为int型。

函数调用形式为“函数名(实参表)”,调用时要求函数的实参与形参个数相等、类型一致、按顺序一一对应。

在同一文件,使用后面的函数(除返回值是int类型和void类型而且不带形参的函数)需要在文件头进行声明。如果有多个源文件,也可以把函数声明放在一个单独的头文件中,其他源文件包含此头文件。

2.从用户角度划分函数

从用户角度可划分为库函数和用户自定义函数,具体说明如下:

[1]库函数:由C系统提供,用户无须定义,也不必在程序中做类型说明,只须在程序前包含有该函数原型的头文件即可在程序中直接调用,如printf、scanf、strcpy、strcat等函数均属此类。

[2]用户自定义函数:由用户按需要自己写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而且在主调函数程序中还必须对该被调函数进行类型说明,然后才能使用。

3.从功能角度划分函数

从功能角度可以把C语言函数划分为如下13类:

[1]字符类型测试函数:用于对字符类型进行测试。

[2]转换函数:用于字符或字符串的转换,在字符量和各类数字量(整型、实型等)之间进行转换。

[3]目录路径函数:用于文件目录和路径操作。

[4]诊断函数:用于内部错误检测。

[5]图形函数:用于屏幕管理和各种图形功能。

[6]输入输出函数:用于完成输入输出功能。

[7]字符串函数:用于字符串操作和处理。

[8]内存管理函数:用于内存管理。

[9]数学函数:用于数学函数计算。

[10]日期和时间函数:用于日期、时间转换操作。

[11]进程控制函数:用于进程管理和控制。

[12]文件操作函数:用于对文件的操作。

[13]其他函数:用于其他各种功能的操作。

4.程序执行时的内存布局

当一个源代码通过gcc编译成a.out,执行a.out时,程序便开始了执行之旅(即进程)。操作系统为进程分配堆栈空间,随后把程序执行码放入文本段,把程序中经过初始化的全局变量和静态变量放入数据段,把程序中未初始化的全局变量和静态变量放入bss段,并对bss段数据初始化为0。之后CPU代码段指针指向main的入口,CPU堆栈段指针指向栈顶,代码段指针从main的入口地址顺序读取指令代码并进行执行,碰到局部变量和函数调用时,需要在栈顶分配空间并把堆栈段指针下移,碰到malloc等动态分配内存函数就在堆上分配内存。图5-1给出了程序执行时堆栈空间图,此图对于理解程序的运行机理非常重要。

图5-1 程序执行堆栈图

下面是对图5-1各段的具体说明。

.text:文本段,又称代码段,存放程序执行码。

.data:数据段,存放已初始化全局/静态变量,在整个程序执行过程中有效。

.bss:bss段,存放未初始化全局/静态变量,在整个程序执行过程中有效。

.stack:栈段,存放函数调用栈和函数局部变量,其中的内容在函数执行期间有效,并由编译器负责分配和收回。

.heap:堆段,由程序显式分配和收回,如果不回收就会产生内存泄漏。

数据段和bss段有时也统称为数据区。

5.2 函数变量

1.函数变量简述

在C语言中,每个变量和函数都有两个属性:数据类型和数据的存储类别。

变量存储类型的属性按生存期(时间)分为静态变量与动态变量,按照作用域(空间)分为局部变量与全局变量。

局部变量即内部变量,在函数内定义,只在本函数内有效。main中定义的变量只在main中有效,所以也属于局部变量。不同函数中的同名变量,占用不同内存单元。函数中的形参属于局部变量。局部变量可用存储类型有auto、register、static三种类型,局部变量默认为auto类型。

静态存储是程序运行前分配固定存储空间,如bss段和数据段。

动态存储是程序运行期间根据需要动态分配的存储空间,如程序运行时使用的栈段和使用malloc函数申请空间使用的堆段。

静态变量从程序开始执行前分配空间到程序执行结束才释放空间。静态变量属于静态存储,静态变量包括全局变量、静态全局变量、静态局部变量。

动态变量作用域是从包含该变量定义的函数开始执行至此函数执行结束。动态变量属于动态存储,函数中的变量属于动态变量。

2.变量存储类型关键字说明

变量的存储类型有auto(自动型)、register(寄存器型)、static(静态型)、extern (外部型)四种,下面是这四种存储类型的具体说明。

[1]auto:局部变量。编译器在默认的情况下,所有变量都是auto。

[2]register:寄存器变量。变量存在于CPU寄存器中,CPU寄存器不足时此变量当做auto变量来处理。寄存器变量只能是单个值,长度小于或等于整型变量,由于此变量存在于CPU的寄存器中,不能用“&”来获得地址。把经常使用的变量放入寄存器中是为了获得高速度。

[3]static:静态变量。在文件头定义的静态变量是全局静态变量,在函数中定义的静态变量是局部静态变量。静态变量在执行前将分配内存空间,在程序执行成后才释放内存空间。全局静态变量作用域为整个文件,局部静态变量作用域为单个函数。

[4]extern:外部变量。引用的变量是其在其他文件头中进行定义的。

5.3 函数定义与调用

5.3.1 函数定义

函数使用前必须先定义,函数定义从函数调用或被调用形式上可分为有参函数和无参函数两种,下面是对这两种函数定义形式的具体说明。

1.无参函数的定义形式

无参函数定义一般形式如下:

      类型标识符 函数名()
     {   变量定义部分
        语句
      }

函数名是由用户定义的标识符,下面是一个无参也无返回值的函数。

    void Hello()
    {
          printf ("Hello, world \n");
    }

2.有参函数定义的一般形式

有参函数比无参函数多了一个内容,即形式参数表列。在形参表中给出的参数称为形式参数,它们可以是各种类型的变量,各参数之间用逗号间隔。在进行函数调用时,主调函数将赋予这些形式参数实际的值。形参既然是变量,必须在形参表中给出形参的类型说明。

有参函数定义一般形式如下:

    类型标识符 函数名(形参类型  [形参名], …)
    {
        变量定义部分
          语句
    }

例如,定义一个函数,用于求两个数中的大数,可写为:

    int max(int a, int b)
    {
          if (a>b) return a;
          else return b;
    }

第一行说明max函数是一个整型函数,其返回的函数值是一个整数,形参a和b均为整型量。

3.静态函数

用static修饰的函数为静态函数。静态函数的作用域范围局限于本文件,又称为内部函数。使用内部函数的好处是不同的人在编写函数时,不用担心自己定义的函数,是否与其他文件中的函数同名。

5.3.2 函数的参数与返回值

1.函数的形参与实参

发生函数调用时,主调函数把实参的值传送给被调函数的形参,从而实现主调函数向被调函数的数据传送。

函数的形参和实参具有以下特点:

[1]C语言函数调用方式为传值调用方式。

[2]C语言函数调用时,为形参分配单元,并将实参的值复制到形参中。函数调用结束,形参单元被释放,实参单元仍保留并维持原值。

[3]C语言值传递的特点为形参与实参占用不同的内存单元,值为单向传递。

实参和形参在数量上、类型上、顺序上应严格一致,否则会发生类型不匹配的错误。

2.函数的返回值

函数的值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调函数的值。

函数的值只能通过return语句返回主调函数。

return语句的一般形式如下:

return表达式;

或者为return(表达式);

或者为return;

该语句的功能是计算表达式的值,并返回给主调函数。在函数中允许有多个return语句,但每次调用只能有一个return语句被执行,因此只能返回一个值。

函数返回值类型应与函数类型保持一致,如果两者不一致,则以函数类型为准,自动进行类型转换。

如果函数返回值为整型,在函数定义时可以省去类型说明。

没有返回值的函数,可以明确定义为“空类型”,类型说明符为“void”。

3.形参实参、变量与返回值综合举例

(1)有参函数举例。

formreal.c源代码如下:

    #include <stdio.h>
    int lable1 ;
    int lable2 = 0 ;
    static int lable3 ;
    int formreal(int x, int y)
    {
        static int lable4 ;
        int z ;
        printf("&lable4=%d\n", &lable4) ;
        printf("&x=%d, &y=%d\n", &x, &y) ;
        x= x*5 ;
        y= y*5 ;
        printf("x=%d, y=%d, lable1=%d, lable2=%d, lable3=%d, \
            lable4=%d\n", x, y, lable1, lable2, lable3, lable4) ;
        return 0 ;
    }
    int main()
    {
        int a, b, c, d;
        printf("&lable1=%d, &lable2=%d, &lable3=%d\n", \
              &lable1, &lable2, &lable3 ) ;
        printf("&a=%d, &b=%d, &c=%d, &d=%d\n", &a, &b, &c, &d) ;
        printf("input two numbers:\n");
        scanf("%d%d", &a, &b);
        c=formreal(a, b);
        printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d);
        return 0 ;
    }

编译gcc formreal.c -o formreal。

执行./formreal,执行结果如下:

    &lable1=134518780, &lable2=134518768, &lable3=134518776
    &a=-1075307972, &b=-1075307976, &c=-1075307980, &d=-1075307984
    input two numbers:
    2 3
    &lable4=134518772
    &x=-1075308016, &y=-1075308012
    x=10, y=15, lable1=0, lable2=0, lable3=0, lable4=0
    a=2, b=3, c=0, d=-1208725516

在上例中,x、y属于形参,调用c=formreal(a, b)语句时,CPU执行将堆栈指针下移,为函数、形参x与y分配内存单元,同时将实参a的值复制给形参x,将实参b的值复制给形参y。图5-2给出了函数调用时实参传值给形参的方式。

图5-2 函数调用时实参传值给形参方式图

(2)formreal程序执行堆栈表

表5-1列出了formreal程序执行时堆栈空间的内存模型,与实际执行堆栈空间并不完全一致。但可以让读者更好地理解全局变量、静态变量、实参和形参,特别是实参和形参是怎样进行传值调用的。

对表5-1设计的具体说明如下:

由于上面实例中使用的都是整型变量,整型变量占用内存空间为4个字节,所以这里以4个字节为单位对堆栈空间进行说明。

函数的返回值一般是通过CPU数据寄存器EAX返回,这里为了好理解,依然为函数返回值分配内存空间。

当执行./formreal时,操作系统会为执行码分配好代码段、静态数据段、bss段和栈段,然后代码指针指向main的入口。所以上述lable1~lable4变量是在程序执行前分配到数据段和bss段并完成赋值的。在这里要补充说明的是,静态局部变量作用范围是在编译时进行检查的,在编译后,静态局部变量和静态全局变量在执行程序看来,没有本质的差别。

从下面的执行过程可以看出,调用函数时系统为形参分配空间,将实参值复制给形参,所以说C语言传值调用为单向传值调用。

在代码编译成二进制码时,所有的变量失去意义,变量的操作其实都转化为对应内存地址的操作,变量其实只是一段内存空间值的抽象。编译完成后,机器代码中没有变量的存在,只有对内存线性逻辑地址的操作。

表5-1 formreal程序堆栈空间表

5.3.3 函数调用

1.函数调用简述

函数声明是说明函数的调用形式,函数声明必须是已存在的函数。使用库函数须要包含对应的头文件,包含方法为#include <*.h>。使用用户自定义函数需在文件内进行函数声明或将函数声明放在自定义头文件中然后进行包含,包含用户自定义头文件方法为#include "*.h"。

函数声明的一般形式为:“函数类型 函数名(形参类型 [形参名], …)”或“函数类型 函数名()”。函数声明的作用是告诉编译系统函数类型、参数个数及类型,以便检验。函数定义与函数声明不同,函数定义是函数的实现,函数声明是说明函数的调用形式,方便其他程序按接口调用。函数声明位置可在函数内或外。

函数调用时实参必须有确定的值,形参必须指定类型,形参与实参类型一致,个数相同。若形参与实参类型不一致,自动按形参类型转换。形参在函数被调用前不占内存,函数调用时为形参分配内存,调用结束,内存释放。

函数定义不可嵌套,但可以嵌套调用函数。

2.函数调用的方式

函数调用有如下三种方式:

[1]函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算,这种方式要求函数是有返回值的。例如,z=max(x, y)是一个赋值表达式,把max的返回值赋予变量z。

[2]函数语句:函数调用的一般形式加上分号即构成函数语句。例如,printf ("%d", a)和scanf ("%d", &b)都是以函数语句的方式调用函数。

[3]函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。例如,printf("%d", max(x, y)) ,即把max调用的返回值又作为printf函数实参来使用。在函数调用中还应该注意的一个问题是求值顺序的问题,所谓求值顺序是指对实参表中各量是自左至右使用还是自右至左使用,对此,各系统的规定不一定相同。

3.被调用函数的声明

在主调函数中调用某函数之前应对该被调函数进行声明,这与使用变量之前要先进行变量说明是一样的。在主调函数中对被调函数做说明的目的是使编译系统知道被调函数返回值的类型,以便在主调函数中按此种类型对返回值做相应的处理。

其一般形式为:

类型说明符 被调函数名(类型 形参,类型 形参……) ;

或为:

类型说明符 被调函数名(类型,类型……) ;

函数声明时圆括号内需给出形参类型和形参名,或只给出形参类型,这便于编译系统进行检错,以防止可能出现的错误。

例如,main函数中对max函数的说明如下:

    int max(int a, int b);

或写为:

    int max(int, int) ;

C语言中规定在以下几种情况时,可以省去函数声明。

[1]如果被调函数的返回值是int或void类型且没有形参时,可以不对被调函数做说明,而直接调用,这时系统将自动对被调函数返回值按整型处理。但不提倡不进行声明。

[2]当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数再做说明而直接调用。

[3]如在所有函数定义之前或头文件中对函数原型进行了声明,则在以后的各主调函数中,可不再对被调函数做说明。

4.函数的递归调用

一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。C语言允许函数的递归调用,在递归调用中,主调函数又是被调函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层。直到碰到结束条件然后递归返回。递归的次数是有限的,常用的办法是加条件判断,满足某种条件后就不再做递归调用,然后逐层返回。

5.用递归法计算n!

(1)n!的递归程序实现

用递归法计算n! ,可用下述公式表示:

按公式编程实现如下,recursion.c源代码如下:

    #include <stdio.h>
    int ff(int n)
    {
        int f;
        if(n<0) printf("n<0, input error");
        else if(n==0——n==1) f=1;
        else f=ff(n-1)*n;
        return(f);
    }
    void main()
    {
        int n;
        int y;
        printf("\n input a inteager number:\n");
        scanf("%d", &n);
        y=ff(n);
        printf("%d! =%ld", n, y);
    }

编译gcc recursion.c -o recursion。

执行./recursion,执行结果如下:

    input a inteager number:
    3
    3! =6

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

(2)recursion程序执行堆栈表

表5-2列出程序recursion递归调用时堆栈变化情况。

表5-2 递归调用堆栈表

根据表5-2,对递归调用流程说明如下:

[1]递归调用时,栈不停向下增长,直到碰到结束条件才依次向上返回。

[2]如ff(1)碰到结束条件f=1返回,此时ff(1)=1。

[3]上一级ff(2)中函数f=ff(1)*2, f=1*2=2, f返回给ff(2), ff(2)=2。

[4]再往上一级ff(3)中f=ff(2)*3, f=2*3=6, f返回给ff(3), ff(3)=6。

[5]将ff(3)赋值给main函数中变量y,所以打印出来的值为6。