- Keil Cx51 V7.0單片機高級語言編程與μVision2應用實踐
- 徐愛鈞 彭秀華編著
- 1754字
- 2018-12-29 19:18:32
第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系列單片機中有限的存儲器空間,同時還可加快程序的執行速度。
- INSTANT Mock Testing with PowerMock
- 小程序實戰視頻課:微信小程序開發全案精講
- SoapUI Cookbook
- 零基礎學Scratch少兒編程:小學課本中的Scratch創意編程
- C/C++算法從菜鳥到達人
- Mastering RStudio:Develop,Communicate,and Collaborate with R
- SQL經典實例(第2版)
- Visual Basic程序設計實驗指導(第二版)
- AIRIOT物聯網平臺開發框架應用與實戰
- INSTANT Yii 1.1 Application Development Starter
- Mastering jQuery Mobile
- SignalR:Real-time Application Development(Second Edition)
- 監控的藝術:云原生時代的監控框架
- Mastering Concurrency in Python
- JSP應用與開發技術(第3版)