第3章 函数

函数是C语言中的一种基本模块,实际上,一个C语言程序就是由若干个模块化的函数所构成的。前面我们已经看到,C语言程序总是由主函数main()开始,main()函数是一个控制程序流程的特殊函数,它是程序的起点。在进行程序设计的过程中,如果所设计的程序较大,一般应将其分成若干个子程序模块,每个模块完成一种特定的功能。在C语言中,子程序是用函数来实现的。对于一些需要经常使用的子程序可以设计成一个专门的函数库,以供反复调用。此外,Keil Cx51编译器还提供了丰富的运行库函数,用户可以根据需要随时调用。这种模块化的程序设计方法,可以大大提高编程效率和速度。

3.1 函数的定义

从用户的角度来看,有两种函数:标准库函数和用户自定义函数。标准库函数是Keil Cx51编译器提供的,不需要用户进行定义,可以直接调用。用户自定义函数是用户根据自己需要编写的能实现特定功能的函数,它必须先进行定义之后才能调用。函数定义的一般形式为:

函数类型  函数名(形式参数表)
    形式参数说明
    {
      局部变量定义
      函数体语句
    }

其中,“函数类型”说明了自定义函数返回值的类型。

“函数名”是用标识符表示的自定义函数名字。

“形式参数表”中列出的是在主调用函数与被调用函数之间传递数据的形式参数,形式参数的类型必须加以说明。ANSI C标准允许在形式参数表中对形式参数的类型进行说明。如果定义的是无参函数,可以没有形式参数表,但圆括号不能省略。

“局部变量定义”是对在函数内部使用的局部变量进行定义。

“函数体语句”是为完成该函数的特定功能而设置的各种语句。

如果定义函数时只给出一对花括号{}而不给出其局部变量和函数体语句,则该函数为“空函数”,这种空函数也是合法的。在进行C语言模块化程序设计时,各模块的功能可通过函数来实现。开始时只设计最基本的模块,其他作为扩充功能在以后需要时再加上。编写程序时可在将来准备扩充的地方写上一个空函数,这样可使程序的结构清晰,可读性好,而且易于扩充。

例3.1:定义一个计算整数的正整数次幂的函数。

int power(x, n)
  int x, n;
  {
    int i, p;
    p=1;
    for(i=1; i<=n; ++i)
      p=p*x;
    return(p);
  }

这里定义了一个返回值为整型值的函数power(),它有两个形式参数:x,n。形式参数的作用是接受从主调用函数传递过来的实际参数的值。上例中形式参数x和n被说明为int类型。花括号以内的部分是自定义函数的函数体。上例中在函数体内定义了两个局部变量i和p,它们均为整型数据。

需要注意的是,形式参数的说明与函数体内的局部变量定义是完全不同的两个部分,前者应写在花括号的外面,而后者是函数体的一个组成部分,必须写在花括号的里面。为了不发生混淆,ANSI C标准允许在形式参数表中对形式参数的类型进行说明,如上例可写成:int power(int x, int n)。

在函数体中可以根据用户自己的需要,设置各种不同的语句。这些语句应能完成所需要的功能。上例在函数体中用一个for循环结构完成一个整数的正整数次幂的计算,计算结果赋值给变量p。函数体中最后一条语句return(p)的作用是将p的值返回到主调用函数中去。return语句后面圆括号中的值称为函数的返回值,圆括号可以省略,即return p和return(p)是等价的。

由于p是函数的返回值,因此在函数体中进行变量定义时,应将变量p的类型定义得与函数本身的类型相一致。如果二者类型不一致,则函数调用时的返回值可能发生错误。如果函数体中没有return语句,则该函数由函数体最后面的右闭花括号“}”返回。在这种情况下,函数的返回值是不确定的。

对于不需要有返回值的函数,可以将该函数定义为void类型(空类型)。对于上例,如果定义为:void power(int x, int n),则可将函数体中的return语句去掉,这样,编译器会保证在函数调用结束时不使函数返回任何值。为了使程序减少出错,保证函数的正确调用,凡是不要求有返回值的函数,都应将其定义成void类型。

例3.2:不同函数的定义方法。

char fun1(x, y)               /* 定义一个char型函数 */
int x;                        /* 说明形式参数的类型 */
char y;
  {
  char z;                     /* 定义函数内部的局部变量 */
  z=x+y;                      /* 函数体语句 */
    return(z);                /* 返回函数的值z,注意变量z与函数本身
   }                             的类型均为char型 */
  int fun2(float a, float b)  /* 定义一个int型函数,在形式参数表中说
   {                              明形式参数的类型 */
    int x;                    /* 定义函数内部的局部变量 */
    x=a-b;                    /* 函数体语句 */
    return(x);                /* 返回函数的值x,注意变量x与函数本身
   }                              的类型均为int型 */
  long fun3()                 /* 定义一个long型函数,它没有形式参数 */
   {
    long x;                   /* 定义函数内部的局部变量 */
    int i, j;
    x=i*j;                    /* 函数体语句 */
    return(x);                /* 返回函数的值x,注意变量x与函数本身
   }                              的类型均为long型 */
  void fun4(char a, char b)   /* 定义一个无返回值的void型函数 */
   {
    char x;                   /* 局部变量定义 */
    x=a+b;                    /* 函数体语句 */
   }                          /* 函数不需要返回值,省略return语句 */
  void fun5( )                /* 定义一个空函数 */
   {
   }

3.2 函数的调用

3.2.1 函数的调用形式

C语言程序中函数是可以互相调用的。所谓函数调用就是在一个函数体中引用另外一个已经定义了的函数,前者称为主调用函数,后者称为被调用函数。函数调用的一般形式为:

函数名(实际参数表)

其中,“函数名”指出被调用的函数。

“实际参数表”中可以包含多个实际参数,各个参数之间用逗号隔开。实际参数的值被传递给被调用函数中的形式参数。需要注意的是,函数调用中的实际参数与函数定义中的形式参数必须在个数、类型及顺序上严格保持一致,以便将实际参数的值正确地传递给形式参数。否则在函数调用时会产生意想不到的结果。如果调用的是无参函数,则可以没有实际参数表,但圆括号不能省略。

在C语言中可以采用三种方式完成函数的调用。

(1)函数语句

在主调函数中将函数调用作为一条语句,例如:

fun1();

这是无参调用,它不要求被调用函数返回一个确定的值,只要求它完成一定的操作。

(2)函数表达式

在主调函数中将函数调用作为一个运算对象直接出现在表达式中,这种表达式称为函数表达式。例如:

c = power(x,n) + power(y,m);

这其实是一个赋值语句,它包括两个函数调用,每个函数调用都有一个返回值,将两个返回值相加的结果,赋值给变量c。因此这种函数调用方式要求被调函数返回一个确定的值。

(3)函数参数

在主调函数中将函数调用作为另一个函数调用的实际参数。例如:

y=power(power(i, j), k);

其中,函数调用power(i, j)放在另一个函数调用power(power(i, j), k)的实际参数表中,以其返回值作为另一个函数调用的实际参数。这种在调用一个函数的过程中又调用了另外一个函数的方式,称为嵌套函数调用。在输出一个函数的值时经常采用这种方法,例如:

printf("%d", power(i,j));

其中,函数调用power(i,j)是作为printf()函数的一个实际参数处理的,它也属于嵌套函数调用方式。

3.2.2 对被调用函数的说明

与使用变量一样,在调用一个函数之前(包括标准库函数),必须对该函数的类型进行说明,即“先说明,后调用”。如果调用的是库函数,一般应在程序的开始处用预处理命令#include将有关函数说明的头文件包含进来。例如前面例子中经常出现的预处理命令#include<stdio.h>,就是将与库输出函数printf()有关的头文件stdio.h包含到程序文件中来。头文件“stdio.h”中有关于库输入输出函数的一些说明信息,如果不使用这个包含命令,库输入输出函数就无法被正确地调用。

如果调用的是用户自定义函数,而且该函数与调用它的主调函数在同一个文件中,一般应该在主调函数中对被调用函数的类型进行说明。函数说明的一般形式为:

类型标识符  被调用的函数名(形式参数表);

其中,“类型标识符”说明了函数返回值的类型。

“形式参数表”中说明各个形式参数的类型。

需要注意的是,函数的说明与函数的定义是完全不同的。函数的定义是对函数功能的确立,它是一个完整的函数单位。而函数的说明,只是说明了函数返回值的类型。二者在书写形式上也不一样,函数说明结束时在圆括号的后面需要有一个分号“;”作为结束标志,而在函数定义时,被定义函数名的圆括号后面没有分号“;”,即函数定义还未结束,后面应接着书写形式参数说明和被定义的函数体部分。

如果被调函数是在主调函数前面定义的,或者已经在程序文件的开始处说明了所有被调函数的类型,在这两种情况下可以不必再在主调函数中对被调函数进行说明。也可以将所有用户自定义函数的说明另存为一个专门的头文件,需要时用#include将其包含到主程序中去。

C语言程序中不允许在一个函数定义的内部包括另一个函数的定义,即不允许嵌套函数定义。但是允许在调用一个函数的过程中包含另一个函数调用,即嵌套函数调用在C语言程序中是允许的。

例3.3:函数调用的例子。

#include <stdio.h>
int Max(int x, int y);          /* 对被调用函数进行说明 */
void main() {                   /* 主函数 */
  int a, b;                     /* 主函数的局部变量定义 */
  printf("input a and b: \n");
  scanf("%d %d", &a, &b);       /* 调用库输入函数,从键盘获得a、b的值 */
  printf("Max=%d", Max(a, b));  /* 调用库输出函数,输出a、b中较大者的值*/
  while(1);
}
int Max(int x, int y) {         /* 功能函数定义 */
  int z;                        /* 局部变量定义 */
  if(x>y)                       /* 函数体语句 */
    z=x;
  else
    z=y;
  return(z);
}

程序执行结果:

input a and b:
123  456 回车
Max=456

在这个例子中,主函数main()先调用库输入函数sacnf(),从键盘输入两个值分别赋值给局部变量a和b,然后调用库输出函数printf()将a、b中较大者输出。在调用库输出函数printf()的过程中又调用了自定义功能函数Max(),将键盘输入的a、b的值作为实际参数传递给Max()函数中的形式参数x、y。在Max()函数中对实际输入值进行比较以获得较大者的值。这也是一个嵌套函数调用的例子,图3.1是例3.3程序中函数调用的执行过程。

图3.1 函数的嵌套调用过程

3.2.3 函数的参数与返回值

通常在进行函数调用时,主调用函数与被调用函数之间具有数据传递关系。这种数据传递是通过函数的参数实现的。在定义一个函数时,位于函数名后面圆括号中的变量名称为“形式参数”,而在调用函数时,函数名后面括号中的表达式称为“实际参数”。形式参数在未发生函数调用之前,不占用内存单元,因而也是没有值的。只有在发生函数调用时才为它分配内存单元,同时获得从主调用函数中实际参数传递过来的值。函数调用结束后,它所占用的内存单元也被释放。

实际参数可以是常数,也可以是变量或表达式,但要求它们具有确定的值。进行函数调用时,主调用函数将实际参数的值传递给被调用函数中的形式参数。为了完成正确的参数传递,实际参数的类型必须与形式参数的类型一致,如果两者不一致,则会发生“类型不匹配”错误。

例3.4:计算一个整数的正整数次幂。

#include<stdio.h>
 main()  {
  int power(int x, int n);
  int a, b, c;
  printf("please input X and n: \n");
  scanf("%d %d",&a, &b);
  c=power(a, b);
  printf("\n%d to the power of %d is: %d", a, b, c);
  while(1);
 }
 int power(int x, int n) {
  int i, p;
  p=1;
  for(i=1; i<=n; ++i)
    p=p*x;
  return(p);
 }

程序执行结果:

  please input X and n:
  5, 3  回车
  5 to the power of 3 is 125

在这个程序中定义了一个计算整数的正整数次幂的函数int power(int x, int n);它有两个整型的形式参数x和n。在程序开始时,变量x和n是不占用内存单元的,因此也是没有值的。在主函数main()中先从键盘输入两个整数值a和b,然后通过函数调用语句c=power(a, b);将实际参数a和b的值传递给被调用函数power()中的形式参数。调用发生时,形式参数变量x和n被赋以实际参数a和b的值,从而使函数power()能按实际参数的值进行计算。从这个例子可以看到,形式参数和实际参数可以不同名,但它们的类型必须要一致。

一般情况下,希望通过函数调用使主调用函数获得一个确定的值,这就是函数的返回值。例如,上例中的函数调用语句c=power(a, b);就是将函数power()的返回值赋给变量c。函数的返回值是通过return语句获得的,如果希望从被调用函数中带回一个值到主调用函数,被调用函数中必须包含有return语句。

一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个return语句起作用。return后面可以跟一个表达式,例如,return(x>y? x: y);这种写法只用一条return语句即可同时完成表达式的计算和函数值的返回。return后面还可以跟另外一个已定义了的函数名,例如:return keyval(rdkey);采用这种写法可实现函数的嵌套调用,即在函数返回的同时调用另一个函数。

函数返回值的类型确定了该函数的类型,因此在定义一个函数时,函数本身的类型应与return语句中变量或表达式的类型一致。例如,上例中power()函数被定义为int类型,return语句中的变量p也被定义为int类型。如果函数类型与return语句中表达式的值类型不一致,则以函数的类型为准。对于返回的数值数据可以自动进行类型转换,即函数的类型决定返回值的类型。如果不需要被调用函数返回一个确定的值,则可以不要return语句,同时应将被调用函数定义成void类型。事实上,main()函数就是一个典型的没有返回值的函数,因此可以将其写成void main()的形式。由于void类型的函数没有return语句,因此在一个void类型函数的调用结束时,将从该函数的最后一个花括号处返回到主调用函数。

例3.5:使用void类型函数的例子。

#include<stdio.h>
void main()  {              /* void类型的主函数 */
    void prn_char(char x);  /* 功能函数说明 */
    prn_char('w');          /* 功能函数调用 */
    while(1);
  }
void prn_char(char x)       /* 将功能函数定义为void类型,无返回值 */
  {
    printf("%c has ASCII value %bd\n",x,x);
}                           /* void型功能函数从此处返回主调用函数 */

程序执行结果:

  w has ASCII value 119

3.2.4 实际参数的传递方式

在进行函数调用时,必须用主调函数中的实际参数来替换被调函数中的形式参数,这就是所谓的参数传递。在C语言中,对于不同类型的实际参数,有三种不同的参数传递方式。

(1)基本类型的实际参数传递

当函数的参数是基本类型的变量时,主调函数将实际参数的值传递给被调函数中的形式参数,这种方式称为值传递。前面讲过,函数中的形式参数在未发生数调用之前是不占用内存单元的,只有在进行函数调用时才为其分配临时存储单元。而函数的实际参数是要占用确定的存储单元的。值传递方式是将实际参数的值传递到为被调函数中形式参数分配的临时存储单元中,函数调用结束后,临时存储单元被释放,形式参数的值也就不复存在,但实际参数所占用的存储单元保持原来的值不变。这种参数传递方式在执行被调函数时,如果形式参数的值发生变化,可以不必担心主调函数中实际参数的值会受到影响。因此值传递是一种单向传递。

(2)数组类型的实际参数传递

当函数的参数是数组类型的变量时,主调函数将实际参数数组的起始地址传递到被调函数中形式参数的临时存储单元,这种方式称为地址传递。地址传递方式在执行被调函数时,形式参数通过实际参数传来的地址,直接到主调函数中去存取相应的数组元素,故形式参数的变化会改变实际参数的值。因此地址传递是一种双向传递。

(3)指针类型的实际参数传递

当函数的参数是指针类型的变量时,主调函数将实际参数的地址传递给被调函数中形式参数的临时存储单元,因此也属于地址传递。在执行被调函数时,也是直接到主调函数中去访问实际参数变量,在这种情况下,形式参数的变化会改变实际参数的值。

前面介绍的一些函数调用中所涉及的都是基本类型的实际参数传递,这种参数传递方式比较容易理解和应用。关于数组类型和指针类型实际参数的传递较为复杂,将在第4章中详细介绍。

3.3 函数的递归调用与再入函数

如果在调用一个函数的过程中又间接或直接地调用该函数本身,称为函数的递归调用。例如,计算阶乘函数f (n)=n!,可以先计算f (n-1)=(n-1)!,而计算f (n-1)时又可以先计算f (n-2)=(n-2)!,这就是递归算法。再入函数是一种可以在函数体内直接或间接调用其自身的一种函数,显然再入函数是可以进行递归调用的。

Keil Cx51编译器采用一个扩展关键字reentrant,作为定义函数时的选项,需要将一个函数定义为再入函数时,只要在函数名后面加上关键字reentrant即可:

函数类型  函数名(形式参数表)[reentrant]

再入函数可被递归调用,无论何时,包括中断服务函数在内的任何函数都可调用再入函数。与非再入函数的参数传递和局部变量的存储分配方法不同,Cx51编译器为再入函数生成一个模拟栈,通过这个模拟栈来完成参数传递和存放局部变量。模拟栈所在的存储器空间根据再入函数存储器模式的不同,可以是DATA、PDATA或XDATA存储器空间。当程序中包含有多种存储器模式的再入函数时,Cx51编译器为每种模式单独建立一个模拟栈并独立管理各自的栈指针。对于再入函数有如下规定。

① 再入函数不能传送bit类型的参数,也不能定义一个局部位变量,再入函数不能包括位操作以及8051系列单片机的可位寻址区。

② 与PL/M51兼容的函数不能具有reentrant属性,也不能调用再入函数。

③ 在编译时存储器模式的基础上为再入函数在内部或外部存储器中建立一个模拟堆栈区,称为再入栈。在Small模式下再入栈位于IDATA区,在compact模式下再入栈位于PDATA区,在Large模式下再入栈位于XDATA区。再入函数的局部变量及参数都被放在再入栈中,从而使再入函数可以进行递归调用。而非再入函数的局部变量被放在再入栈之外的暂存区内,如果对非再入函数进行递归调用,则上次调用时使用的局部变量数据将被覆盖。

④ 在同一个程序中可以定义和使用不同存储器模式的再入函数,任意模式的再入函数不能调用不同模式的再入函数,但可任意调用非再入函数。

⑤ 在参数的传递上,实际参数可以传递给间接调用的再入函数。无再入属性的间接调用函数不能包含调用参数,但是可以使用定义的全局变量来进行参数传递。

例3.6:利用函数的递归调用计算整数的阶乘。

#include<stdio.h>
fac(int n) reentrant {
  if (n<1)  return(1);
  else     return(n*fac(n-1));
}
main() {
  int n;
  printf("please input a number: \n");
  scanf("%d", &n);
  printf("fac(%d)=%d\n", n, fac(n));
  while(1);
}

程序执行结果:

please input a number
3  回车
fac(3)=6

在这个程序中定义了一个再入函数fac(n),它是用来计算阶乘n!的函数。在fac()的函数体中又调用了fac()函数本身,因此这是一种函数的递归调用。再入函数在进行递归调用时,新的局部变量和参数在再入栈中重新分配存储单元,并以新的变量重新开始执行。每次递归调用返回时,前面压入的局部变量和参数会从再入栈中弹出,并恢复到上次调用自身的地方继续执行。如果是非再入函数进行递归调用,每次调用函数自身时,上次调用时使用的局部变量数据将被覆盖,因而在递归调用结束时不能得到正确的结果。对于例3.6的程序,如果将函数fac(n)定义成非再入函数,则程序的运行结果为0,显然这是不正确的。

采用函数的递归调用可使程序的结构紧凑,但是递归调用要求采用再入函数,以便利用再入栈来保存有关的局部变量数据,从而要占据较大的内存空间。另外递归调用时对函数的处理速度也比较慢,因此一般情况下应尽量避免采用函数递归调用,定义函数时应尽量避免使用再入属性。

3.4 中断服务函数与寄存器组定义

Keil Cx51编译器支持在C语言源程序中直接编写8051单片机的中断服务函数程序,从而减轻了采用汇编语言编写中断服务程序的烦琐程度。为了在C语言源程序中直接编写中断服务函数的需要,Keil Cx51编译器对函数的定义进行了扩展,增加了一个扩展关键字interrupt,它是函数定义时的一个选项,加上这个选项即可以将一个函数定义成中断服务函数。定义中断服务函数的一般形式为:

函数类型  函数名(形式参数表)[interrupt n][using n]

关键字interrupt后面的n是中断号,n的取值范围为0~31。编译器从8n+3处产生中断向量,具体的中断号n和中断向量取决于8051系列单片机芯片型号,常用中断源和中断向量如表3-1所示。

表3-1 常用中断号与中断向量

8051系列单片机可以在片内RAM中使用4个不同的工作寄存器组,每个寄存器组中包含8个工作寄存器(R0~R7)。Keil Cx51编译器扩展了一个关键字using,专门用来选择8051单片机中不同的工作寄存器组。using后面的n是一个0~3的常整数,分别选中4个不同的工作寄存器组。在定义一个函数时using是一个选项,如果不用该选项,则由编译器自动选择一个寄存器组作绝对寄存器组访问。需要注意的是,关键字using和interrupt的后面都不允许跟带运算符的表达式。

关键字using对函数目标代码的影响如下:在函数的入口处将当前工作寄存器组保护到堆栈中;指定的工作寄存器内容不会改变;函数退出之前将被保护的工作寄存器组从堆栈中恢复。

使用关键字using在函数中确定一个工作寄存器组时必须十分小心,要保证任何寄存器组的切换都只在仔细控制的区域内发生,如果不做到这一点将产生不正确的函数结果。另外还要注意,带using属性的函数原则上不能返回bit类型的值。并且关键字using不允许用于外部函数。

关键字interrupt也不允许用于外部函数,它对中断函数目标代码的影响如下:在进入中断函数时,特殊功能寄存器ACC、B、DPH、DPL、PSW将被保存入栈;如果不使用关键字using进行工作寄存器组切换,则将中断函数中所用到的全部工作寄存器都入栈保存;函数退出之前所有的寄存器内容出栈恢复;中断函数由8051单片机指令RETI结束。

下面给出一个带有寄存器组切换的中断函数定义的例子,该例中还给出了C51编译器所生成的8051单片机的指令代码。

例3.7:带有寄存器组切换的中断函数定义。

stmt level   source
 1          #pragma  cd
 2          #include <reg51.h>
 3          extern void alfunc(bit b0);
 4          extern bit alarm;
 5          int DTIMES;
 6          char bdata flag;
 7          sbit flag0=flag^0;
 8          int dtime1=0x0a;
 9
10         void int0 () interrupt 0 using 1 {
11   1        TR1=0;
12   1        flag0=!flag0;
13   1        DTIMES=dtime1;
14   1        dtime1=0;
15   1        TR1=1;
16   1      }
17
18         void timer1 () interrupt 3 using 3 {
19   1        alfunc(alarm=1);
20   1        TH1=0x3c;
21   1        TL1=0xB0;
22   1        dtime1=dtime1+1;
23   1        if (dtime1==0)
24   1          {
25   2          P0=0;
26   2          }
27   1       }
28
ASSEMBLY LISTING OF GENERATED OBJECT CODE
              ;FUNCTION int0 (BEGIN)
                                            ;SOURCE LINE # 10
                                            ;SOURCE LINE # 11
0000 C28E            CLR    TR1
                                            ;SOURCE LINE # 12
0002 B200       R    CPL    flag0
                                            ;SOURCE LINE # 13
0004850000     R    MOV    DTIMES,dtime1
0007850000     R    MOV    DTIMES+01H,dtime1+01H
                                            ;SOURCE LINE # 14
000A 750000     R    MOV    dtime1,#00H
000D 750000     R    MOV    dtime1+01H,#00H
                                            ;SOURCE LINE # 15
0010 D28E            SETB   TR1
                                            ;SOURCE LINE # 16
0012 32              RETI
            ;FUNCTION int0 (END)
            ;FUNCTION timer1 (BEGIN)
0000 C0E0            PUSH   ACC
0002 C0F0            PUSH   B
0004 C083            PUSH   DPH
0006 C082            PUSH   DPL
0008 C0D0            PUSH   PSW
000A 75D018          MOV    PSW,#018H
                                            ;SOURCE LINE # 18
                                            ;SOURCE LINE # 19
000D D3              SETB     C
000E 9200       E    MOV     alarm,C
00109200       E    MOV     ?alfunc?BIT,C
0012120000     E    LCALL   alfunc
                                            ;SOURCE LINE # 20
0015758D3C          MOV    TH1,#03CH
                                            ;SOURCE LINE # 21
0018758BB0          MOV    TL1,#0B0H
                                            ;SOURCE LINE # 22
001B 0500       R    INC    dtime1+01H
001D E500       R    MOV    A,dtime1+01H
001F 7002            JNZ    ?C0004
00210500       R    INC    dtime1
0023        ?C0004:
                                            ;SOURCE LINE # 23
00234500       R    ORL    A,dtime1
00257002            JNZ    ?C0003
                                            ;SOURCE LINE # 24
                                            ;SOURCE LINE # 25
0027 F580            MOV    P0,A
                                            ;SOURCE LINE # 26
                                            ;SOURCE LINE # 27
0029        ?C0003:
0029 D0D0            POP    PSW
002B D082            POP    DPL
002D D083            POP    DPH
002F D0F0            POP    B
0031 D0E0            POP    ACC
0033 32              RETI
            ;FUNCTION timer1 (END)

编写8051单片机中断函数时应遵循以下规则。

① 中断函数不能进行参数传递,如果中断函数中包含任何参数声明都将导致编译出错。

② 中断函数没有返回值,如果企图定义一个返回值将得到不正确的结果。因此建议在定义中断函数时将其定义为void类型,以明确说明没有返回值。

③ 在任何情况下都不能直接调用中断函数,否则会产生编译错误。因为中断函数的退出是由8051单片机指令RETI完成的,RETI指令影响8051单片机的硬件中断系统。如果在没有实际中断请求的情况下直接调用中断函数,RETI指令的操作结果会产生一个致命的错误。

④ 如果在中断函数中调用了其他函数,则被调用函数所使用的寄存器组必须与中断函数相同。用户必须保证按要求使用相同的寄存器组,否则会产生不正确的结果,这一点必须引起足够的注意。如果定义中断函数时没有使用using选项,则由编译器自动选择一个寄存器组作绝对寄存器组访问。另外,由于中断的产生不可预测,中断函数对其他函数的调用可能形成递规调用,需要时可将被中断函数所调用的其他函数定义成再入函数。

⑤ Keil Cx51编译器从绝对地址8n+3处产生一个中断向量,其中n为中断号。该向量包含一个到中断函数入口地址的绝对跳传。在对源程序编译时,可用编译控制命令NOINTVECTOR抑制中断向量的产生,从而使用户有能力从独立的汇编程序模块中提供中断向量。

3.5 函数变量的存储方式

3.5.1 局部变量与全局变量

按照变量的有效作用范围可划分为局部变量和全局变量。局部变量是在一个函数内部定义的变量,该变量只在定义它的那个函数范围以内有效。在此函数之外局部变量即失去意义,因而也就不能使用这些变量了。不同的函数可以使用相同的局部变量名,由于它们的作用范围不同,不会相互干扰。函数的形式参数也属于局部变量。在一个函数内部的复合语句中也可以定义局部变量,该局部变量只在该复合语句中有效。

全局变量是在函数外部定义的变量,又称为外部变量。全局变量可以为多个函数共同使用,其有效作用范围是从它定义的位置开始到整个程序文件结束。如果全局变量定义在一个程序文件的开始处,则在整个程序文件范围内都可以使用它。如果一个全局变量不是在程序文件的开始处定义的,但又希望在它的定义点之前的函数中引用该变量,这时应在引用该变量的函数中用关键字extern将其说明为“外部变量”。另外,如果在一个程序模块文件中引用另一个程序模块文件中定义的变量时,也必须用extern进行说明。

外部变量说明与外部变量定义是不相同的。外部变量定义只能有一次,定义的位置在所有函数之外。而同一个程序文件中的外部变量说明可以有多次,说明的位置在需要引用该变量的函数之内。外部变量说明的作用只是声明该变量是一个已经在外部定义过了的变量而已。

如果在同一个程序文件中,全局变量与局部变量同名,则在局部变量的有效作用范围之内,全局变量不起作用。换句话说,局部变量的优先级比全局变量高。在编写C语言程序时,不是特别必要的地方一般不要使用全局变量,而应当尽可能地使用局部变量。这是因为局部变量只在使用它时,才为其分配内存单元,而全局变量在整个程序的执行过程中都要占用内存单元。另外,如果使用全局变量过多,在各个函数执行时都有可能改变全局变量的值,使人们难以清楚地判断出在各个程序执行点处全局变量的值,这样会使降低程序的通用性和可读性。

还有一点需要说明,如果程序中的全局变量在定义时赋给了初值,按ANSI C标准规定,在程序进入main()函数之前必须先对该全局变量进行初始化。这是由连接定位器BL51对目标程序连接定位时,在最后生成的目标代码中自动加入一段运行库“INIT.OBJ”来实现的。由于增加了这么一段代码,程序的长度会增加,运行速度也会受到影响。因此要限制使用全局变量。

下面通过一个例子来说明局部变量与全局变量的区别。

例3.8:局部变量与全局变量的区别。

#include<stdio.h>
int a=3, b=5;          /* 定义a、b为全局变量,并赋以初值 */
max(int a, int b)  {   /* 形参a、b为局部变量 */
      int c;           /* 定义c为局部变量 */
  c=a>b? a:b;
  return(c);
}
main()  {
  int a=8;             /* 定义a为局部变量 */
  printf("%d", max(a,b));
  while(1);
}

程序执行结果:

8

这个程序中故意使用了相同的变量名a和b,请读者仔细区别它们的作用范围。程序的第一行将a和b定义成全局变量并且赋了初值,由于具有初值的全局变量需要先行初始化,因此读者如果用dScope51对这个例子程序进行调试,可以看到程序在进入main()函数之前,除了要执行一段启动程序STARTUP的代码之外,还需要执行一段全局变量初始化程序INIT的代码。

第二行开始是定义一个求最大值函数max(),其作用是求得a和b中较大者的值。这里的a和b是max()函数的形式参数,属于局部变量。外部变量a和b在函数max()内部不起作用,即形式参数a和b的值不再是3和5,它们的值是通过主调函数中的实际参数传递过来的。

程序的最后四行是main()函数,在main()函数内部定义了一个局部变量a并赋值为8,全局变量a在这里不起作用,而全局变量b在此范围内有效。因此printf()函数中的max(a, b)相当于max(8, 5),故程序的最后执行结果为8。

3.5.2 变量的存储种类

按变量的有效作用范围可以将其划分为局部变量和全局变量;还可以按变量的存储方式为其划分存储种类。在C语言中变量有四种存储种类,即自动变量(auto)、外部变量(extern)、静态变量(static)和寄存器变量(register)。这四种存储种类与全局变量和局部变量之间的关系如图3.2所示。

图3.2 变量的存储种类

1.自动变量(auto)

定义一个变量时,在变量名前面加上存储种类说明符“auto”,即将该变量定义为自动变量。自动变量是C语言中使用最为广泛的一类变量。按照默认规则,在函数体内部或复合语句内部定义的变量,如果省略存储种类说明,该变量即为自动变量。习惯上通常采用默认形式,例如:

{
char x;
int y;
...
}

等价于

{
auto char x;
auto int y;
...
}

自动变量的作用范围在定义它的函数体或复合语句内部,只有在定义它的函数被调用,或是定义它的复合语句被执行时,编译器才为其分配内存空间,开始其生存期。当函数调用结束返回,或复合语句执行结束时,自动变量所占用的内存空间就被释放,变量的值当然也就不复存在,其生存期结束。当函数被再次调用或复合语句被再次执行,编译器又会为它们内部的自动变量重新分配内存空间,但它不会保留上次运行时的值,而必须被重新赋值。因此自动变量始终是相对于函数或复合语句的局部变量。

2.外部变量(extern)

使用存储种类说明符“extern”定义的变量称为外部变量。按照默认规则,凡是在所有函数之前,在函数外部定义的变量都是外部变量,定义时可以不写extern说明符。但是,在一个函数体内说明一个已在该函数体外或别的程序模块文件中定义过的外部变量时,则必须使用extern说明符。一个外部变量被定义之后,它就被分配了固定的内存空间。外部变量的生存期为程序的整个执行时间,即在程序的执行期间外部变量可被随意使用,当一条复合语句执行完毕或是从某一个函数返回时,外部变量的存储空间并不被释放,其值也仍然保留。因此外部变量属于全局变量。

C语言允许将大型程序分解为若干个独立的程序模块文件,各个模块可分别进行编译,然后再将它们连接在一起。在这种情况下,如果某个变量需要在所有程序模块文件中使用,只要在一个程序模块文件中将该变量定义成全局变量,而在其他程序模块文件中用extern说明该变量是已被定义过的外部变量就可以了。

函数是可以相互调用的,因此函数都具有外部存储种类的属性。定义函数时如果冠以关键字extern即将其明确定义为一个外部函数。例如,extern int func2(char a, b)。如果在定义函数时省略关键字extern,则隐含为外部函数。如果要调用一个在本程序模块文件以外的其他模块文件所定义的函数,则必须用关键字extern说明被调用函数是一个外部函数。对于具有外部函数相互调用的多模块程序,利用μVision51集成开发环境很容易完成编译连接。

这个例子中有两个程序模块文件“ex1.c”和“ex2.c”,可以在μVision51环境下将它们分别添加到一个项目文件“ex.prj”中,然后执行Project菜单中的Make:Updat Project选项即可将它们连接在一起,生成OMF51绝对目标文件ex,绝对目标文件可以装入dScope51中进行仿真调试。

例3.9:多模块程序。

(程序模块1  文件名为ex1.c)
#include<stdio.h>
int x = 5;
void main() {
  extern void fun1();             /* 说明函数fun1在其他文件中定义 */
  extern int fun2(int y);         /* 说明函数fun2在其他文件中定义 */
  fun1();  fun1();  fun1();
  printf("\n%d   %d\n",x,fun2(x));
  while(1);
}
(程序模块2  文件名为ex2.c)
#include <stdio.h>
extern int x;                     /* 说明变量x在其他文件中定义 */
void fun1() {
  static int a = 5;
  int b = 5;
  printf("%d  %d  %d | ",a,b,x);
  a -= 2;
  b -= 2;
  x -= 2;
  printf("%d  %d  %d\n",a,b,x);
}
int fun2(int y) {
  return( 35 * x * y );
}

程序的执行结果为:

5  5  5  |  3  3  3
3  5  3  |  1  3  1
1  5  1  |  -1 3  -1
      -1  35

由于C语言不允许在一个函数体内嵌套定义另一个函数,为了能够访问不同文件中各个函数的变量,除了可以采用我们在前面介绍过的参数传递方法之外,还可以采用外部变量的方法。上面的例子就说明了这一点。需要指出的是,尽管使用外部变量在不同函数之间传递数据有时比使用函数的参数更为方便,但是当外部变量较多时,会增加程序调试排错时的困难,使程序不便于维护。另外,不通过参数传递而直接在函数中改变全局变量的值,有时还会发生一些意想不到的副作用。因此一般情况下最好还是使用函数的参数来传递数据。

3.静态变量(static)

使用存储种类说明符“static”定义的变量称为静态变量。在例3.9的模块2程序文件中使用了一个静态变量:static int a=5。由于这个变量是在函数fun1()内部定义的,因此称为内部静态变量或局部静态变量。局部静态变量不像自动变量那样只有当函数调用它时才存在,退出函数后它就消失,局部静态变量始终都是存在的,但只能在定义它的函数内部进行访问,退出函数之后,变量的值仍然保持,但不能进行访问。

还有一种全局静态变量,它是在函数外部被定义的,作用范围从它的定义点开始,一直到程序结束。当一个C语言程序由若干个模块文件所组成时,全局静态变量始终存在,但它只能在被定义的模块文件中访问,其数据值可为该文件内的所有函数共享,退出该文件后,虽然变量的值仍然保持着,但不能被其他模块文件访问。

局部静态变量是一种在两次函数调用之间仍能保持其值的局部变量。有些程序需要在多次调用之间仍然保持变量的值,使用自动变量无法实现这一点,使用全局变量有时又会带来意外的副作用,这时就可采用局部静态变量。

例3.10:局部静态变量的使用——计算并输出1~5的阶乘值。

#include<stdio.h>
int fac(int n) {
  static int f=1;
  f=f*n;
  return(f);
}
main() {
  int i;
  for (i=1; i<=5; i++)
  printf(“%d! = %d\n”, i, fac(i));
  while(1);
}

程序执行结果:

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120

在这个程序中,一共调用了5次计算阶乘的函数fac(i),每次调用后输出一个阶乘值i!,同时保留这个i!值,以便下次再乘(i+1)。由此可见,如果要保留函数上一次调用结束时的值,或是在初始化之后变量只被引用而不改变其值,则这时使用局部静态变量较为方便,以免在每次调用时都要重新进行赋值。但是,使用局部静态变量需要占用较多的内存空间,而且降低了程序的可读性,当调用次数较多时往往弄不清局部静态变量的当前值是什么。因此,建议不要多用局部静态变量。

全局静态变量是一种作用范围受限制的外部变量,它的有效作用范围从其定义点开始直至程序文件的末尾,而且只有在定义它的程序模块文件中才能对它进行访问。全局静态变量与我们在前面介绍过的单纯全局变量是有区别的。全局静态变量有一个特点,就是只有在定义它的程序文件中才可以使用它,其他文件不能改变其内容。

C语言允许进行多模块程序设计,一个较大型的程序可被分成若干个模块,分别由几个人来完成。如果各人在独立设计各自的程序模块时,有些变量可能只希望在自己的程序模块文件中使用,而不希望被别的模块文件引用,对于这种变量就可以定义为全局静态变量。

需要指出的是,全局静态变量和单纯全局变量都是在编译时就已经分配了固定的内存空间的变量,只是它们的作用范围不同而已。

对于函数也可以定义成具有静态存储种类的属性。定义函数时在函数名前面冠以关键字static即将其定义为一个静态函数。例如,static int func1(char x, int y)。使用静态函数可使该函数只局限于其所在的模块文件。由于函数都是外部型的,因此静态外部函数定义就限制了该函数只能在定义它的模块文件中使用,其他模块文件是不能调用它的。换句话说,在其他模块文件中可以定义与静态函数完全同名的另一个函数,分别编译并连接成为一个可执行程序之后,不会由于程序中存在相同的函数名而发生函数调用时的混乱。这一特点在进行模块化程序设计时是十分有用的。

4.寄存器变量(register)

为了提高程序的执行效率,C语言允许将一些使用频率最高的那些变量,定义为能够直接使用硬件寄存器的所谓寄存器变量。定义一个变量时在变量名前面冠以存储种类符号“register”即将该变量定义成为了寄存器变量。寄存器变量可以被认为是自动变量的一种,它的有效作用范围也与自动变量相同。

由于计算机中的寄存器是有限的,不能将所有变量都定义成寄存器变量。通常在程序中定义的寄存器变量时只是给编译器一个建议,该变量是否能真正成为寄存器变量,要由编译器根据实际情况来确定。另一方面,Cx51编译器能够识别程序中使用频率最高的变量,在可能的情况下,即使程序中并未将该变量定义为寄存器变量,编译器也会自动将其作为寄存器变量处理。下面来看一个带有汇编码的程序例子。

例3.11:使用寄存器变量的例子——计算以整数为底的指数的幂。

stmt level   source
    1          #include<stdio.h>
    2          int_power(m, e)
    3          int m;
    4          register int e;
    5          {
    6   1        register int temp;
    7   1        temp=1;
    8   1        for (; e; e--)
    9   1        temp*=m;
    10   1        return(temp);
    11   1       }
    12
    13          main()  {
    14   1        int x, y;
    15   1        printf("please input X  Y\n");
    16   1        scanf("%d  %d", &x, &y);
    17   1        printf("%d to the power of %d = %d",x,y,int_power(x, y));
    18   1       }
    19
ASSEMBLY LISTING OF GENERATED OBJECT CODE
              ;FUNCTION _int_power (BEGIN)
                                            ;SOURCE LINE # 2
0000 8E00       R    MOV    m,R6
0002 8F00       R    MOV    m+01H,R7
;---- Variable 'e' assigned to Register 'R2/R3' ----
0004 AB05            MOV    R3,AR5
0006 AA04            MOV    R2,AR4
                                            ;SOURCE LINE # 3
                                            ;SOURCE LINE # 7
;---- Variable 'temp' assigned to Register 'R6/R7' ----
0008 7F01            MOV    R7,#01H
000A 7E00            MOV    R6,#00H
                                            ;SOURCE LINE # 8
000C        ?C0001:
000C EB              MOV    A,R3
000D 4A              ORL    A,R2
000E 600E            JZ     ?C0002
                                            ;SOURCE LINE # 9
0010 AC00       R     MOV    R4,m
0012 AD00       R     MOV    R5,m+01H
0014120000     E    LCALL   ?C?IMUL
0017 EB               MOV    A,R3
0018 1B               DEC    R3
0019 70F1             JNZ    ?C0001
001B 1A               DEC    R2
001C        ?C0006:
    001C 80EE             SJMP   ?C0001
    001E        ?C0002:
                                                ;SOURCE LINE # 10
                                                ;SOURCE LINE # 11
    001E        ?C0004:
    001E 22               RET
                ;FUNCTION _int_power (END)
                ;FUNCTION main (BEGIN)
                                                ;SOURCE LINE # 13
                                                ;SOURCE LINE # 15
    0000 7BFF            MOV    R3,#0FFH
    0002 7A00       R    MOV    R2,#HIGH ?SC_0
    00047900       R    MOV    R1,#LOW ?SC_0
    0006120000     E    LCALL   _printf
                                                ;SOURCE LINE # 16
    0009750000     E    MOV    ?_scanf?BYTE+03H,#00H
    000C 750000     R    MOV    ?_scanf?BYTE+04H,#HIGH x
    000F 750000     R    MOV    ?_scanf?BYTE+05H,#LOW x
    0012750000     E    MOV    ?_scanf?BYTE+06H,#00H
    0015750000     R    MOV    ?_scanf?BYTE+07H,#HIGH y
    0018750000     R    MOV    ?_scanf?BYTE+08H,#LOW y
    001B 7BFF            MOV    R3,#0FFH
    001D 7A00       R    MOV    R2,#HIGH ?SC_19
    001F 7900       R    MOV    R1,#LOW ?SC_19
    0021120000     E    LCALL   _scanf
                                                ;SOURCE LINE # 17
    0024 AD00       R    MOV    R5,y+01H
    0026 AC00       R    MOV    R4,y
    0028 AF00       R    MOV    R7,x+01H
    002A AE00       R    MOV    R6,x
    002C 120000     R    LCALL   _int_power
    002F 8E00       E    MOV    ?_printf?BYTE+07H,R6
    0031 8F00       E    MOV    ?_printf?BYTE+08H,R7
    0033 7BFF            MOV    R3,#0FFH
    0035 7A00       R    MOV    R2,#HIGH ?SC_26
    00377900       R    MOV    R1,#LOW ?SC_26
    0039850000     E    MOV    ?_printf?BYTE+03H,x
    003C 850000     E    MOV    ?_printf?BYTE+04H,x+01H
    003F 850000     E    MOV    ?_printf?BYTE+05H,y
    0042850000     E    MOV    ?_printf?BYTE+06H,y+01H
    0045020000     E    LJMP   _printf
                ;FUNCTION main (END)

程序执行结果:

    please input X  Y
    5  3  回车
    5 to the power of 3 = 125

在这个程序中定义了一个计算以整数为底的指数幂的函数int_power(),该函数中有两个形式参数int m和register int e。它们都是int类型的变量,但参数e前面带有存储种类说明符register,被特别说明为寄存器变量。另外,在该函数体中还定义了一个int类型的寄存器变量register int temp。从编译得到的汇编码可以看到,形式参数e被分配给了8051单片机的工作寄存器R2和R3,但变量temp则未分配到工作寄存器,而是作为临时工作单元被存放到内存之中。由此可见,尽管可以在程序中定义寄存器变量,但实际上被定义的变量是否真能成为寄存器变量最终是由编译器决定的。

3.5.3 函数的参数和局部变量的存储器模式

Keil Cx51编译器允许采用三种存储器模式:small、compact和large。一个函数的存储器模式确定了函数的参数和局部变量在内存中的地址空间。处于small模式下函数的参数和局部变量位于8051单片机的内部RAM中,处于compact和large模式下函数的参数和局部变量则使用8051单片机的外部RAM。在定义一个函数时可以明确指定该函数的存储器模式,一般形式为:

函数类型  函数名(形式参数表)[存储器模式]

其中,“存储器模式”是Keil Cx51编译器扩展的一个选项。不用该选项时即没有明确指定函数的存储器模式,这时该函数按编译时的默认存储器模式处理。

例3.12:函数的存储器模式。

#pragma large                               /* 默认存储器模式为LARGE */
extern int calc(char i, int b) small;       /* 指定SMALL模式 */
extern int func(int i, float f) large;      /* 指定LARGE模式 */
extern void * tcp(char xdata *xp, int ndx) small; /* 指定SMALL模式 */
int mtest(int i, int y) small               /* 指定SMALL模式 */
    {
    return(i*y+y*i+func(-1, 4.75))'
    }
int large_func(int i, int k)    /* 未指定模式,按默认的LARGE模式处理 */
    {
    return(mtest(i,k)+2);
    }

这个例子程序的第一行用了一个预编译命令“#pragma”,它的意思是告诉Keil Cx51编译器在对程序进行编译时,按该预编译命令后面给出的编译控制指令“large”进行编译,即本例程序编译时的默认存储器模式为large。程序中一共有五个函数:calc()、func()、*tcp()、mtest()和large_func(),其中前面四个函数都在定义时明确指定了其存储器模式,只有最后一个函数未指定。在用Cx51进行编译时,只有最后一个函数按large存储器模式处理,其余四个函数则分别按它们各自指定的存储器模式处理。

这个例子说明,Keil Cx51编译器允许采用所谓存储器的混合模式,即允许在一个程序中某个(或几个)函数使用一种存储器模式,另一个(或几个)函数使用另一种存储器模式。采用存储器混合模式编程,可以充分利用8051系列单片机中有限的存储器空间,同时还可加快程序的执行速度。