官术网_书友最值得收藏!

第3章 函數(shù)

函數(shù)是C語(yǔ)言中的一種基本模塊,實(shí)際上,一個(gè)C語(yǔ)言程序就是由若干個(gè)模塊化的函數(shù)所構(gòu)成的。前面我們已經(jīng)看到,C語(yǔ)言程序總是由主函數(shù)main()開(kāi)始,main()函數(shù)是一個(gè)控制程序流程的特殊函數(shù),它是程序的起點(diǎn)。在進(jìn)行程序設(shè)計(jì)的過(guò)程中,如果所設(shè)計(jì)的程序較大,一般應(yīng)將其分成若干個(gè)子程序模塊,每個(gè)模塊完成一種特定的功能。在C語(yǔ)言中,子程序是用函數(shù)來(lái)實(shí)現(xiàn)的。對(duì)于一些需要經(jīng)常使用的子程序可以設(shè)計(jì)成一個(gè)專門的函數(shù)庫(kù),以供反復(fù)調(diào)用。此外,Keil Cx51編譯器還提供了豐富的運(yùn)行庫(kù)函數(shù),用戶可以根據(jù)需要隨時(shí)調(diào)用。這種模塊化的程序設(shè)計(jì)方法,可以大大提高編程效率和速度。

3.1 函數(shù)的定義

從用戶的角度來(lái)看,有兩種函數(shù):標(biāo)準(zhǔn)庫(kù)函數(shù)和用戶自定義函數(shù)。標(biāo)準(zhǔn)庫(kù)函數(shù)是Keil Cx51編譯器提供的,不需要用戶進(jìn)行定義,可以直接調(diào)用。用戶自定義函數(shù)是用戶根據(jù)自己需要編寫的能實(shí)現(xiàn)特定功能的函數(shù),它必須先進(jìn)行定義之后才能調(diào)用。函數(shù)定義的一般形式為:

函數(shù)類型  函數(shù)名(形式參數(shù)表)
    形式參數(shù)說(shuō)明
    {
      局部變量定義
      函數(shù)體語(yǔ)句
    }

其中,“函數(shù)類型”說(shuō)明了自定義函數(shù)返回值的類型。

“函數(shù)名”是用標(biāo)識(shí)符表示的自定義函數(shù)名字。

“形式參數(shù)表”中列出的是在主調(diào)用函數(shù)與被調(diào)用函數(shù)之間傳遞數(shù)據(jù)的形式參數(shù),形式參數(shù)的類型必須加以說(shuō)明。ANSI C標(biāo)準(zhǔn)允許在形式參數(shù)表中對(duì)形式參數(shù)的類型進(jìn)行說(shuō)明。如果定義的是無(wú)參函數(shù),可以沒(méi)有形式參數(shù)表,但圓括號(hào)不能省略。

“局部變量定義”是對(duì)在函數(shù)內(nèi)部使用的局部變量進(jìn)行定義。

“函數(shù)體語(yǔ)句”是為完成該函數(shù)的特定功能而設(shè)置的各種語(yǔ)句。

如果定義函數(shù)時(shí)只給出一對(duì)花括號(hào){}而不給出其局部變量和函數(shù)體語(yǔ)句,則該函數(shù)為“空函數(shù)”,這種空函數(shù)也是合法的。在進(jìn)行C語(yǔ)言模塊化程序設(shè)計(jì)時(shí),各模塊的功能可通過(guò)函數(shù)來(lái)實(shí)現(xiàn)。開(kāi)始時(shí)只設(shè)計(jì)最基本的模塊,其他作為擴(kuò)充功能在以后需要時(shí)再加上。編寫程序時(shí)可在將來(lái)準(zhǔn)備擴(kuò)充的地方寫上一個(gè)空函數(shù),這樣可使程序的結(jié)構(gòu)清晰,可讀性好,而且易于擴(kuò)充。

例3.1:定義一個(gè)計(jì)算整數(shù)的正整數(shù)次冪的函數(shù)。

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

這里定義了一個(gè)返回值為整型值的函數(shù)power(),它有兩個(gè)形式參數(shù):x,n。形式參數(shù)的作用是接受從主調(diào)用函數(shù)傳遞過(guò)來(lái)的實(shí)際參數(shù)的值。上例中形式參數(shù)x和n被說(shuō)明為int類型。花括號(hào)以內(nèi)的部分是自定義函數(shù)的函數(shù)體。上例中在函數(shù)體內(nèi)定義了兩個(gè)局部變量i和p,它們均為整型數(shù)據(jù)。

需要注意的是,形式參數(shù)的說(shuō)明與函數(shù)體內(nèi)的局部變量定義是完全不同的兩個(gè)部分,前者應(yīng)寫在花括號(hào)的外面,而后者是函數(shù)體的一個(gè)組成部分,必須寫在花括號(hào)的里面。為了不發(fā)生混淆,ANSI C標(biāo)準(zhǔn)允許在形式參數(shù)表中對(duì)形式參數(shù)的類型進(jìn)行說(shuō)明,如上例可寫成:int power(int x, int n)。

在函數(shù)體中可以根據(jù)用戶自己的需要,設(shè)置各種不同的語(yǔ)句。這些語(yǔ)句應(yīng)能完成所需要的功能。上例在函數(shù)體中用一個(gè)for循環(huán)結(jié)構(gòu)完成一個(gè)整數(shù)的正整數(shù)次冪的計(jì)算,計(jì)算結(jié)果賦值給變量p。函數(shù)體中最后一條語(yǔ)句return(p)的作用是將p的值返回到主調(diào)用函數(shù)中去。return語(yǔ)句后面圓括號(hào)中的值稱為函數(shù)的返回值,圓括號(hào)可以省略,即return p和return(p)是等價(jià)的。

由于p是函數(shù)的返回值,因此在函數(shù)體中進(jìn)行變量定義時(shí),應(yīng)將變量p的類型定義得與函數(shù)本身的類型相一致。如果二者類型不一致,則函數(shù)調(diào)用時(shí)的返回值可能發(fā)生錯(cuò)誤。如果函數(shù)體中沒(méi)有return語(yǔ)句,則該函數(shù)由函數(shù)體最后面的右閉花括號(hào)“}”返回。在這種情況下,函數(shù)的返回值是不確定的。

對(duì)于不需要有返回值的函數(shù),可以將該函數(shù)定義為void類型(空類型)。對(duì)于上例,如果定義為:void power(int x, int n),則可將函數(shù)體中的return語(yǔ)句去掉,這樣,編譯器會(huì)保證在函數(shù)調(diào)用結(jié)束時(shí)不使函數(shù)返回任何值。為了使程序減少出錯(cuò),保證函數(shù)的正確調(diào)用,凡是不要求有返回值的函數(shù),都應(yīng)將其定義成void類型。

例3.2:不同函數(shù)的定義方法。

char fun1(x, y)               /* 定義一個(gè)char型函數(shù) */
int x;                        /* 說(shuō)明形式參數(shù)的類型 */
char y;
  {
  char z;                     /* 定義函數(shù)內(nèi)部的局部變量 */
  z=x+y;                      /* 函數(shù)體語(yǔ)句 */
    return(z);                /* 返回函數(shù)的值z(mì),注意變量z與函數(shù)本身
   }                             的類型均為char型 */
  int fun2(float a, float b)  /* 定義一個(gè)int型函數(shù),在形式參數(shù)表中說(shuō)
   {                              明形式參數(shù)的類型 */
    int x;                    /* 定義函數(shù)內(nèi)部的局部變量 */
    x=a-b;                    /* 函數(shù)體語(yǔ)句 */
    return(x);                /* 返回函數(shù)的值x,注意變量x與函數(shù)本身
   }                              的類型均為int型 */
  long fun3()                 /* 定義一個(gè)long型函數(shù),它沒(méi)有形式參數(shù) */
   {
    long x;                   /* 定義函數(shù)內(nèi)部的局部變量 */
    int i, j;
    x=i*j;                    /* 函數(shù)體語(yǔ)句 */
    return(x);                /* 返回函數(shù)的值x,注意變量x與函數(shù)本身
   }                              的類型均為long型 */
  void fun4(char a, char b)   /* 定義一個(gè)無(wú)返回值的void型函數(shù) */
   {
    char x;                   /* 局部變量定義 */
    x=a+b;                    /* 函數(shù)體語(yǔ)句 */
   }                          /* 函數(shù)不需要返回值,省略return語(yǔ)句 */
  void fun5( )                /* 定義一個(gè)空函數(shù) */
   {
   }

3.2 函數(shù)的調(diào)用

3.2.1 函數(shù)的調(diào)用形式

C語(yǔ)言程序中函數(shù)是可以互相調(diào)用的。所謂函數(shù)調(diào)用就是在一個(gè)函數(shù)體中引用另外一個(gè)已經(jīng)定義了的函數(shù),前者稱為主調(diào)用函數(shù),后者稱為被調(diào)用函數(shù)。函數(shù)調(diào)用的一般形式為:

函數(shù)名(實(shí)際參數(shù)表)

其中,“函數(shù)名”指出被調(diào)用的函數(shù)。

“實(shí)際參數(shù)表”中可以包含多個(gè)實(shí)際參數(shù),各個(gè)參數(shù)之間用逗號(hào)隔開(kāi)。實(shí)際參數(shù)的值被傳遞給被調(diào)用函數(shù)中的形式參數(shù)。需要注意的是,函數(shù)調(diào)用中的實(shí)際參數(shù)與函數(shù)定義中的形式參數(shù)必須在個(gè)數(shù)、類型及順序上嚴(yán)格保持一致,以便將實(shí)際參數(shù)的值正確地傳遞給形式參數(shù)。否則在函數(shù)調(diào)用時(shí)會(huì)產(chǎn)生意想不到的結(jié)果。如果調(diào)用的是無(wú)參函數(shù),則可以沒(méi)有實(shí)際參數(shù)表,但圓括號(hào)不能省略。

在C語(yǔ)言中可以采用三種方式完成函數(shù)的調(diào)用。

(1)函數(shù)語(yǔ)句

在主調(diào)函數(shù)中將函數(shù)調(diào)用作為一條語(yǔ)句,例如:

fun1();

這是無(wú)參調(diào)用,它不要求被調(diào)用函數(shù)返回一個(gè)確定的值,只要求它完成一定的操作。

(2)函數(shù)表達(dá)式

在主調(diào)函數(shù)中將函數(shù)調(diào)用作為一個(gè)運(yùn)算對(duì)象直接出現(xiàn)在表達(dá)式中,這種表達(dá)式稱為函數(shù)表達(dá)式。例如:

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

這其實(shí)是一個(gè)賦值語(yǔ)句,它包括兩個(gè)函數(shù)調(diào)用,每個(gè)函數(shù)調(diào)用都有一個(gè)返回值,將兩個(gè)返回值相加的結(jié)果,賦值給變量c。因此這種函數(shù)調(diào)用方式要求被調(diào)函數(shù)返回一個(gè)確定的值。

(3)函數(shù)參數(shù)

在主調(diào)函數(shù)中將函數(shù)調(diào)用作為另一個(gè)函數(shù)調(diào)用的實(shí)際參數(shù)。例如:

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

其中,函數(shù)調(diào)用power(i, j)放在另一個(gè)函數(shù)調(diào)用power(power(i, j), k)的實(shí)際參數(shù)表中,以其返回值作為另一個(gè)函數(shù)調(diào)用的實(shí)際參數(shù)。這種在調(diào)用一個(gè)函數(shù)的過(guò)程中又調(diào)用了另外一個(gè)函數(shù)的方式,稱為嵌套函數(shù)調(diào)用。在輸出一個(gè)函數(shù)的值時(shí)經(jīng)常采用這種方法,例如:

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

其中,函數(shù)調(diào)用power(i,j)是作為printf()函數(shù)的一個(gè)實(shí)際參數(shù)處理的,它也屬于嵌套函數(shù)調(diào)用方式。

3.2.2 對(duì)被調(diào)用函數(shù)的說(shuō)明

與使用變量一樣,在調(diào)用一個(gè)函數(shù)之前(包括標(biāo)準(zhǔn)庫(kù)函數(shù)),必須對(duì)該函數(shù)的類型進(jìn)行說(shuō)明,即“先說(shuō)明,后調(diào)用”。如果調(diào)用的是庫(kù)函數(shù),一般應(yīng)在程序的開(kāi)始處用預(yù)處理命令#include將有關(guān)函數(shù)說(shuō)明的頭文件包含進(jìn)來(lái)。例如前面例子中經(jīng)常出現(xiàn)的預(yù)處理命令#include<stdio.h>,就是將與庫(kù)輸出函數(shù)printf()有關(guān)的頭文件stdio.h包含到程序文件中來(lái)。頭文件“stdio.h”中有關(guān)于庫(kù)輸入輸出函數(shù)的一些說(shuō)明信息,如果不使用這個(gè)包含命令,庫(kù)輸入輸出函數(shù)就無(wú)法被正確地調(diào)用。

如果調(diào)用的是用戶自定義函數(shù),而且該函數(shù)與調(diào)用它的主調(diào)函數(shù)在同一個(gè)文件中,一般應(yīng)該在主調(diào)函數(shù)中對(duì)被調(diào)用函數(shù)的類型進(jìn)行說(shuō)明。函數(shù)說(shuō)明的一般形式為:

類型標(biāo)識(shí)符  被調(diào)用的函數(shù)名(形式參數(shù)表);

其中,“類型標(biāo)識(shí)符”說(shuō)明了函數(shù)返回值的類型。

“形式參數(shù)表”中說(shuō)明各個(gè)形式參數(shù)的類型。

需要注意的是,函數(shù)的說(shuō)明與函數(shù)的定義是完全不同的。函數(shù)的定義是對(duì)函數(shù)功能的確立,它是一個(gè)完整的函數(shù)單位。而函數(shù)的說(shuō)明,只是說(shuō)明了函數(shù)返回值的類型。二者在書寫形式上也不一樣,函數(shù)說(shuō)明結(jié)束時(shí)在圓括號(hào)的后面需要有一個(gè)分號(hào)“;”作為結(jié)束標(biāo)志,而在函數(shù)定義時(shí),被定義函數(shù)名的圓括號(hào)后面沒(méi)有分號(hào)“;”,即函數(shù)定義還未結(jié)束,后面應(yīng)接著書寫形式參數(shù)說(shuō)明和被定義的函數(shù)體部分。

如果被調(diào)函數(shù)是在主調(diào)函數(shù)前面定義的,或者已經(jīng)在程序文件的開(kāi)始處說(shuō)明了所有被調(diào)函數(shù)的類型,在這兩種情況下可以不必再在主調(diào)函數(shù)中對(duì)被調(diào)函數(shù)進(jìn)行說(shuō)明。也可以將所有用戶自定義函數(shù)的說(shuō)明另存為一個(gè)專門的頭文件,需要時(shí)用#include將其包含到主程序中去。

C語(yǔ)言程序中不允許在一個(gè)函數(shù)定義的內(nèi)部包括另一個(gè)函數(shù)的定義,即不允許嵌套函數(shù)定義。但是允許在調(diào)用一個(gè)函數(shù)的過(guò)程中包含另一個(gè)函數(shù)調(diào)用,即嵌套函數(shù)調(diào)用在C語(yǔ)言程序中是允許的。

例3.3:函數(shù)調(diào)用的例子。

#include <stdio.h>
int Max(int x, int y);          /* 對(duì)被調(diào)用函數(shù)進(jìn)行說(shuō)明 */
void main() {                   /* 主函數(shù) */
  int a, b;                     /* 主函數(shù)的局部變量定義 */
  printf("input a and b: \n");
  scanf("%d %d", &a, &b);       /* 調(diào)用庫(kù)輸入函數(shù),從鍵盤獲得a、b的值 */
  printf("Max=%d", Max(a, b));  /* 調(diào)用庫(kù)輸出函數(shù),輸出a、b中較大者的值*/
  while(1);
}
int Max(int x, int y) {         /* 功能函數(shù)定義 */
  int z;                        /* 局部變量定義 */
  if(x>y)                       /* 函數(shù)體語(yǔ)句 */
    z=x;
  else
    z=y;
  return(z);
}

程序執(zhí)行結(jié)果:

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

在這個(gè)例子中,主函數(shù)main()先調(diào)用庫(kù)輸入函數(shù)sacnf(),從鍵盤輸入兩個(gè)值分別賦值給局部變量a和b,然后調(diào)用庫(kù)輸出函數(shù)printf()將a、b中較大者輸出。在調(diào)用庫(kù)輸出函數(shù)printf()的過(guò)程中又調(diào)用了自定義功能函數(shù)Max(),將鍵盤輸入的a、b的值作為實(shí)際參數(shù)傳遞給Max()函數(shù)中的形式參數(shù)x、y。在Max()函數(shù)中對(duì)實(shí)際輸入值進(jìn)行比較以獲得較大者的值。這也是一個(gè)嵌套函數(shù)調(diào)用的例子,圖3.1是例3.3程序中函數(shù)調(diào)用的執(zhí)行過(guò)程。

圖3.1 函數(shù)的嵌套調(diào)用過(guò)程

3.2.3 函數(shù)的參數(shù)與返回值

通常在進(jìn)行函數(shù)調(diào)用時(shí),主調(diào)用函數(shù)與被調(diào)用函數(shù)之間具有數(shù)據(jù)傳遞關(guān)系。這種數(shù)據(jù)傳遞是通過(guò)函數(shù)的參數(shù)實(shí)現(xiàn)的。在定義一個(gè)函數(shù)時(shí),位于函數(shù)名后面圓括號(hào)中的變量名稱為“形式參數(shù)”,而在調(diào)用函數(shù)時(shí),函數(shù)名后面括號(hào)中的表達(dá)式稱為“實(shí)際參數(shù)”。形式參數(shù)在未發(fā)生函數(shù)調(diào)用之前,不占用內(nèi)存單元,因而也是沒(méi)有值的。只有在發(fā)生函數(shù)調(diào)用時(shí)才為它分配內(nèi)存單元,同時(shí)獲得從主調(diào)用函數(shù)中實(shí)際參數(shù)傳遞過(guò)來(lái)的值。函數(shù)調(diào)用結(jié)束后,它所占用的內(nèi)存單元也被釋放。

實(shí)際參數(shù)可以是常數(shù),也可以是變量或表達(dá)式,但要求它們具有確定的值。進(jìn)行函數(shù)調(diào)用時(shí),主調(diào)用函數(shù)將實(shí)際參數(shù)的值傳遞給被調(diào)用函數(shù)中的形式參數(shù)。為了完成正確的參數(shù)傳遞,實(shí)際參數(shù)的類型必須與形式參數(shù)的類型一致,如果兩者不一致,則會(huì)發(fā)生“類型不匹配”錯(cuò)誤。

例3.4:計(jì)算一個(gè)整數(shù)的正整數(shù)次冪。

#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);
 }

程序執(zhí)行結(jié)果:

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

在這個(gè)程序中定義了一個(gè)計(jì)算整數(shù)的正整數(shù)次冪的函數(shù)int power(int x, int n);它有兩個(gè)整型的形式參數(shù)x和n。在程序開(kāi)始時(shí),變量x和n是不占用內(nèi)存單元的,因此也是沒(méi)有值的。在主函數(shù)main()中先從鍵盤輸入兩個(gè)整數(shù)值a和b,然后通過(guò)函數(shù)調(diào)用語(yǔ)句c=power(a, b);將實(shí)際參數(shù)a和b的值傳遞給被調(diào)用函數(shù)power()中的形式參數(shù)。調(diào)用發(fā)生時(shí),形式參數(shù)變量x和n被賦以實(shí)際參數(shù)a和b的值,從而使函數(shù)power()能按實(shí)際參數(shù)的值進(jìn)行計(jì)算。從這個(gè)例子可以看到,形式參數(shù)和實(shí)際參數(shù)可以不同名,但它們的類型必須要一致。

一般情況下,希望通過(guò)函數(shù)調(diào)用使主調(diào)用函數(shù)獲得一個(gè)確定的值,這就是函數(shù)的返回值。例如,上例中的函數(shù)調(diào)用語(yǔ)句c=power(a, b);就是將函數(shù)power()的返回值賦給變量c。函數(shù)的返回值是通過(guò)return語(yǔ)句獲得的,如果希望從被調(diào)用函數(shù)中帶回一個(gè)值到主調(diào)用函數(shù),被調(diào)用函數(shù)中必須包含有return語(yǔ)句。

一個(gè)函數(shù)中可以有一個(gè)以上的return語(yǔ)句,執(zhí)行到哪一個(gè)return語(yǔ)句,哪一個(gè)return語(yǔ)句起作用。return后面可以跟一個(gè)表達(dá)式,例如,return(x>y? x: y);這種寫法只用一條return語(yǔ)句即可同時(shí)完成表達(dá)式的計(jì)算和函數(shù)值的返回。return后面還可以跟另外一個(gè)已定義了的函數(shù)名,例如:return keyval(rdkey);采用這種寫法可實(shí)現(xiàn)函數(shù)的嵌套調(diào)用,即在函數(shù)返回的同時(shí)調(diào)用另一個(gè)函數(shù)。

函數(shù)返回值的類型確定了該函數(shù)的類型,因此在定義一個(gè)函數(shù)時(shí),函數(shù)本身的類型應(yīng)與return語(yǔ)句中變量或表達(dá)式的類型一致。例如,上例中power()函數(shù)被定義為int類型,return語(yǔ)句中的變量p也被定義為int類型。如果函數(shù)類型與return語(yǔ)句中表達(dá)式的值類型不一致,則以函數(shù)的類型為準(zhǔn)。對(duì)于返回的數(shù)值數(shù)據(jù)可以自動(dòng)進(jìn)行類型轉(zhuǎn)換,即函數(shù)的類型決定返回值的類型。如果不需要被調(diào)用函數(shù)返回一個(gè)確定的值,則可以不要return語(yǔ)句,同時(shí)應(yīng)將被調(diào)用函數(shù)定義成void類型。事實(shí)上,main()函數(shù)就是一個(gè)典型的沒(méi)有返回值的函數(shù),因此可以將其寫成void main()的形式。由于void類型的函數(shù)沒(méi)有return語(yǔ)句,因此在一個(gè)void類型函數(shù)的調(diào)用結(jié)束時(shí),將從該函數(shù)的最后一個(gè)花括號(hào)處返回到主調(diào)用函數(shù)。

例3.5:使用void類型函數(shù)的例子。

#include<stdio.h>
void main()  {              /* void類型的主函數(shù) */
    void prn_char(char x);  /* 功能函數(shù)說(shuō)明 */
    prn_char('w');          /* 功能函數(shù)調(diào)用 */
    while(1);
  }
void prn_char(char x)       /* 將功能函數(shù)定義為void類型,無(wú)返回值 */
  {
    printf("%c has ASCII value %bd\n",x,x);
}                           /* void型功能函數(shù)從此處返回主調(diào)用函數(shù) */

程序執(zhí)行結(jié)果:

  w has ASCII value 119

3.2.4 實(shí)際參數(shù)的傳遞方式

在進(jìn)行函數(shù)調(diào)用時(shí),必須用主調(diào)函數(shù)中的實(shí)際參數(shù)來(lái)替換被調(diào)函數(shù)中的形式參數(shù),這就是所謂的參數(shù)傳遞。在C語(yǔ)言中,對(duì)于不同類型的實(shí)際參數(shù),有三種不同的參數(shù)傳遞方式。

(1)基本類型的實(shí)際參數(shù)傳遞

當(dāng)函數(shù)的參數(shù)是基本類型的變量時(shí),主調(diào)函數(shù)將實(shí)際參數(shù)的值傳遞給被調(diào)函數(shù)中的形式參數(shù),這種方式稱為值傳遞。前面講過(guò),函數(shù)中的形式參數(shù)在未發(fā)生數(shù)調(diào)用之前是不占用內(nèi)存單元的,只有在進(jìn)行函數(shù)調(diào)用時(shí)才為其分配臨時(shí)存儲(chǔ)單元。而函數(shù)的實(shí)際參數(shù)是要占用確定的存儲(chǔ)單元的。值傳遞方式是將實(shí)際參數(shù)的值傳遞到為被調(diào)函數(shù)中形式參數(shù)分配的臨時(shí)存儲(chǔ)單元中,函數(shù)調(diào)用結(jié)束后,臨時(shí)存儲(chǔ)單元被釋放,形式參數(shù)的值也就不復(fù)存在,但實(shí)際參數(shù)所占用的存儲(chǔ)單元保持原來(lái)的值不變。這種參數(shù)傳遞方式在執(zhí)行被調(diào)函數(shù)時(shí),如果形式參數(shù)的值發(fā)生變化,可以不必?fù)?dān)心主調(diào)函數(shù)中實(shí)際參數(shù)的值會(huì)受到影響。因此值傳遞是一種單向傳遞。

(2)數(shù)組類型的實(shí)際參數(shù)傳遞

當(dāng)函數(shù)的參數(shù)是數(shù)組類型的變量時(shí),主調(diào)函數(shù)將實(shí)際參數(shù)數(shù)組的起始地址傳遞到被調(diào)函數(shù)中形式參數(shù)的臨時(shí)存儲(chǔ)單元,這種方式稱為地址傳遞。地址傳遞方式在執(zhí)行被調(diào)函數(shù)時(shí),形式參數(shù)通過(guò)實(shí)際參數(shù)傳來(lái)的地址,直接到主調(diào)函數(shù)中去存取相應(yīng)的數(shù)組元素,故形式參數(shù)的變化會(huì)改變實(shí)際參數(shù)的值。因此地址傳遞是一種雙向傳遞。

(3)指針類型的實(shí)際參數(shù)傳遞

當(dāng)函數(shù)的參數(shù)是指針類型的變量時(shí),主調(diào)函數(shù)將實(shí)際參數(shù)的地址傳遞給被調(diào)函數(shù)中形式參數(shù)的臨時(shí)存儲(chǔ)單元,因此也屬于地址傳遞。在執(zhí)行被調(diào)函數(shù)時(shí),也是直接到主調(diào)函數(shù)中去訪問(wèn)實(shí)際參數(shù)變量,在這種情況下,形式參數(shù)的變化會(huì)改變實(shí)際參數(shù)的值。

前面介紹的一些函數(shù)調(diào)用中所涉及的都是基本類型的實(shí)際參數(shù)傳遞,這種參數(shù)傳遞方式比較容易理解和應(yīng)用。關(guān)于數(shù)組類型和指針類型實(shí)際參數(shù)的傳遞較為復(fù)雜,將在第4章中詳細(xì)介紹。

3.3 函數(shù)的遞歸調(diào)用與再入函數(shù)

如果在調(diào)用一個(gè)函數(shù)的過(guò)程中又間接或直接地調(diào)用該函數(shù)本身,稱為函數(shù)的遞歸調(diào)用。例如,計(jì)算階乘函數(shù)f (n)=n!,可以先計(jì)算f (n-1)=(n-1)!,而計(jì)算f (n-1)時(shí)又可以先計(jì)算f (n-2)=(n-2)!,這就是遞歸算法。再入函數(shù)是一種可以在函數(shù)體內(nèi)直接或間接調(diào)用其自身的一種函數(shù),顯然再入函數(shù)是可以進(jìn)行遞歸調(diào)用的。

Keil Cx51編譯器采用一個(gè)擴(kuò)展關(guān)鍵字reentrant,作為定義函數(shù)時(shí)的選項(xiàng),需要將一個(gè)函數(shù)定義為再入函數(shù)時(shí),只要在函數(shù)名后面加上關(guān)鍵字reentrant即可:

函數(shù)類型  函數(shù)名(形式參數(shù)表)[reentrant]

再入函數(shù)可被遞歸調(diào)用,無(wú)論何時(shí),包括中斷服務(wù)函數(shù)在內(nèi)的任何函數(shù)都可調(diào)用再入函數(shù)。與非再入函數(shù)的參數(shù)傳遞和局部變量的存儲(chǔ)分配方法不同,Cx51編譯器為再入函數(shù)生成一個(gè)模擬棧,通過(guò)這個(gè)模擬棧來(lái)完成參數(shù)傳遞和存放局部變量。模擬棧所在的存儲(chǔ)器空間根據(jù)再入函數(shù)存儲(chǔ)器模式的不同,可以是DATA、PDATA或XDATA存儲(chǔ)器空間。當(dāng)程序中包含有多種存儲(chǔ)器模式的再入函數(shù)時(shí),Cx51編譯器為每種模式單獨(dú)建立一個(gè)模擬棧并獨(dú)立管理各自的棧指針。對(duì)于再入函數(shù)有如下規(guī)定。

① 再入函數(shù)不能傳送bit類型的參數(shù),也不能定義一個(gè)局部位變量,再入函數(shù)不能包括位操作以及8051系列單片機(jī)的可位尋址區(qū)。

② 與PL/M51兼容的函數(shù)不能具有reentrant屬性,也不能調(diào)用再入函數(shù)。

③ 在編譯時(shí)存儲(chǔ)器模式的基礎(chǔ)上為再入函數(shù)在內(nèi)部或外部存儲(chǔ)器中建立一個(gè)模擬堆棧區(qū),稱為再入棧。在Small模式下再入棧位于IDATA區(qū),在compact模式下再入棧位于PDATA區(qū),在Large模式下再入棧位于XDATA區(qū)。再入函數(shù)的局部變量及參數(shù)都被放在再入棧中,從而使再入函數(shù)可以進(jìn)行遞歸調(diào)用。而非再入函數(shù)的局部變量被放在再入棧之外的暫存區(qū)內(nèi),如果對(duì)非再入函數(shù)進(jìn)行遞歸調(diào)用,則上次調(diào)用時(shí)使用的局部變量數(shù)據(jù)將被覆蓋。

④ 在同一個(gè)程序中可以定義和使用不同存儲(chǔ)器模式的再入函數(shù),任意模式的再入函數(shù)不能調(diào)用不同模式的再入函數(shù),但可任意調(diào)用非再入函數(shù)。

⑤ 在參數(shù)的傳遞上,實(shí)際參數(shù)可以傳遞給間接調(diào)用的再入函數(shù)。無(wú)再入屬性的間接調(diào)用函數(shù)不能包含調(diào)用參數(shù),但是可以使用定義的全局變量來(lái)進(jìn)行參數(shù)傳遞。

例3.6:利用函數(shù)的遞歸調(diào)用計(jì)算整數(shù)的階乘。

#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);
}

程序執(zhí)行結(jié)果:

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

在這個(gè)程序中定義了一個(gè)再入函數(shù)fac(n),它是用來(lái)計(jì)算階乘n!的函數(shù)。在fac()的函數(shù)體中又調(diào)用了fac()函數(shù)本身,因此這是一種函數(shù)的遞歸調(diào)用。再入函數(shù)在進(jìn)行遞歸調(diào)用時(shí),新的局部變量和參數(shù)在再入棧中重新分配存儲(chǔ)單元,并以新的變量重新開(kāi)始執(zhí)行。每次遞歸調(diào)用返回時(shí),前面壓入的局部變量和參數(shù)會(huì)從再入棧中彈出,并恢復(fù)到上次調(diào)用自身的地方繼續(xù)執(zhí)行。如果是非再入函數(shù)進(jìn)行遞歸調(diào)用,每次調(diào)用函數(shù)自身時(shí),上次調(diào)用時(shí)使用的局部變量數(shù)據(jù)將被覆蓋,因而在遞歸調(diào)用結(jié)束時(shí)不能得到正確的結(jié)果。對(duì)于例3.6的程序,如果將函數(shù)fac(n)定義成非再入函數(shù),則程序的運(yùn)行結(jié)果為0,顯然這是不正確的。

采用函數(shù)的遞歸調(diào)用可使程序的結(jié)構(gòu)緊湊,但是遞歸調(diào)用要求采用再入函數(shù),以便利用再入棧來(lái)保存有關(guān)的局部變量數(shù)據(jù),從而要占據(jù)較大的內(nèi)存空間。另外遞歸調(diào)用時(shí)對(duì)函數(shù)的處理速度也比較慢,因此一般情況下應(yīng)盡量避免采用函數(shù)遞歸調(diào)用,定義函數(shù)時(shí)應(yīng)盡量避免使用再入屬性。

3.4 中斷服務(wù)函數(shù)與寄存器組定義

Keil Cx51編譯器支持在C語(yǔ)言源程序中直接編寫8051單片機(jī)的中斷服務(wù)函數(shù)程序,從而減輕了采用匯編語(yǔ)言編寫中斷服務(wù)程序的煩瑣程度。為了在C語(yǔ)言源程序中直接編寫中斷服務(wù)函數(shù)的需要,Keil Cx51編譯器對(duì)函數(shù)的定義進(jìn)行了擴(kuò)展,增加了一個(gè)擴(kuò)展關(guān)鍵字interrupt,它是函數(shù)定義時(shí)的一個(gè)選項(xiàng),加上這個(gè)選項(xiàng)即可以將一個(gè)函數(shù)定義成中斷服務(wù)函數(shù)。定義中斷服務(wù)函數(shù)的一般形式為:

函數(shù)類型  函數(shù)名(形式參數(shù)表)[interrupt n][using n]

關(guān)鍵字interrupt后面的n是中斷號(hào),n的取值范圍為0~31。編譯器從8n+3處產(chǎn)生中斷向量,具體的中斷號(hào)n和中斷向量取決于8051系列單片機(jī)芯片型號(hào),常用中斷源和中斷向量如表3-1所示。

表3-1 常用中斷號(hào)與中斷向量

8051系列單片機(jī)可以在片內(nèi)RAM中使用4個(gè)不同的工作寄存器組,每個(gè)寄存器組中包含8個(gè)工作寄存器(R0~R7)。Keil Cx51編譯器擴(kuò)展了一個(gè)關(guān)鍵字using,專門用來(lái)選擇8051單片機(jī)中不同的工作寄存器組。using后面的n是一個(gè)0~3的常整數(shù),分別選中4個(gè)不同的工作寄存器組。在定義一個(gè)函數(shù)時(shí)using是一個(gè)選項(xiàng),如果不用該選項(xiàng),則由編譯器自動(dòng)選擇一個(gè)寄存器組作絕對(duì)寄存器組訪問(wèn)。需要注意的是,關(guān)鍵字using和interrupt的后面都不允許跟帶運(yùn)算符的表達(dá)式。

關(guān)鍵字using對(duì)函數(shù)目標(biāo)代碼的影響如下:在函數(shù)的入口處將當(dāng)前工作寄存器組保護(hù)到堆棧中;指定的工作寄存器內(nèi)容不會(huì)改變;函數(shù)退出之前將被保護(hù)的工作寄存器組從堆棧中恢復(fù)。

使用關(guān)鍵字using在函數(shù)中確定一個(gè)工作寄存器組時(shí)必須十分小心,要保證任何寄存器組的切換都只在仔細(xì)控制的區(qū)域內(nèi)發(fā)生,如果不做到這一點(diǎn)將產(chǎn)生不正確的函數(shù)結(jié)果。另外還要注意,帶using屬性的函數(shù)原則上不能返回bit類型的值。并且關(guān)鍵字using不允許用于外部函數(shù)。

關(guān)鍵字interrupt也不允許用于外部函數(shù),它對(duì)中斷函數(shù)目標(biāo)代碼的影響如下:在進(jìn)入中斷函數(shù)時(shí),特殊功能寄存器ACC、B、DPH、DPL、PSW將被保存入棧;如果不使用關(guān)鍵字using進(jìn)行工作寄存器組切換,則將中斷函數(shù)中所用到的全部工作寄存器都入棧保存;函數(shù)退出之前所有的寄存器內(nèi)容出棧恢復(fù);中斷函數(shù)由8051單片機(jī)指令RETI結(jié)束。

下面給出一個(gè)帶有寄存器組切換的中斷函數(shù)定義的例子,該例中還給出了C51編譯器所生成的8051單片機(jī)的指令代碼。

例3.7:帶有寄存器組切換的中斷函數(shù)定義。

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單片機(jī)中斷函數(shù)時(shí)應(yīng)遵循以下規(guī)則。

① 中斷函數(shù)不能進(jìn)行參數(shù)傳遞,如果中斷函數(shù)中包含任何參數(shù)聲明都將導(dǎo)致編譯出錯(cuò)。

② 中斷函數(shù)沒(méi)有返回值,如果企圖定義一個(gè)返回值將得到不正確的結(jié)果。因此建議在定義中斷函數(shù)時(shí)將其定義為void類型,以明確說(shuō)明沒(méi)有返回值。

③ 在任何情況下都不能直接調(diào)用中斷函數(shù),否則會(huì)產(chǎn)生編譯錯(cuò)誤。因?yàn)橹袛嗪瘮?shù)的退出是由8051單片機(jī)指令RETI完成的,RETI指令影響8051單片機(jī)的硬件中斷系統(tǒng)。如果在沒(méi)有實(shí)際中斷請(qǐng)求的情況下直接調(diào)用中斷函數(shù),RETI指令的操作結(jié)果會(huì)產(chǎn)生一個(gè)致命的錯(cuò)誤。

④ 如果在中斷函數(shù)中調(diào)用了其他函數(shù),則被調(diào)用函數(shù)所使用的寄存器組必須與中斷函數(shù)相同。用戶必須保證按要求使用相同的寄存器組,否則會(huì)產(chǎn)生不正確的結(jié)果,這一點(diǎn)必須引起足夠的注意。如果定義中斷函數(shù)時(shí)沒(méi)有使用using選項(xiàng),則由編譯器自動(dòng)選擇一個(gè)寄存器組作絕對(duì)寄存器組訪問(wèn)。另外,由于中斷的產(chǎn)生不可預(yù)測(cè),中斷函數(shù)對(duì)其他函數(shù)的調(diào)用可能形成遞規(guī)調(diào)用,需要時(shí)可將被中斷函數(shù)所調(diào)用的其他函數(shù)定義成再入函數(shù)。

⑤ Keil Cx51編譯器從絕對(duì)地址8n+3處產(chǎn)生一個(gè)中斷向量,其中n為中斷號(hào)。該向量包含一個(gè)到中斷函數(shù)入口地址的絕對(duì)跳傳。在對(duì)源程序編譯時(shí),可用編譯控制命令NOINTVECTOR抑制中斷向量的產(chǎn)生,從而使用戶有能力從獨(dú)立的匯編程序模塊中提供中斷向量。

3.5 函數(shù)變量的存儲(chǔ)方式

3.5.1 局部變量與全局變量

按照變量的有效作用范圍可劃分為局部變量和全局變量。局部變量是在一個(gè)函數(shù)內(nèi)部定義的變量,該變量只在定義它的那個(gè)函數(shù)范圍以內(nèi)有效。在此函數(shù)之外局部變量即失去意義,因而也就不能使用這些變量了。不同的函數(shù)可以使用相同的局部變量名,由于它們的作用范圍不同,不會(huì)相互干擾。函數(shù)的形式參數(shù)也屬于局部變量。在一個(gè)函數(shù)內(nèi)部的復(fù)合語(yǔ)句中也可以定義局部變量,該局部變量只在該復(fù)合語(yǔ)句中有效。

全局變量是在函數(shù)外部定義的變量,又稱為外部變量。全局變量可以為多個(gè)函數(shù)共同使用,其有效作用范圍是從它定義的位置開(kāi)始到整個(gè)程序文件結(jié)束。如果全局變量定義在一個(gè)程序文件的開(kāi)始處,則在整個(gè)程序文件范圍內(nèi)都可以使用它。如果一個(gè)全局變量不是在程序文件的開(kāi)始處定義的,但又希望在它的定義點(diǎn)之前的函數(shù)中引用該變量,這時(shí)應(yīng)在引用該變量的函數(shù)中用關(guān)鍵字extern將其說(shuō)明為“外部變量”。另外,如果在一個(gè)程序模塊文件中引用另一個(gè)程序模塊文件中定義的變量時(shí),也必須用extern進(jìn)行說(shuō)明。

外部變量說(shuō)明與外部變量定義是不相同的。外部變量定義只能有一次,定義的位置在所有函數(shù)之外。而同一個(gè)程序文件中的外部變量說(shuō)明可以有多次,說(shuō)明的位置在需要引用該變量的函數(shù)之內(nèi)。外部變量說(shuō)明的作用只是聲明該變量是一個(gè)已經(jīng)在外部定義過(guò)了的變量而已。

如果在同一個(gè)程序文件中,全局變量與局部變量同名,則在局部變量的有效作用范圍之內(nèi),全局變量不起作用。換句話說(shuō),局部變量的優(yōu)先級(jí)比全局變量高。在編寫C語(yǔ)言程序時(shí),不是特別必要的地方一般不要使用全局變量,而應(yīng)當(dāng)盡可能地使用局部變量。這是因?yàn)榫植孔兞恐辉谑褂盟鼤r(shí),才為其分配內(nèi)存單元,而全局變量在整個(gè)程序的執(zhí)行過(guò)程中都要占用內(nèi)存單元。另外,如果使用全局變量過(guò)多,在各個(gè)函數(shù)執(zhí)行時(shí)都有可能改變?nèi)肿兞康闹担谷藗冸y以清楚地判斷出在各個(gè)程序執(zhí)行點(diǎn)處全局變量的值,這樣會(huì)使降低程序的通用性和可讀性。

還有一點(diǎn)需要說(shuō)明,如果程序中的全局變量在定義時(shí)賦給了初值,按ANSI C標(biāo)準(zhǔn)規(guī)定,在程序進(jìn)入main()函數(shù)之前必須先對(duì)該全局變量進(jìn)行初始化。這是由連接定位器BL51對(duì)目標(biāo)程序連接定位時(shí),在最后生成的目標(biāo)代碼中自動(dòng)加入一段運(yùn)行庫(kù)“INIT.OBJ”來(lái)實(shí)現(xiàn)的。由于增加了這么一段代碼,程序的長(zhǎng)度會(huì)增加,運(yùn)行速度也會(huì)受到影響。因此要限制使用全局變量。

下面通過(guò)一個(gè)例子來(lái)說(shuō)明局部變量與全局變量的區(qū)別。

例3.8:局部變量與全局變量的區(qū)別。

#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);
}

程序執(zhí)行結(jié)果:

8

這個(gè)程序中故意使用了相同的變量名a和b,請(qǐng)讀者仔細(xì)區(qū)別它們的作用范圍。程序的第一行將a和b定義成全局變量并且賦了初值,由于具有初值的全局變量需要先行初始化,因此讀者如果用dScope51對(duì)這個(gè)例子程序進(jìn)行調(diào)試,可以看到程序在進(jìn)入main()函數(shù)之前,除了要執(zhí)行一段啟動(dòng)程序STARTUP的代碼之外,還需要執(zhí)行一段全局變量初始化程序INIT的代碼。

第二行開(kāi)始是定義一個(gè)求最大值函數(shù)max(),其作用是求得a和b中較大者的值。這里的a和b是max()函數(shù)的形式參數(shù),屬于局部變量。外部變量a和b在函數(shù)max()內(nèi)部不起作用,即形式參數(shù)a和b的值不再是3和5,它們的值是通過(guò)主調(diào)函數(shù)中的實(shí)際參數(shù)傳遞過(guò)來(lái)的。

程序的最后四行是main()函數(shù),在main()函數(shù)內(nèi)部定義了一個(gè)局部變量a并賦值為8,全局變量a在這里不起作用,而全局變量b在此范圍內(nèi)有效。因此printf()函數(shù)中的max(a, b)相當(dāng)于max(8, 5),故程序的最后執(zhí)行結(jié)果為8。

3.5.2 變量的存儲(chǔ)種類

按變量的有效作用范圍可以將其劃分為局部變量和全局變量;還可以按變量的存儲(chǔ)方式為其劃分存儲(chǔ)種類。在C語(yǔ)言中變量有四種存儲(chǔ)種類,即自動(dòng)變量(auto)、外部變量(extern)、靜態(tài)變量(static)和寄存器變量(register)。這四種存儲(chǔ)種類與全局變量和局部變量之間的關(guān)系如圖3.2所示。

圖3.2 變量的存儲(chǔ)種類

1.自動(dòng)變量(auto)

定義一個(gè)變量時(shí),在變量名前面加上存儲(chǔ)種類說(shuō)明符“auto”,即將該變量定義為自動(dòng)變量。自動(dòng)變量是C語(yǔ)言中使用最為廣泛的一類變量。按照默認(rèn)規(guī)則,在函數(shù)體內(nèi)部或復(fù)合語(yǔ)句內(nèi)部定義的變量,如果省略存儲(chǔ)種類說(shuō)明,該變量即為自動(dòng)變量。習(xí)慣上通常采用默認(rèn)形式,例如:

{
char x;
int y;
...
}

等價(jià)于

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

自動(dòng)變量的作用范圍在定義它的函數(shù)體或復(fù)合語(yǔ)句內(nèi)部,只有在定義它的函數(shù)被調(diào)用,或是定義它的復(fù)合語(yǔ)句被執(zhí)行時(shí),編譯器才為其分配內(nèi)存空間,開(kāi)始其生存期。當(dāng)函數(shù)調(diào)用結(jié)束返回,或復(fù)合語(yǔ)句執(zhí)行結(jié)束時(shí),自動(dòng)變量所占用的內(nèi)存空間就被釋放,變量的值當(dāng)然也就不復(fù)存在,其生存期結(jié)束。當(dāng)函數(shù)被再次調(diào)用或復(fù)合語(yǔ)句被再次執(zhí)行,編譯器又會(huì)為它們內(nèi)部的自動(dòng)變量重新分配內(nèi)存空間,但它不會(huì)保留上次運(yùn)行時(shí)的值,而必須被重新賦值。因此自動(dòng)變量始終是相對(duì)于函數(shù)或復(fù)合語(yǔ)句的局部變量。

2.外部變量(extern)

使用存儲(chǔ)種類說(shuō)明符“extern”定義的變量稱為外部變量。按照默認(rèn)規(guī)則,凡是在所有函數(shù)之前,在函數(shù)外部定義的變量都是外部變量,定義時(shí)可以不寫extern說(shuō)明符。但是,在一個(gè)函數(shù)體內(nèi)說(shuō)明一個(gè)已在該函數(shù)體外或別的程序模塊文件中定義過(guò)的外部變量時(shí),則必須使用extern說(shuō)明符。一個(gè)外部變量被定義之后,它就被分配了固定的內(nèi)存空間。外部變量的生存期為程序的整個(gè)執(zhí)行時(shí)間,即在程序的執(zhí)行期間外部變量可被隨意使用,當(dāng)一條復(fù)合語(yǔ)句執(zhí)行完畢或是從某一個(gè)函數(shù)返回時(shí),外部變量的存儲(chǔ)空間并不被釋放,其值也仍然保留。因此外部變量屬于全局變量。

C語(yǔ)言允許將大型程序分解為若干個(gè)獨(dú)立的程序模塊文件,各個(gè)模塊可分別進(jìn)行編譯,然后再將它們連接在一起。在這種情況下,如果某個(gè)變量需要在所有程序模塊文件中使用,只要在一個(gè)程序模塊文件中將該變量定義成全局變量,而在其他程序模塊文件中用extern說(shuō)明該變量是已被定義過(guò)的外部變量就可以了。

函數(shù)是可以相互調(diào)用的,因此函數(shù)都具有外部存儲(chǔ)種類的屬性。定義函數(shù)時(shí)如果冠以關(guān)鍵字extern即將其明確定義為一個(gè)外部函數(shù)。例如,extern int func2(char a, b)。如果在定義函數(shù)時(shí)省略關(guān)鍵字extern,則隱含為外部函數(shù)。如果要調(diào)用一個(gè)在本程序模塊文件以外的其他模塊文件所定義的函數(shù),則必須用關(guān)鍵字extern說(shuō)明被調(diào)用函數(shù)是一個(gè)外部函數(shù)。對(duì)于具有外部函數(shù)相互調(diào)用的多模塊程序,利用μVision51集成開(kāi)發(fā)環(huán)境很容易完成編譯連接。

這個(gè)例子中有兩個(gè)程序模塊文件“ex1.c”和“ex2.c”,可以在μVision51環(huán)境下將它們分別添加到一個(gè)項(xiàng)目文件“ex.prj”中,然后執(zhí)行Project菜單中的Make:Updat Project選項(xiàng)即可將它們連接在一起,生成OMF51絕對(duì)目標(biāo)文件ex,絕對(duì)目標(biāo)文件可以裝入dScope51中進(jìn)行仿真調(diào)試。

例3.9:多模塊程序。

(程序模塊1  文件名為ex1.c)
#include<stdio.h>
int x = 5;
void main() {
  extern void fun1();             /* 說(shuō)明函數(shù)fun1在其他文件中定義 */
  extern int fun2(int y);         /* 說(shuō)明函數(shù)fun2在其他文件中定義 */
  fun1();  fun1();  fun1();
  printf("\n%d   %d\n",x,fun2(x));
  while(1);
}
(程序模塊2  文件名為ex2.c)
#include <stdio.h>
extern int x;                     /* 說(shuō)明變量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 );
}

程序的執(zhí)行結(jié)果為:

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

由于C語(yǔ)言不允許在一個(gè)函數(shù)體內(nèi)嵌套定義另一個(gè)函數(shù),為了能夠訪問(wèn)不同文件中各個(gè)函數(shù)的變量,除了可以采用我們?cè)谇懊娼榻B過(guò)的參數(shù)傳遞方法之外,還可以采用外部變量的方法。上面的例子就說(shuō)明了這一點(diǎn)。需要指出的是,盡管使用外部變量在不同函數(shù)之間傳遞數(shù)據(jù)有時(shí)比使用函數(shù)的參數(shù)更為方便,但是當(dāng)外部變量較多時(shí),會(huì)增加程序調(diào)試排錯(cuò)時(shí)的困難,使程序不便于維護(hù)。另外,不通過(guò)參數(shù)傳遞而直接在函數(shù)中改變?nèi)肿兞康闹担袝r(shí)還會(huì)發(fā)生一些意想不到的副作用。因此一般情況下最好還是使用函數(shù)的參數(shù)來(lái)傳遞數(shù)據(jù)。

3.靜態(tài)變量(static)

使用存儲(chǔ)種類說(shuō)明符“static”定義的變量稱為靜態(tài)變量。在例3.9的模塊2程序文件中使用了一個(gè)靜態(tài)變量:static int a=5。由于這個(gè)變量是在函數(shù)fun1()內(nèi)部定義的,因此稱為內(nèi)部靜態(tài)變量或局部靜態(tài)變量。局部靜態(tài)變量不像自動(dòng)變量那樣只有當(dāng)函數(shù)調(diào)用它時(shí)才存在,退出函數(shù)后它就消失,局部靜態(tài)變量始終都是存在的,但只能在定義它的函數(shù)內(nèi)部進(jìn)行訪問(wèn),退出函數(shù)之后,變量的值仍然保持,但不能進(jìn)行訪問(wèn)。

還有一種全局靜態(tài)變量,它是在函數(shù)外部被定義的,作用范圍從它的定義點(diǎn)開(kāi)始,一直到程序結(jié)束。當(dāng)一個(gè)C語(yǔ)言程序由若干個(gè)模塊文件所組成時(shí),全局靜態(tài)變量始終存在,但它只能在被定義的模塊文件中訪問(wèn),其數(shù)據(jù)值可為該文件內(nèi)的所有函數(shù)共享,退出該文件后,雖然變量的值仍然保持著,但不能被其他模塊文件訪問(wèn)。

局部靜態(tài)變量是一種在兩次函數(shù)調(diào)用之間仍能保持其值的局部變量。有些程序需要在多次調(diào)用之間仍然保持變量的值,使用自動(dòng)變量無(wú)法實(shí)現(xiàn)這一點(diǎn),使用全局變量有時(shí)又會(huì)帶來(lái)意外的副作用,這時(shí)就可采用局部靜態(tài)變量。

例3.10:局部靜態(tài)變量的使用——計(jì)算并輸出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);
}

程序執(zhí)行結(jié)果:

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

在這個(gè)程序中,一共調(diào)用了5次計(jì)算階乘的函數(shù)fac(i),每次調(diào)用后輸出一個(gè)階乘值i!,同時(shí)保留這個(gè)i!值,以便下次再乘(i+1)。由此可見(jiàn),如果要保留函數(shù)上一次調(diào)用結(jié)束時(shí)的值,或是在初始化之后變量只被引用而不改變其值,則這時(shí)使用局部靜態(tài)變量較為方便,以免在每次調(diào)用時(shí)都要重新進(jìn)行賦值。但是,使用局部靜態(tài)變量需要占用較多的內(nèi)存空間,而且降低了程序的可讀性,當(dāng)調(diào)用次數(shù)較多時(shí)往往弄不清局部靜態(tài)變量的當(dāng)前值是什么。因此,建議不要多用局部靜態(tài)變量。

全局靜態(tài)變量是一種作用范圍受限制的外部變量,它的有效作用范圍從其定義點(diǎn)開(kāi)始直至程序文件的末尾,而且只有在定義它的程序模塊文件中才能對(duì)它進(jìn)行訪問(wèn)。全局靜態(tài)變量與我們?cè)谇懊娼榻B過(guò)的單純?nèi)肿兞渴怯袇^(qū)別的。全局靜態(tài)變量有一個(gè)特點(diǎn),就是只有在定義它的程序文件中才可以使用它,其他文件不能改變其內(nèi)容。

C語(yǔ)言允許進(jìn)行多模塊程序設(shè)計(jì),一個(gè)較大型的程序可被分成若干個(gè)模塊,分別由幾個(gè)人來(lái)完成。如果各人在獨(dú)立設(shè)計(jì)各自的程序模塊時(shí),有些變量可能只希望在自己的程序模塊文件中使用,而不希望被別的模塊文件引用,對(duì)于這種變量就可以定義為全局靜態(tài)變量。

需要指出的是,全局靜態(tài)變量和單純?nèi)肿兞慷际窃诰幾g時(shí)就已經(jīng)分配了固定的內(nèi)存空間的變量,只是它們的作用范圍不同而已。

對(duì)于函數(shù)也可以定義成具有靜態(tài)存儲(chǔ)種類的屬性。定義函數(shù)時(shí)在函數(shù)名前面冠以關(guān)鍵字static即將其定義為一個(gè)靜態(tài)函數(shù)。例如,static int func1(char x, int y)。使用靜態(tài)函數(shù)可使該函數(shù)只局限于其所在的模塊文件。由于函數(shù)都是外部型的,因此靜態(tài)外部函數(shù)定義就限制了該函數(shù)只能在定義它的模塊文件中使用,其他模塊文件是不能調(diào)用它的。換句話說(shuō),在其他模塊文件中可以定義與靜態(tài)函數(shù)完全同名的另一個(gè)函數(shù),分別編譯并連接成為一個(gè)可執(zhí)行程序之后,不會(huì)由于程序中存在相同的函數(shù)名而發(fā)生函數(shù)調(diào)用時(shí)的混亂。這一特點(diǎn)在進(jìn)行模塊化程序設(shè)計(jì)時(shí)是十分有用的。

4.寄存器變量(register)

為了提高程序的執(zhí)行效率,C語(yǔ)言允許將一些使用頻率最高的那些變量,定義為能夠直接使用硬件寄存器的所謂寄存器變量。定義一個(gè)變量時(shí)在變量名前面冠以存儲(chǔ)種類符號(hào)“register”即將該變量定義成為了寄存器變量。寄存器變量可以被認(rèn)為是自動(dòng)變量的一種,它的有效作用范圍也與自動(dòng)變量相同。

由于計(jì)算機(jī)中的寄存器是有限的,不能將所有變量都定義成寄存器變量。通常在程序中定義的寄存器變量時(shí)只是給編譯器一個(gè)建議,該變量是否能真正成為寄存器變量,要由編譯器根據(jù)實(shí)際情況來(lái)確定。另一方面,Cx51編譯器能夠識(shí)別程序中使用頻率最高的變量,在可能的情況下,即使程序中并未將該變量定義為寄存器變量,編譯器也會(huì)自動(dòng)將其作為寄存器變量處理。下面來(lái)看一個(gè)帶有匯編碼的程序例子。

例3.11:使用寄存器變量的例子——計(jì)算以整數(shù)為底的指數(shù)的冪。

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)

程序執(zhí)行結(jié)果:

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

在這個(gè)程序中定義了一個(gè)計(jì)算以整數(shù)為底的指數(shù)冪的函數(shù)int_power(),該函數(shù)中有兩個(gè)形式參數(shù)int m和register int e。它們都是int類型的變量,但參數(shù)e前面帶有存儲(chǔ)種類說(shuō)明符register,被特別說(shuō)明為寄存器變量。另外,在該函數(shù)體中還定義了一個(gè)int類型的寄存器變量register int temp。從編譯得到的匯編碼可以看到,形式參數(shù)e被分配給了8051單片機(jī)的工作寄存器R2和R3,但變量temp則未分配到工作寄存器,而是作為臨時(shí)工作單元被存放到內(nèi)存之中。由此可見(jiàn),盡管可以在程序中定義寄存器變量,但實(shí)際上被定義的變量是否真能成為寄存器變量最終是由編譯器決定的。

3.5.3 函數(shù)的參數(shù)和局部變量的存儲(chǔ)器模式

Keil Cx51編譯器允許采用三種存儲(chǔ)器模式:small、compact和large。一個(gè)函數(shù)的存儲(chǔ)器模式確定了函數(shù)的參數(shù)和局部變量在內(nèi)存中的地址空間。處于small模式下函數(shù)的參數(shù)和局部變量位于8051單片機(jī)的內(nèi)部RAM中,處于compact和large模式下函數(shù)的參數(shù)和局部變量則使用8051單片機(jī)的外部RAM。在定義一個(gè)函數(shù)時(shí)可以明確指定該函數(shù)的存儲(chǔ)器模式,一般形式為:

函數(shù)類型  函數(shù)名(形式參數(shù)表)[存儲(chǔ)器模式]

其中,“存儲(chǔ)器模式”是Keil Cx51編譯器擴(kuò)展的一個(gè)選項(xiàng)。不用該選項(xiàng)時(shí)即沒(méi)有明確指定函數(shù)的存儲(chǔ)器模式,這時(shí)該函數(shù)按編譯時(shí)的默認(rèn)存儲(chǔ)器模式處理。

例3.12:函數(shù)的存儲(chǔ)器模式。

#pragma large                               /* 默認(rèn)存儲(chǔ)器模式為L(zhǎng)ARGE */
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)    /* 未指定模式,按默認(rèn)的LARGE模式處理 */
    {
    return(mtest(i,k)+2);
    }

這個(gè)例子程序的第一行用了一個(gè)預(yù)編譯命令“#pragma”,它的意思是告訴Keil Cx51編譯器在對(duì)程序進(jìn)行編譯時(shí),按該預(yù)編譯命令后面給出的編譯控制指令“l(fā)arge”進(jìn)行編譯,即本例程序編譯時(shí)的默認(rèn)存儲(chǔ)器模式為large。程序中一共有五個(gè)函數(shù):calc()、func()、*tcp()、mtest()和large_func(),其中前面四個(gè)函數(shù)都在定義時(shí)明確指定了其存儲(chǔ)器模式,只有最后一個(gè)函數(shù)未指定。在用Cx51進(jìn)行編譯時(shí),只有最后一個(gè)函數(shù)按large存儲(chǔ)器模式處理,其余四個(gè)函數(shù)則分別按它們各自指定的存儲(chǔ)器模式處理。

這個(gè)例子說(shuō)明,Keil Cx51編譯器允許采用所謂存儲(chǔ)器的混合模式,即允許在一個(gè)程序中某個(gè)(或幾個(gè))函數(shù)使用一種存儲(chǔ)器模式,另一個(gè)(或幾個(gè))函數(shù)使用另一種存儲(chǔ)器模式。采用存儲(chǔ)器混合模式編程,可以充分利用8051系列單片機(jī)中有限的存儲(chǔ)器空間,同時(shí)還可加快程序的執(zhí)行速度。

主站蜘蛛池模板: 子洲县| 建德市| 正宁县| 华容县| 西峡县| 介休市| 波密县| 包头市| 泰和县| 安龙县| 定州市| 北宁市| 陆河县| 城固县| 将乐县| 松溪县| 渑池县| 望城县| 白山市| 沙坪坝区| 日喀则市| 平湖市| 田林县| 墨江| 永嘉县| 七台河市| 镇平县| 四子王旗| 渭源县| 定襄县| 江孜县| 和田市| 沅陵县| 祁东县| 鄂尔多斯市| 公安县| 青川县| 根河市| 尼玛县| 新泰市| 永靖县|