【分享】C語言初學者入門講座 第十講 函數(2)@ngchk.com
【分享】C語言初學者入門講座 第十講 函數(2)
C語言初學者入門講座 第十講 函數(2)
一、函數的參數
前面已經介紹過,函數的參數分爲形參和實參兩種。 在本小節中,進一步介紹形參、實參的特點和兩者的關係。 形參出現在函數定義中,在整個函數體內都可以使用, 離開該函數則不能使用。實參出現在主調函數中,進入被調函數後,實參變數也不能使用。 形參和實參的功能是作數據傳送。發生函數調用時, 主調函數把實參的值傳送給被調函數的形參從而實現主調函數向被調函數的資料傳送。
函數的形參和實參具有以下特點:
1.形參變數只有在被調用時才分配記憶體單元,在調用結束時, 即刻釋放所分配的記憶體單元。因此,形參只有在函數內部有效。 函數調用結束返回主調函數後則不能再使用該形參變數。
2.實參可以是常量、變數、運算式、函數等, 無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值, 以便把這些值傳送給形參。 因此應預先用賦值,輸入等辦法使實參獲得確定值。
3.實參和形參在數量上,類型上,順序上應嚴格一致, 否則會發生“類型不匹配”的錯誤。
4.函數調用中發生的資料傳送是單向的。 即只能把實參的值傳送給形參,而不能把形參的值反向地傳送給實參。 因此在函數調用過程中,形參的值發生改變,而實參中的值不會變化。例5.3可以說明這個問題。
void main()
{
int n;
printf("input number\n");
scanf("%d",&n);
s(n);
printf("n=%d\n",n);
}
int s(int n)
{
int i;
for(i=n-1;i>=1;i--)
n=n+i;
printf("n=%d\n",n);
}
本程式中定義了一個函數s,該函數的功能是求∑ni=1i 的值。在主函數中輸入n值,並作爲實參,在調用時傳送給s 函數的形參量n( 注意,本例的形參變數和實參變數的識別字都爲n, 但這是兩個不同的量,各自的作用域不同)。 在主函數中用printf 語句輸出一次n值,這個n值是實參n的值。在函數s中也用printf 語句輸出了一次n值,這個n值是形參最後取得的n值0。從運行情況看,輸入n值爲100。即實參n的值爲100。把此值傳給函數s時,形參 n 的初值也爲100,在執行函數過程中,形參n的值變爲5050。 返回主函數之後,輸出實參n的值仍爲100。可見實參的值不隨形參的變化而變化。
二、函數的值
函數的值是指函數被調用之後, 執行函數體中的程式段所取得的並返回給主調函數的值。如調用正弦函數取得正弦值,調用例5.1的max函數取得的最大數等。對函數的值(或稱函數返回值)有以下一些說明:
1. 函數的值只能通過return語句返回主調函數。return 語句的一般形式爲:
return 運算式;
或者爲:
return (運算式);
該語句的功能是計算運算式的值,並返回給主調函數。 在函數中允許有多個return語句,但每次調用只能有一個return 語句被執行, 因此只能返回一個函數值。
2. 函數值的類型和函數定義中函數的類型應保持一致。 如果兩者不一致,則以函數類型爲准,自動進行類型轉換。
3. 如函數值爲整型,在函數定義時可以省去類型說明。
4. 不返回函數值的函數,可以明確定義爲“空類型”, 類型說明符爲“void”。如例5.3中函數s並不向主函數返函數值,因此可定義爲:
void s(int n)
{ ……
}
一旦函數被定義爲空類型後, 就不能在主調函數中使用被調函數的函數值了。例如,在定義s爲空類型後,在主函數中寫下述語句 sum=s(n); 就是錯誤的。爲了使程式有良好的可讀性並減少出錯, 凡不要求返回值的函數都應定義爲空類型。函數說明在主調函數中調用某函數之前應對該被調函數進行說明, 這與使用變數之前要先進行變數說明是一樣的。 在主調函數中對被調函數作說明的目的是使編譯系統知道被調函數返回值的類型, 以便在主調函數中按此種類型對返回值作相應的處理。 對被調函數的說明也有兩種格式,一種爲傳統格式,其一般格式爲: 類型說明符 被調函數名(); 這種格式只給出函數返回值的類型,被調函數名及一個空括弧。
這種格式由於在括弧中沒有任何參數資訊, 因此不便於編譯系統進行錯誤檢查,易於發生錯誤。另一種爲現代格式,其一般形式爲:
類型說明符 被調函數名(類型 形參,類型 形參…);
或爲:
類型說明符 被調函數名(類型,類型…);
現代格式的括弧內給出了形參的類型和形參名, 或只給出形參類型。這便於編譯系統進行檢錯,以防止可能出現的錯誤。例5.1 main函數中對max函數的說明若
用傳統格式可寫爲:
int max();
用現代格式可寫爲:
int max(int a,int b);
或寫爲:
int max(int,int);
C語言中又規定在以下幾種情況時可以省去主調函數中對被調函數的函數說明。
1. 如果被調函數的返回值是整型或字元型時, 可以不對被調函數作說明,而直接調用。這時系統將自動對被調函數返回值按整型處理。例5.3的主函數中未對函數s作說明而直接調用即屬此種情形。
2. 當被調函數的函數定義出現在主調函數之前時, 在主調函數中也可以不對被調函數再作說明而直接調用。例如例5.1中, 函數max的定義放在main 函數之前,因此可在main函數中省去對 max函數的函數說明int max(int a,int b)。
3. 如在所有函數定義之前, 在函數外預先說明了各個函數的類型,則在以後的各主調函數中,可不再對被調函數作說明。例如:
char str(int a);
float f(float b);
main()
{
……
}
char str(int a)
{
……
}
float f(float b)
{
……
}
其中第一,二行對str函數和f函數預先作了說明。 因此在以後各函數中無須對str和f函數再作說明就可直接調用。
4. 對庫函數的調用不需要再作說明, 但必須把該函數的頭文件用include命令包含在原始檔案前部。陣列作爲函數參數陣列可以作爲函數的參數使用,進行資料傳送。 陣列用作函數參數有兩種形式,一種是把陣列元素(下標變數)作爲實參使用; 另一種是把陣列名作爲函數的形參和實參使用。一、陣列元素作函數實參數組元素就是下標變數,它與普通變數並無區別。 因此它作爲函數實參使用與普通變數是完全相同的,在發生函數調用時, 把作爲實參的陣列元素的值傳送給形參,實現單向的值傳送。例5.4說明了這種情況。[例5.4]判別一個整數陣列中各元素的值,若大於0 則輸出該值,若小於等於0則輸出0值。編程如下:
void nzp(int v)
{
if(v>0)
printf("%d ",v);
else
printf("%d ",0);
}
main()
{
int a[5],i;
printf("input 5 numbers\n");
for(i=0;i<5;i++)
{
scanf("%d",&a);
nzp(a);
}
}void nzp(int v)
{ ……
}
main()
{
int a[5],i;
printf("input 5 numbers\n");
for(i=0;i<5;i++)
{ scanf("%d",&a);
nzp(a);
}
}
本程式中首先定義一個無返回值函數nzp,並說明其形參v 爲整型變數。在函數體中根據v值輸出相應的結果。在main函數中用一個for 語句輸入陣列各元素, 每輸入一個就以該元素作實參調用一次nzp函數,即把a的值傳送給形參v,供nzp函數使用。
二、陣列名作爲函數參數
用陣列名稱作函數參數與用陣列元素作實參有幾點不同:
1. 用陣列元素作實參時,只要陣列類型和函數的形參變數的類型一致,那麽作爲下標變數的陣列元素的類型也和函數形參變數的類型是一致的。因此, 並不要求函數的形參也是下標變數。 換句話說,對陣列元素的處理是按普通變數對待的。用陣列名稱作函數參數時, 則要求形參和相對應的實參都必須是類型相同的陣列,都必須有明確的陣列說明。當形參和實參二者不一致時,即會發生錯誤。
2. 在普通變數或下標變數作函數參數時,形參變數和實參變數是由編譯系統分配的兩個不同的記憶體單元。在函數調用時發生的值傳送是把實參變數的值賦予形參變數。在用陣列名稱作函數參數時,不是進行值的傳送,即不是把實參數組的每一個元素的值都賦予形參數組的各個元素。因爲實際上形參數組並不存在,編譯系統不爲形參數組分配記憶體。那麽,資料的傳送是如何實現的呢? 在第四章中我們曾介紹過,陣列名稱就是陣列的首位址。因此在陣列名稱作函數參數時所進行的傳送只是位址的傳送, 也就是說把實參數組的首地址賦予形參陣列名稱。形參陣列名稱取得該首位址之後,也就等於有了實在的陣列。實際上是形參數組和實參數組爲同一陣列,共同擁有一段記憶體空間。圖5.1說明了這種情形。圖中設a爲實參數組,類型爲整型。a佔有以2000 爲首位址的一塊記憶體區。b爲形參陣列名稱。當發生函數調用時,進行位址傳送, 把實參數 組a的首地址傳送給形參陣列名稱b,於是b也取得該地址2000。 於是a,b兩陣列共同佔有以2000 爲首位址的一段連續記憶體單元。從圖中還可以看出a和b下標相同的元素實際上也占相同的兩個記憶體單元(整型陣列每個元素占二位元組)。例如a[0]和b[0]都佔用2000和2001單元,當然a[0]等於b[0]。類推則有a等於b。
[例5.5]陣列a中存放了一個學生5門課程的成績,求平均成績。float aver(float a[5])
{
int i;
float av,s=a[0];
for(i=1;i<5;i++)
s=s+a;
av=s/5;
return av;
}
void main()
{
float sco[5],av;
int i;
printf("\ninput 5 scores:\n");
for(i=0;i<5;i++)
scanf("%f",&sco);
av=aver(sco);
printf("average score is %5.2f",av);
}
float aver(float a[5])
{ ……
}
void main()
{
……
for(i=0;i<5;i++)
scanf("%f",&sco);
av=aver(sco);
……
}
本程式首先定義了一個實型函數aver,有一個形參爲實型陣列a,長度爲5。在函數aver中,把各元素值相加求出平均值,返回給主函數。主函數main 中首先完成陣列sco的輸入,然後以sco作爲實參調用aver函數,函數返回值送av,最後輸出av值。 從運行情況可以看出,程式實現了所要求的功能\n
3. 前面已經討論過,在變數作函數參數時,所進行的值傳送是單向的。即只能從實參傳向形參,不能從形參傳回實參。形參的初值和實參相同, 而形參的值發生改變後,實參並不變化, 兩者的終值是不同的。例5.3證實了這個結論。 而當用陣列名稱作函數參數時,情況則不同。 由於實際上形參和實參爲同一陣列, 因此當形參數組發生變化時,實參數組也隨之變化。 當然這種情況不能理解爲發生了“雙向”的值傳遞。但從實際情況來看,調用函數之後實參數組的值將由於形參數組值的變化而變化。爲了說明這種情況,把例5.4改爲例5.6的形式。[例5.6]題目同5.4例。改用陣列名稱作函數參數。
void nzp(int a[5])
{
int i;
printf("\nvalues of array a are:\n");
for(i=0;i<5;i++)
{
if(a<0) a=0;
printf("%d ",a);
}
}
main()
{
int b[5],i;
printf("\ninput 5 numbers:\n");
for(i=0;i<5;i++)
scanf("%d",&b);
printf("initial values of array b are:\n");
for(i=0;i<5;i++)
printf("%d ",b);
nzp(b);
printf("\nlast values of array b are:\n");
for(i=0;i<5;i++)
printf("%d ",b);
}
void nzp(int a[5])
{ ……
}
main()
{
int b[5],i;
……
nzp(b);
……
}
本程式中函數nzp的形參爲整數組a,長度爲 5。 主函數中實參數組b也爲整型,長度也爲5。在主函數中首先輸入陣列b的值,然後輸出陣列b的初始值。 然後以陣列名稱b爲實參調用nzp函數。在nzp中,按要求把負值單元清0,並輸出形參數組a的值。 返回主函數之後,再次輸出陣列b的值。從運行結果可以看出,陣列b 的初值和終值是不同的,陣列b 的終值和陣列a是相同的。這說明實參形參爲同一陣列,它們的值同時得以改變。 用陣列名作爲函數參數時還應注意以下幾點:
a. 形參數組和實參數組的類型必須一致,否則將引起錯誤。
b. 形參數組和實參數組的長度可以不相同,因爲在調用時,只傳送首地址而不檢查形參數組的長度。當形參數組的長度與實參數組不一致時,雖不至於出現語法錯誤(編譯能通過),但程式執行結果將與實際不符,這是應予以注意的。如把例5.6修改如下:
void nzp(int a[8])
{
int i;
printf("\nvalues of array aare:\n");
for(i=0;i<8;i++)
{
if(a<0)a=0;
printf("%d",a);
}
}
main()
{
int b[5],i;
printf("\ninput 5 numbers:\n");
for(i=0;i<5;i++)
scanf("%d",&b);
printf("initial values of array b are:\n");
for(i=0;i<5;i++)
printf("%d",b);
nzp(b);
printf("\nlast values of array b are:\n");
for(i=0;i<5;i++)
printf("%d",b);
}
本程式與例5.6程式比,nzp函數的形參數組長度改爲8,函數體中,for語句的迴圈條件也改爲i<8。因此,形參數組 a和實參數組b的長度不一致。編譯能夠通過,但從結果看,陣列a的元素a[5],a[6],a[7]顯然是無意義的。c. 在函數形參表中,允許不給出形參數組的長度,或用一個變數來表示陣列元素的個數。\n
例如:可以寫爲:
void nzp(int a[])
或寫爲
void nzp(int a[],int n)
其中形參數組a沒有給出長度,而由n值動態地表示陣列的長度。n的值由主調函數的實參進行傳送。
由此,例5.6又可改爲例5.7的形式。
[例5.7]
void nzp(int a[],int n)
{
int i;
printf("\nvalues of array a are:\n");
for(i=0;i<n;i++)
{
if(a<0) a=0;
printf("%d ",a);
}
}
main()
{
int b[5],i;
printf("\ninput 5 numbers:\n");
for(i=0;i<5;i++)
scanf("%d",&b);
printf("initial values of array b are:\n");
for(i=0;i<5;i++)
printf("%d ",b);
nzp(b,5);
printf("\nlast values of array b are:\n");
for(i=0;i<5;i++)
printf("%d ",b);
}
void nzp(int a[],int n)
{ ……
}
main()
{
……
nzp(b,5);
……
}
本程式nzp函數形參數組a沒有給出長度,由n 動態確定該長度。在main函數中,函數調用語句爲nzp(b,5),其中實參5將賦予形參n作爲形參數組的長度。
d. 多維陣列也可以作爲函數的參數。 在函數定義時對形參數組可以指定每一維的長度,也可省去第一維的長度。因此,以下寫法都是合法的。
int MA(int a[3][10])
或
int MA(int a[][10])
函數的嵌套調用
C語言中不允許作嵌套的函數定義。因此各函數之間是平行的,不存在上一級函數和下一級函數的問題。 但是C語言允許在一個函數的定義中出現對另一個函數的調用。 這樣就出現了函數的嵌套調用。即在被調函數中又調用其他函數。 這與其他語言的副程式嵌套的情形是類似的。其關係可表示如圖5.2。
圖5.2表示了兩層嵌套的情形。其執行過程是:執行main函數中調用a函數的語句時,即轉去執行a函數,在a函數中調用b 函數時,又轉去執行b函數,b函數執行完畢返回a函數的中斷點繼續執行,a 函數執行完畢返回main函數的中斷點繼續執行。
[例5.8]計算s=2
|