C程式設計|用函式實現模組化程式設計詳解(一)
目錄
一、為什麼要用函式
- 使用函式可使程式清晰、精煉、簡單、靈活。
- 函式就是功能。每一個函式用來實現一個特定的功能。函式名應反映其代表的功能。
- 在設計較大程式時,往往把它分為若干個程式模組,每一個模組包括一個或多個函式,每個函式實現一個特定的功能。
- 一個C程式可由一個主函式和若干個其他函式構成。由主函式呼叫其他函式,其他函式也可以互相呼叫。
說明:
(1) 一個C程式由一個或多個程式模組組成,每一個程式模組作為一個源程式檔案。較大的程式,可分別放在若干個原始檔中。這樣便於分別編寫和編譯,提高除錯效率。
一個源程式檔案可以為多個C程式共用。(2) 一個源程式檔案由一個或多個函式以及其他有關內容(如指令、資料宣告與定義等)組成。一個源程式檔案是一個編譯單位,在程式編譯時是以源程式檔案為單位進行編譯的,而不是以函式為單位進行編譯的。
(3) C程式的執行是從main函式開始的,如果在main函式中呼叫其他函式,在呼叫後流程返回到main函式,在main函式中結束整個程式的執行。
(4) 所有函式都是平行的,即在定義函式時是分別進行的,是互相獨立的。一個函式並不從屬於另一個函式,即函式不能巢狀定義。函式間可以互相呼叫,但不能呼叫main函式。main函式是被作業系統呼叫的。
(5) 從使用者使用的角度看,函式有兩種。
- 庫函式,它是由系統提供的,使用者不必自己定義,可直接使用它們。應該說明,不同的C語言編譯系統提供的庫函式的數量和功能會有一些不同,當然許多基本的函式是共同的。
- 使用者自己定義的函式。它是用以解決使用者專門需要的函式。
(6) 從函式的形式看,函式分兩類。
- 無參函式。在呼叫無參函式時,主調函式不向被呼叫函式傳遞資料。
- 有參函式。在呼叫函式時,主調函式在呼叫被呼叫函式時,通過引數向被呼叫函式傳遞資料。
例:想輸出以下的結果,用函式呼叫實現。
#include <stdio.h> int main() { void print_star(); //宣告print_star函式 void print_message(); //宣告print_message函式 print_star(); //呼叫print_star函式 print_message(); //呼叫print_message函式 print_star(); //呼叫print_star函式 return 0; } void print_star() //定義print_star函式 { printf("******************\n"); //輸出一行*號 } void print_message() //定義print_message函式 { printf("How do you do!\n"); //輸出一行文字資訊 }
程式分析:
print_star和print_message都是使用者定義的函式名,分別用來輸出一排“*”號和一行文字資訊。在定義這兩個函式時指定函式的型別為void,意為函式無型別,即無函式值,也就是說,執行這兩個函式後不會把任何值帶回main函式。
在程式中,定義print_star函式和print_message函式的位置是在main函式的後面,在這種情況下,應當在main函式之前或main函式中的開頭部分,對以上兩個函式進行“宣告”。函式宣告的作用是把有關函式的資訊(函式名、函式型別、函式引數的個數與型別)通知編譯系統,以便在編譯系統對程式進行編譯時,在進行到main函式呼叫print_star()和 print_message()時知道它們是函式而不是變數或其他物件。此外,還對呼叫函式的正確性進行檢查(如型別、函式名、引數個數、引數型別等是否正確)。
二、定義函式
1、為什麼要定義函式
C語言要求,在程式中用到的所有函式,必須“先定義,後使用”。
定義函式應包括以下幾個內容:
(1) 指定函式的名字,以便以後按名呼叫。
(2) 指定函式的型別,即函式返回值的型別。
(3) 指定函式的引數的名字和型別,以便在呼叫函式時向它們傳遞資料。對無參函式不需要這項。
(4) 指定函式應當完成什麼操作,也就是函式是做什麼的,即函式的功能。這是最重要的,是在函式體中解決的。
2、定義函式的方法
①定義無參函式
- 函式名後面括號內的void表示“空”,即函式沒有引數。
- 函式體包括宣告部分和語句部分。
- 在定義函式時要用“型別識別符號”(即型別名)指定函式值的型別,即指定函式帶回來的值的型別。
②定義有參函式
形式引數表列的格式 :資料型別 變數1,資料型別 變數2,…,資料型別 變數n
int max(int x,int y)
{ int z; //宣告部分
z=x>y?x:y; //執行語句部分
return(z);
}
③定義空函式
函式體為空,什麼也不做。
三、呼叫函式
1、函式呼叫的形式
一般形式為:函式名(實參表列)
print_star(); //呼叫無參函式
c=max(a,b); //呼叫有參函式
按函式呼叫在程式中出現的形式和位置來分,可以有以下三種函式呼叫方式:
1. 函式呼叫語句
把函式呼叫單獨作為一個語句。如printf_star(); 這時不要求函式帶回值,只要求函式完成一定的操作。
2. 函式表示式
函式呼叫出現在另一個表示式中,如c=max(a,b); 這時要求函式帶回一個確定的值以參加表示式的運算。
3. 函式引數
函式呼叫作為另一個函式呼叫時的實參。如m=max(a,max(b,c));,又如:printf (″%d″, max (a,b));
2、函式呼叫時的資料傳遞
① 形式引數和實際引數
在呼叫有參函式時,主調函式和被呼叫函式之間有資料傳遞關係。
- 在定義函式時函式名後面括號中的變數名稱為“形式引數”(簡稱“形參”)或“虛擬引數”。
- 在主調函式中呼叫一個函式時,函式名後面括號中的引數稱為“實際引數”(簡稱“實參”)。
- 實際引數可以是常量、變數或表示式,但要求它們有確定的值。
- 實參與形參的型別應相同或賦值相容。賦值相容是指實參與形參型別不同時能按不同型別數值的賦值規則進行轉換。
② 實參和形參間的資料傳遞
例:輸入兩個整數,要求輸出其中值較大者。要求用函式來找到大數。
#include <stdio.h>
int main()
{ int max(int x,int y); //對max函式的宣告
int a,b,c;
printf("please enter two integer numbers:"); //提示輸入資料
scanf("%d,%d",&a,&b); //輸入兩個整數
c=max(a,b); //呼叫max函式,有兩個實參。大數賦給變數c
printf("max is %d\n",c); //輸出大數c
return 0; }
int max(int x,int y) //定義max函式,有兩個引數
{
int z; //定義臨時變數z
z=x>y?x:y; //把x和y中大者賦給z
return(z); //把z作為max函式的值帶回main函式
}
執行結果:
程式分析:
定義函式,名為max,函式型別為int。指定兩個形參x和y,形參的型別為int。
主函式中包含了一個函式呼叫max(a,b)。max後面括號內的a和b是實參。a和b是在main函式中定義的變數,x和y是函式max的形式引數。通過函式呼叫,在兩個函式之間發生資料傳遞,實參a和b的值傳遞給形參x和y,在max函式中把x和y中的大者賦給變數z,z的值作為函式值返回main函式,賦給變數c。
在呼叫函式過程中發生的實參與形參間的資料傳遞稱為“虛實結合”。
3、函式呼叫的過程
(1) 在定義函式中指定的形參,在未出現函式呼叫時,它們並不佔記憶體中的儲存單元。在發生函式呼叫時,函式的形參才被臨時分配記憶體單元。
(2) 將實參的值傳遞給對應形參。
(3) 在執行函式期間,由於形參已經有值,就可以利用形參進行有關的運算。
(4) 通過return語句將函式值帶回到主調函式。應當注意返回值的型別與函式型別一致。如果函式不需要返回值,則不需要return語句。這時函式的型別應定義為void型別。
(5) 呼叫結束,形參單元被釋放。注意: 實參單元仍保留並維持原值,沒有改變。如果在執行一個被呼叫函式時,形參的值發生改變,不會改變主調函式的實參的值。因為實參與形參是兩個不同的儲存單元。
注意:實參向形參的資料傳遞是“值傳遞”,單向傳遞,只能由實參傳給形參,而不能由形參傳給實參。實參和形參在記憶體中佔有不同的儲存單元,實參無法得到形參的值。
4、函式的返回值
通常,希望通過函式呼叫使主調函式能得到一個確定的值,這就是函式值(函式的返回值)。
int max (float x,float y) //函式值為整型
char letter (char c1,char c2) //函式值為字元型
double min (int x,int y) //函式值為雙精度型
(1) 函式的返回值是通過函式中的return語句獲得的。一個函式中可以有一個以上的return語句,執行到哪一個return語句,哪一個return語句就起作用。return語句後面的括號可以不要,如“return z;”與“return(z);”等價。return後面的值可以是一個表示式。
(2) 函式值的型別。函式值的型別在定義函式時指定。
(3) 在定義函式時指定的函式型別一般應該和return語句中的表示式型別一致。 如果函式值的型別和return語句中表達式的值不一致,則以函式型別為準。對數值型資料,可以自動進行型別轉換。即函式型別決定返回值的型別。
(4) 對於不帶回值的函式,應當用定義函式為“void型別”(或稱“空型別”)。這樣,系統就保證不使函式帶回任何值,即禁止在呼叫函式中使用被呼叫函式的返回值。此時在函式體中不得出現return語句。
例:輸入兩個整數,要求輸出其中值較大者。要求用函式來找到大數。(在max函式中定義的變數z改為float型。函式返回值的型別與指定的函式型別不同,分析其處理方法。)
#include <stdio.h>
int main()
{ int max(float x,float y);
float a,b;
int c;
printf("please enter two float numbers:"); //提示輸入資料
scanf("%f,%f",&a,&b);
c=max(a,b);
printf("max is %d\n",c);
return 0; }
int max(float x,float y)
{
float z; //z為實型變數
z=x>y?x:y;
return(z);
}
執行結果:
程式分析:
max函式的形參是float型,實參也是float型,在main函式中輸入給a和b的值是1.5和2.6。在呼叫max(a,b)時,把a和b的值1.5和2.6傳遞給形參x和y。執行函式max中的條件表示式“z=x>y?x:y”,使得變數z得到的值為2.6。
現在出現了矛盾: 函式定義為int型,而return語句中的z為float型,要把z的值作為函式的返回值,二者不一致。 怎樣處理呢?
按賦值規則處理,先將z的值轉換為int型,得到2,它就是函式得到的返回值。 如果將main函式中的c改為float型,用%f格式符輸出,輸出2.000000。因為呼叫max函式得到的是int型,函式值為整數2。
四、對被呼叫函式的宣告和函式原型
在一個函式中呼叫另一個函式(即被呼叫函式)需要具備如下條件:
(1) 首先被呼叫的函式必須是已經定義的函式(是庫函式或使用者自己定義的函式)。
(2) 如果使用庫函式,應該在本檔案開頭用#include指令將呼叫有關庫函式時所需用到的資訊“包含”到本檔案中來。
(3) 如果使用使用者自己定義的函式,而該函式的位置在呼叫它的函式(即主調函式)的後面(在同一個檔案中),應該在主調函式中對被呼叫的函式作宣告(declaration)。宣告的作用是把函式名、函式引數的個數和引數型別等資訊通知編譯系統,以便在遇到函式呼叫時,編譯系統能正確識別函式並檢查呼叫是否合法。
函式宣告的一般形式有兩種:
- 函式型別 函式名(引數型別1 引數名1, 引數型別2 引數名2, …, 引數型別n 引數名n);
- 函式型別 函式名(引數型別1, 引數型別2, …, 引數型別n);
float add(float x, float y);
float add(float, float); //不寫引數名,只寫引數型別
float add(float a, float b); //引數名不用x,y,而用a,b。合法
char letter(char, char); //所有函式之前,且在函式外部進行函式宣告
float f(float,float);
int i (float,float);
int main() //在main函式中要呼叫letter,f和i函式
{
… //不必再對所呼叫的這3個函式進行宣告
}
char letter(char c1,char c2) //定義letter函式
{
…
}
float f(float x,float y) //定義f函式
{
…
}
int i(float j,float k) //定義i函式
{
…
}
在函式宣告中的形參名可以省寫,而只寫形參的型別。
如果已在檔案的開頭(在所有函式之前),已對本檔案中所呼叫的函式進行了宣告,則在各函式中不必對其所呼叫的函式再作宣告。
注意:對函式的“定義”和“宣告”不是同一回事。函式的定義是指對函式功能的確立,包括指定函式名、函式值型別、形參及其型別以及函式體等,它是一個完整的、獨立的函式單位。而函式的宣告的作用則是把函式的名字、函式型別以及形參的型別、個數和順序通知編譯系統,以便在呼叫該函式時系統按此進行對照檢查,它不包含函式體。
例:輸入兩個實數,用一個函式求出它們之和。
#include <stdio.h>
int main()
{ float add(float x, float y); //對add函式作宣告
float a,b,c;
printf("Please enter a and b:"); //提示輸入
scanf("%f,%f",&a,&b); //輸入兩個實數
c=add(a,b); //呼叫add函式
printf("sum is %f\n",c); //輸出兩數之和
return 0;
}
float add(float x,float y) //定義add函式
{ float z;
z=x+y;
return(z); //把變數z的值作為函式值返回
}
執行結果:
程式分析:函式的宣告和函式定義中的第1行(函式首部)基本上是相同的,只差一個分號(函式宣告比函式定義中的首行多一個分號)。 函式的首行(即函式首部)稱為函式原型(function prototype)。 因為在函式的首部包含了檢查呼叫函式是否合法的基本資訊(它包括了函式名、函式值型別、引數個數、引數型別和引數順序),因此,在函式呼叫時檢查函式原型是否與函式宣告一致。這樣就能保證函式的正確呼叫。
五、函式的巢狀呼叫
C語言的函式定義是互相平行、獨立的,也就是說,在定義函式時,一個函式內不能再定義另一個函式,即不能巢狀定義,但可以巢狀呼叫函式,即在呼叫一個函式的過程中,又呼叫另一個函式。
① 執行main函式的開頭部分;
② 遇函式呼叫語句,呼叫函式a,流程轉去a函式;
③ 執行a函式的開頭部分;
④ 遇函式呼叫語句,呼叫函式b,流程轉去函式b;
⑤ 執行b函式,如果再無其他巢狀的函式,則完成b函式的全部操作;
⑥ 返回到a函式中呼叫b函式的位置;
⑦ 繼續執行a函式中尚未執行的部分,直到a函式結束;
⑧ 返回main函式中呼叫a函式的位置;
⑨ 繼續執行main函式的剩餘部分直到結束。
例:輸入4個整數,找出其中最大的數。用函式的巢狀呼叫來處理。
#include <stdio.h>
int main()
{ int max4(int a,int b,int c,int d); //對max4的函式宣告
int a,b,c,d,max;
printf("Please enter 4 interger numbers:"); //提示輸入4個數
scanf("%d %d %d %d",&a,&b,&c,&d); //輸入4個數
max=max4(a,b,c,d); //呼叫max4函式,得到4個數中的最大者
printf("max=%d \n",max); //輸出4個數中的最大者
return 0;
}
int max4(int a,int b,int c,int d) //定義max4函式
{ int max2(int a,int b); //對max2的函式宣告
int m;
m=max2(a,b); //呼叫max2函式,得到a和b中的大者,放在m中
m=max2(m,c);//呼叫max2函式,得到a,b,c中的大者,放在m中
m=max2(m,d);//呼叫max2函式,得到a,b,c,d中的大者,放在m中
return(m); //把m作為函式值帶回main函式
}
int max2(int a,int b) //定義max2函式
{ if(a>=b)
return a; //若a≥b,將a作為函式返回值
else
return b; //若a<b,將b作為函式返回值
}
執行結果:
程式分析:
在主函式中要呼叫max4函式,因此在主函式的開頭要對max4函式作宣告。
在max4函式中3次呼叫max2函式,因此在max4函式的開頭要對max2函式作宣告。
由於在主函式中沒有直接呼叫max2函式,因此在主函式中不必對max2函式作宣告,只須在max4函式中作宣告即可。
max4函式執行過程: 第1次呼叫max2函式得到的函式值是a和b中的大者,把它賦給變數m,第2次呼叫max2得到m和c中的大者,也就是a,b,c中的最大者,再把它賦給變數m。第3次呼叫max2得到m和d中的大者,也就是a,b,c,d中的最大者,再把它賦給變數m。
這是一種遞推方法,先求出2個數的大者;再以此為基礎求出3個數的大者;再以此為基礎求出4個數的大者。m的值一次一次地變化,直到實現最終要求。
程式改進:
(1) 可以將max2函式的函式體改為只用一個return語句,返回一個條件表示式的值:
int max2(int a,int b) //定義max2函式
{return(a>=b?a:b);} //返回條件表示式的值,即a和b中的大者
(2) 在max4函式中,3個呼叫max2的語句可以用以下一行代替:
m=max2(max2(max2(a,b),c),d); //把函式呼叫作為函式引數
甚至可以取消變數m,max4函式可寫成
int max4(int a,int b,int c,int d)
{ int max2(int a,int b); //對max2的函式宣告
return max2(max2(max2(a,b),c),d);
}
六、函式的遞迴呼叫
在呼叫一個函式的過程中又出現直接或間接地呼叫該函式本身,稱為函式的遞迴呼叫。
int f(int x)
{
int y,z;
z=f(y); //在執行f函式的過程中又要呼叫f函式
return (2*z);
}
程式中不應出現無終止的遞迴呼叫,而只應出現有限次數的、有終止的遞迴呼叫,這可以用if語句來控制,只有在某一條件成立時才繼續執行遞迴呼叫;否則就不再繼續。
例:
① 有5個學生坐在一起,問第5個學生多少歲,他說比第4個學生大2歲。問第4個學生歲數,他說比第3個學生大2歲。問第3個學生,又說比第2個學生大2歲。問第2個學生,說比第1個學生大2歲。最後問第1個學生,他說是10歲。請問第5個學生多大。
#include <stdio.h>
int main()
{ int age(int n); //對age函式的宣告
printf("NO.5,age:%d\n",age(5)); //輸出第5個學生的年齡
return 0;
}
int age(int n) //定義遞迴函式
{ int c; //c用作存放函式的返回值的變數
if(n==1) //如果n等於1
c=10; //年齡為10
else //如果n不等於1
c=age(n-1)+2; //年齡是前一個學生的年齡加2(如第4個學生年齡是第3個學生年齡加2)
return(c); //返回年齡
}
執行結果:
② 用遞迴方法求n!。
#include <stdio.h>
int main()
{ int fac(int n); //fac函式宣告
int n;
int y;
printf("input an integer number:");
scanf("%d",&n); //輸入要求階乘的數
y=fac(n);
printf("%d!=%d\n",n,y);
return 0;
}
int fac(int n) //定義fac函式
{
int f;
if(n<0) //n不能小於0
printf("n<0,data error!");
else if(n==0||n==1) //n=0或,1時n!=1
f=1; //遞迴終止條件
else
f=fac(n-1)*n; //n>1時,n!=n*(n-1)!
return(f);
}
執行結果:
七、陣列作為函式引數
形式引數 |
實際引數 |
變數 |
常量、變數、表示式、陣列元素 |
陣列 |
陣列 |
1、陣列元素作為函式實參
陣列元素可以用作函式實參,但是不能用作形參。因為形參是在函式被呼叫時臨時分配儲存單元的,不可能為一個數組元素單獨分配儲存單元(陣列是一個整體,在記憶體中佔連續的一段儲存單元)。在用陣列元素作函式實參時,把實參的值傳給形參,是“值傳遞”方式。資料傳遞的方向是從實參傳到形參,單向傳遞。
例:輸入10個數,要求輸出其中值最大的元素和該數是第幾個數。
#include <stdio.h>
int main()
{ int max(int x,int y); //函式宣告
int a[10],m,n,i;
printf("enter 10 integer numbers:");
for(i=0;i<10;i++) //輸入10個數給a[0]~a[9]
scanf("%d",&a[i]);
printf("\n");
for(i=1,m=a[0],n=0;i<10;i++)
{ if(max(m,a[i])>m) //若max函式返回的值大於m
{ m=max(m,a[i]); //max函式返回的值取代m原值
n=i; //把此陣列元素的序號記下來,放在n中
}
}
printf("The largest number is %d\nit is the %dth number.\n",m,n+1);
}
int max(int x,int y) //定義max函式
{ return(x>y?x:y); //返回x和y中的大者
}
執行結果:
程式分析:
從鍵盤輸入10個數給a[0]~a[9]。變數m用來存放當前已比較過的各數中的最大者。開始時設m的值為a[0],然後依次將m與a[i]比,如果a[i]大於m,就以a[i]的值取代m的原值。下一次以m的新值與下一個a[i]比較。經過9輪迴圈的比較,m最後的值就是10個數的最大數。
注意分析怎樣得到最大數是10個數中第幾個數。當每次出現以max(m,a[i])的值取代m的原值時,就把i的值儲存在變數n中。n最後的值就是最大數的序號(注意序號從0開始),如果要輸出“最大數是10個數中第幾個數”,應為n+1。因為陣列元素序號從0開始。
2、一維陣列名作函式引數
陣列名也可以做函式引數(包括實參和形參),應當注意的是:用陣列元素做實參時,向形參變數傳遞的是陣列元素的值,而用陣列名做函式實參時,向形參(陣列名或指標變數)傳遞的是陣列首元素的地址。
例:
① 有一個一維陣列score,內放10個學生成績,求平均成績。
#include <stdio.h>
int main()
{ float average(float array[10]); //函式宣告
float score[10],aver;
int i;
printf("input 10 scores:\n");
for(i=0;i<10;i++)
scanf("%f",&score[i]);
printf("\n");
aver=average(score); //呼叫average函式
printf("average score is %5.2f\n",aver);
return 0;
}
float average(float array[10]) //定義average函式
{ int i;
float aver,sum=array[0];
for(i=1;i<10;i++)
sum=sum+array[i]; //累加學生成績
aver=sum/10;
return(aver);
}
執行結果:
程式分析:
(1) 用陣列名作函式引數,應該在主調函式和被呼叫函式分別定義陣列。
(2) 實引數組與形引數組型別必須一致。
(3) 在定義average函式時,宣告形引數組的大小為10,但在實際上,指定其大小是不起任何作用的,因為C語言編譯系統並不檢查形引數組大小,只是將實引數組的首元素的地址傳給形引數組名。
(4) 形引數組可以不指定大小,在定義陣列時在陣列名後面跟一個空的方括號。eg:float average(float array[])
② 有兩個班級,分別有5和10名學生,呼叫average函式,分別求這兩個班的學生的平均成績。
#include <stdio.h>
int main()
{ float average(float array[],int n);
float score1[5]={98.5,97,91.5,60,55}; //定義長度為5的陣列
float score2[10]={67.5,89.5,99,69.5,77,89.5,76.5,54,60,99.5};
//定義長度為10的陣列
printf("The average of class A is %6.2f\n",average(score1,5));
//用陣列名score1和5作實參
printf("The average of class B is %6.2f\n",average(score2,10));
//用陣列名score2和10作實參
return 0;
}
float average(float array[],int n) //定義average函式,未指定形引數組長度
{ int i;
float aver,sum=array[0];
for(i=1;i<n;i++)
sum=sum+array[i]; //累加n個學生成績
aver=sum/n;
return(aver);
}
執行結果:
程式分析:兩次呼叫average函式時需要處理的陣列元素個數是不同的,在第一次呼叫時將實參(值為5)傳遞給形參n,表示丘5個學生的平均分。第二次呼叫時,求10個學生的平均分。
注意:
③ 用選擇法對陣列中10個整數按由小到大排序。
演算法:選擇排序法
解題思路: 所謂選擇法就是先將10個數中最小的數與a[0]對換;再將a[1]~a[9]中最小的數與a[1]對換……每比較一輪,找出一個未經排序的數中最小的一個。共比較9輪。
#include <stdio.h>
int main()
{ void sort(int array[],int n);
int a[10],i;
printf("enter array:\n");
for(i=0;i<10;i++)
scanf("%d",&a[i]);
sort(a,10); //呼叫sort函式,a為陣列名,大小為10
printf("The sorted array:\n");
for(i=0;i<10;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
void sort(int array[],int n)
{ int i,j,k,t;
for(i=0;i<n-1;i++)
{ k=i;
for(j=i+1;j<n;j++)
if(array[j]<array[k])
k=j;
t=array[k]; array[k]=array[i]; array[i]=t;
}
}
執行結果:
程式分析:可以看到在執行函式呼叫語句“sort(a,10);”之前和之後,a陣列中各元素的值是不同的。原來是無序的,執行“sort(a,10);”後,a陣列已經排好序了,這是由於形引數組array已用選擇法進行排序了,形引數組改變也使實引數組隨之改變。
3、多維陣列名作函式引數
可以用多維陣列名作為函式的實參和形參,在被呼叫函式中對形引數組定義時可以指定每一維的大小,也可以省略第一維的大小說明。
int array[3][10]; 或 int array[][10]; //二者等價
int array[][]; 或 int array[3][ ]; //錯誤,必須指定列數
在定義二維陣列時,必須指定列數(即一行中包含幾個元素) ,由於形引數組與實引數組型別相同,所以它們是由具有相同長度的一維陣列所組成的。不能只指定第1維(行數)而省略第2維(列數)。
在第2維大小相同的前提下,形引數組的第1維可以與實引數組不同。例如,實引數組定義為int score[5][10];而形引數組定義為int array[ ][10];或int array[8][10];均可以。這時形引數組和實引數組都是由相同型別和大小的一維陣列組成的。C語言編譯系統不檢查第一維的大小。
例:有一個3×4的矩陣,求所有元素中的最大值。
#include <stdio.h>
int main()
{ int max_value(int array[][4]); //函式宣告
int a[3][4]={{1,3,5,7},{2,4,6,8},{15,17,34,12}}; //對陣列元素賦初值
printf("Max value is %d\n",max_value(a)); //max_value(a)為函式呼叫
return 0;
}
int max_value(int array[][4]) //函式定義
{ int i,j,max;
max=array[0][0];
for(i=0;i<3;i++)
for(j=0;j<4;j++)
if(array[i][j]>max) max=array[i][j]; //把大者放在max中
return(max);
}
執行結果:
程式分析:
形引數組array第1維的大小省略,第2維大小不能省略,而且要和實引數組a的第2維的大小相同。
在主函式呼叫max_value函式時,把實參二維陣列a的第1行的起始地址傳遞給形引數組array,因此array陣列第1行的起始地址與a陣列的第1行的起始地址相同。
由於兩個陣列的列數相同,因此array陣列第2行的起始地址與a陣列的第2行的起始地址相同。a[i][j]與array[i][j]同佔一個儲存單元,它們具有同一個值。實際上,array[i][j]就是a[i][j],在函式中對array[i][j]的操作就是對a[i][j]的操作。
八、區域性變數和全域性變數
每一個變數都有一個作用域問題,即它們在什麼範圍內有效。
1、區域性變數
定義變數可能有3種情況:
(1) 在函式的開頭定義;
(2) 在函式內的複合語句內定義;
(3) 在函式的外部定義。
在一個函式內部定義的變數只在本函式範圍內有效,也就是說只有在本函式內才能引用它們,在此函式以外是不能使用這些變數的。在複合語句內定義的變數只在本複合語句範圍內有效,只有在本複合語句內才能引用它們,在該複合語句以外是不能使用這些變數的,以上這些稱為“區域性變數”。
(1) 主函式中定義的變數也只在主函式中有效。主函式也不能使用其他函式中定義的變數。
(2) 不同函式中可以使用同名的變數,它們代表不同的物件,互不干擾。
(3) 形式引數也是區域性變數。只在定義它的函式中有效。其他函式中不能直接引用形參。
(4) 在一個函式內部,可以在複合語句中定義變數,這些變數只在本複合語句中有效,這種複合語句也稱為“分程式”或“程式塊”。
2、全域性變數
程式的編譯單位是源程式檔案,一個原始檔可以包含一個或若干個函式。在函式內定義的變數是區域性變數,而在函式之外定義的變數稱為外部變數,外部變數是全域性變數(也稱全程變數)。全域性變數可以為本檔案中其他函式所共用。它的有效範圍為從定義變數的位置開始到本原始檔結束。
注意:在函式內定義的變數是區域性變數,在函式外定義的變數是全域性變數。
設定全域性變數的作用是增加了函式間資料聯絡的渠道。由於同一檔案中的所有函式都能引用全域性變數的值,因此如果在一個函式中改變了全域性變數的值,就能影響到其他函式中全域性變數的值。相當於各個函式間有直接的傳遞通道。由於函式的呼叫只能帶回一個函式返回值,因此有時可以利用全域性變數來增加函式間的聯絡渠道,通過函式呼叫能得到一個以上的值。
為了便於區別全域性變數和區域性變數,在C程式設計人員中有一個習慣(但非規定),將全域性變數名的第1個字母用大寫表示。
例:有一個一維陣列,內放10個學生成績,寫一個函式,當主函式呼叫此函式後,能求出平均分、最高分和最低分。
#include <stdio.h>
float Max=0,Min=0; //定義全域性變數Max,Min
int main()
{ float average(float array[],int n);
float ave,score[10];
int i;
printf("Please enter 10 scores:");
for(i=0;i<10;i++)
scanf("%f",&score[i]);
ave=average(score,10);
printf("max=%6.2f\nmin=%6.2f\naverage=%6.2f\n",Max,Min,ave);
return 0;
}
float average(float array[],int n) //定義函式,有一形參是陣列
{ int i;
float aver,sum=array[0];
Max=Min=array[0];
for(i=1;i<n;i++)
{ if(array[i]>Max) Max=array[i];
else if(array[i]<Min) Min=array[i];
sum=sum+array[i];
}
aver=sum/n;
return(aver);
}
執行結果:
但是,建議不在必要時不要使用全域性變數,原因如下:
① 全域性變數在程式的全部執行過程中都佔用儲存單元,而不是僅在需要時才開闢單元。
② 它使函式的通用性降低了,因為如果在函式中引用了全域性變數,那麼執行情況會受到有關的外部變數的影響,如果將一個函式移到另一個檔案中,還要考慮把有關的外部變數及其值一起移過去。但是若該外部變數與其他檔案的變數同名時,就會出現問題。這就降低了程式的可靠性和通用性。在程式設計中,在劃分模組時要求模組的“內聚性”強、與其他模組的“耦合性”弱。即模組的功能要單一(不要把許多互不相干的功能放到一個模組中),與其他模組的相互影響要儘量少,而用全域性變數是不符合這個原則的。一般要求把C程式中的函式做成一個相對的封閉體,除了可以通過“實參—形參”的渠道與外界發生聯絡外,沒有其他渠道。這樣的程式移植性好,可讀性強。
③ 使用全域性變數過多,會降低程式的清晰性,人們往往難以清楚地判斷出每個瞬時各個外部變數的值。由於在各個函式執行時都可能改變外部變數的值,程式容易出錯。因此,要限制使用全域性變數。
例: 若外部變數與區域性變數同名,分析結果。
#include <stdio.h>
int a=3,b=5; //a,b是全域性變數
int main()
{
int max(int a,int b); //函式宣告。a,b是形參
int a=8; //a是區域性變數,值為8,在main函式中相當於遮蔽了值為3全域性變數a
printf("a=%d,b=%d,最大值是max=%d\n",a,b,max(a,b));
return 0;
}
int max(int a,int b) //a,b是函式形參
{ int c;
c=a>b?a:b; //把a和b中的大者存放在c中
return(c);
}
執行結果:
程式分析:
程式第2行定義了全域性變數a和b,並對其初始化。
第3行是main函式,在main函式中(第6行)定義了一個區域性變數a。區域性變數a的作用範圍為第6~8行。在此範圍內全域性變數a被區域性變數a遮蔽,相當於全域性變數a在此範圍內不存在(即它不起作用),而全域性變數b在此範圍內有效。因此第6行中max(a,b)的實參a應是區域性變數a,所以max(a,b)相當於max(8,5)。它的值為8。
第10行起定義max函式,形參a和b是區域性變數。全域性變數a和b在max函式範圍內不起作用,所以函式max中的a和b不是全域性變數a和b,而是形參a和b,它們的值是由實參傳給形參的,即8和5。
九、變數的儲存方式和生存期
1、動態儲存方式與靜態儲存方式
從變數值存在的時間(即生存期)來觀察,有的變數在程式執行的整個過程都是存在的,而有的變數則是在呼叫其所在的函式時才臨時分配儲存單元,而在函式呼叫結束後該儲存單元就馬上釋放了,變數不存在了。 也就是說,變數的儲存有兩種不同的方式: 靜態儲存方式和動態儲存方式。
- 靜態儲存方式是指在程式執行期間由系統分配固定的儲存空間的方式。
- 動態儲存方式則是在程式執行期間根據需要進行動態的分配儲存空間的方式。
如下圖是記憶體中的供使用者使用的儲存空間的情況,這個儲存空間可以分為3部分:
資料分別存放在靜態儲存區和動態儲存區中。全域性變數全部存放在靜態儲存區中,在程式開始執行時給全域性變數分配儲存區,程式執行完畢就釋放。在程式執行過程中它們佔據固定的儲存單元,而不是動態地進行分配和釋放。
在動態儲存區中存放以下資料:
① 函式形式引數。在呼叫函式時給形參分配儲存空間。
② 函式中定義的沒有用關鍵字static宣告的變數,即自動變數。
③ 函式呼叫時的現場保護和返回地址等。
對以上這些資料,在函式呼叫開始時分配動態儲存空間,函式結束時釋放這些空間。在程式執行過程中,這種分配和釋放是動態的,如果在一個程式中兩次呼叫同一函式,而在此函式中定義了局部變數,在兩次呼叫時分配給這些區域性變數的儲存空間的地址可能是不相同的。
如果一個程式中包含若干個函式,每個函式中的區域性變數的生存期並不等於整個程式的執行週期,它只是程式執行週期的一部分。在程式執行過程中,先後呼叫各個函式,此時會動態地分配和釋放儲存空間。
2、區域性變數的儲存類別
在C語言中,每一個變數和函式都有兩個屬性: 資料型別和資料的儲存類別。 儲存類別指的是資料在記憶體中儲存的方式(如靜態儲存和動態儲存)。
在定義和宣告變數和函式時,一般應同時指定其資料型別和儲存類別,也可以採用預設方式指定(即如果使用者不指定,系統會隱含地指定為某一種儲存類別)。
C的儲存類別包括4種: 自動的(auto)、靜態的(statis)、暫存器的(register)、外部的(extern)。根據變數的儲存類別,可以知道變數的作用域和生存期。
① 自動變數(auto變數)
函式中的區域性變數,如果不專門宣告為static(靜態)儲存類別,都是動態地分配儲存空間的,資料儲存在動態儲存區中。函式中的形參和在函式中定義的區域性變數(包括在複合語句中定義的區域性變數),都屬於此類。在呼叫該函式時,系統會給這些變數分配儲存空間,在函式呼叫結束時就自動釋放這些儲存空間。因此這類區域性變數稱為自動變數。自動變數用關鍵字auto作儲存類別的宣告。
實際上,關鍵字auto可以省略,不寫auto則隱含指定為“自動儲存類別”,它屬於動態儲存方式。程式中大多數變數屬於自動變數。 auto int b,c=3; //等價於int b,c=3;
② 靜態區域性變數(static區域性變數)
(1) 靜態區域性變數屬於靜態儲存類別,在靜態儲存區內分配儲存單元。在程式整個執行期間都不釋放。而自動變數(即動態區域性變數)屬於動態儲存類別,分配在動態儲存區空間而不在靜態儲存區空間,函式呼叫結束後即釋放。
(2) 對靜態區域性變數是在編譯時賦初值的,即只賦初值一次,在程式執行時它已有初值。以後每次呼叫函式時不再重新賦初值而只是保留上次函式呼叫結束時的值。而對自動變數賦初值,不是在編譯時進行的,而是在函式呼叫時進行的,每呼叫一次函式重新給一次初值,相當於執行一次賦值語句。
(3) 如果在定義區域性變數時不賦初值的話,則對靜態區域性變數來說,編譯時自動賦初值0(對數值型變數)或空字元′\0′(對字元變數)。而對自動變數來說,它的值是一個不確定的值。這是由於每次函式呼叫結束後儲存單元已釋放,下次呼叫時又重新另分配儲存單元,而所分配的單元中的內容是不可知的。
(4) 雖然靜態區域性變數在函式呼叫結束後仍然存在,但其他函式是不能引用它的。因為它是區域性變數,只能被本函式引用,而不能被其他函式引用。
例:輸出1到5的階乘值。(當需要保留函式上一次呼叫結束時的值時,使用靜態區域性變數比較方便)
#include <stdio.h>
int main()
{ int fac(int n);
int i;
for(i=1;i<=5;i++) //先後5次呼叫fac函式
printf("%d!=%d\n",i,fac(i)); //每次計算並輸出i!的值
return 0;
}
int fac(int n)
{ static int f=1; //f保留了上次呼叫結束時的值
f=f*n; //在上次的f值的基礎上再乘以n
return(f); //返回值f是n!的值
}
執行結果:
程式分析:
(1) 每次呼叫fac(i),輸出一個i!,同時保留這個i!的值以便下次再乘(i+1)。
(2) 如果函式中的變數只被引用而不改變值,則定義為靜態區域性變數(同時初始化)比較方便,以免每次呼叫時重新賦值。
注意:用靜態儲存要多佔記憶體(長期佔用不釋放,而不能像動態儲存那樣一個儲存單元可以先後為多個變數使用,節約記憶體),而且降低了程式的可讀性,當呼叫次數多時往往弄不清靜態區域性變數的當前值是什麼。因此,若非必要,不要多用靜態區域性變數。
③ 暫存器變數(register變數)
一般情況下,變數(包括靜態儲存方式和動態儲存方式)的值是存放在記憶體中的。當程式中用到哪一個變數的值時,由控制器發出指令將記憶體中該變數的值送到運算器中。 經過運算器進行運算,如果需要存數,再從運算器將資料送到記憶體存放。
如果有一些變數使用頻繁(例如,在一個函式中執行10 000次迴圈,每次迴圈中都要引用某區域性變數),則為存取變數的值要花費不少時間。為提高執行效率,允許將區域性變數的值放在CPU中的暫存器中,需要用時直接從暫存器取出參加運算,不必再到記憶體中去存取。由於對暫存器的存取速度遠高於對記憶體的存取速度,因此這樣做可以提高執行效率。這種變數叫做暫存器變數,用關鍵字register作宣告。如
register int f; //定義f為暫存器變數
由於現在的計算機的速度愈來愈快,效能愈來愈高, 優化的編譯系統能夠識別使用頻繁的變數,從而自動地將這些變數放在暫存器中,而不需要程式設計者指定。因此,現在實際上用register宣告變數的必要性不大