1. 概述
在前面已經介紹過,C源程式是由函數組成的。雖然在前面各章的程式中大都隻有一個主函數main(),但實用程式往往由多個函數組成。函數是C源程式的基本子產品,通過對函數子產品的調用實作特定的功能。C語言中的函數相當于其它進階語言的子程式。C語言不僅提供了極為豐富的庫函數(如Turbo C,MS C都提供了三百多個庫函數),還允許使用者建立自己定義的函數。使用者可把自己的算法編成一個個相對獨立的函數子產品,然後用調用的方法來使用函數。可以說C程式的全部工作都是由各式各樣的函數完成的,是以也把C語言稱為函數式語言。
由于采用了函數子產品式的結構,C語言易于實作結構化程式設計。使程式的層次結構清晰,便于程式的編寫、閱讀、調試。
在C語言中可從不同的角度對函數分類。
1.1 從函數定義角度分類
從函數定義的角度看,函數可分為庫函數和使用者定義函數兩種。
1 庫函數:由C系統提供,使用者無須定義,也不必在程式中作類型說明,隻需在程式前包含有該函數原型的頭檔案即可在程式中直接調用。在前面各章的例題中反複用到printf、scanf、getchar、putchar、gets、puts、strcat等函數均屬此類。
2 使用者定義函數:由使用者按需要寫的函數。對于使用者自定義函數,不僅要在程式中定義函數本身,而且在主調函數子產品中還必須對該被調函數進行類型說明,然後才能使用。
C語言提供了極為豐富的庫函數,這些庫函數又可從功能角度作以下分類。
1) 字元類型分類函數:用于對字元按ASCII碼分類:字母,數字,控制字元,分隔符,大小寫字母等。
2) 轉換函數:用于字元或字元串的轉換;在字元量和各類數字量(整型,實型等)之間進行轉換;在大、小寫之間進行轉換。
3) 目錄路徑函數:用于檔案目錄和路徑操作。
4) 診斷函數:用于内部錯誤檢測。
5) 圖形函數:用于螢幕管理和各種圖形功能。
6) 輸入輸出函數:用于完成輸入輸出功能。
7) 接口函數:用于與DOS,BIOS和硬體的接口。
8) 字元串函數:用于字元串操作和處理。
9) 記憶體管理函數:用于記憶體管理。
10) 數學函數:用于數學函數計算。
11) 日期和時間函數:用于日期,時間轉換操作。
12) 程序控制函數:用于程序管理和控制。
13) 其它函數:用于其它各種功能。
1.2 從函數傳回值角度分類
C語言的函數兼有其它語言中的函數和過程兩種功能,從這個角度看,又可把函數分為有傳回值函數和無傳回值函數兩種。
1 有傳回值函數:此類函數被調用執行完後将向調用者傳回一個執行結果,稱為函數傳回值。如數學函數即屬于此類函數。由使用者定義的這種要傳回函數值的函數,必須在函數定義和函數說明中明确傳回值的類型。
2 無傳回值函數:此類函數用于完成某項特定的處理任務,執行完成後不向調用者傳回函數值。這類函數類似于其它語言的過程。由于函數無須傳回值,使用者在定義此類函數時可指定它的傳回為“空類型”,空類型的說明符為“void”。
1.3 從參數傳遞角度分類
從主調函數和被調函數之間資料傳送的角度看又可分為無參函數和有參函數兩種。
1) 無參函數:函數定義、函數說明及函數調用中均不帶參數。主調函數和被調函數之間不進行參數傳送。此類函數通常用來完成一組指定的功能,可以傳回或不傳回函數值。
2) 有參函數:也稱為帶參函數。在函數定義及函數說明時都有參數,稱為形式參數(簡稱為形參)。在函數調用時也必須給出參數,稱為實際參數(簡稱為實參)。進行函數調用時,主調函數将把實參的值傳送給形參,供被調函數使用。
1.4 從函數能否被其他源檔案調用的角度分類
函數本質上是全局的,因為一個函數要被另外的函數調用,但是也可以制定函數隻能被本檔案調用,而不能被其他檔案調用。根據函數能否被其他源檔案調用,将函數區分為内部函數和外部函數。
1. 内部函數
定義:一個函數隻能被本檔案中其他函數調用,它稱為内部函數。
格式:
static 類型辨別符 函數名(形參表)
2. 外部函數
定義:在函數首部的最左邊冠以關鍵字extern,則表示此安徽省農戶是外部函數,可以供其他檔案調用。
格式:
extern 類型辨別符 函數名(形參表)
注釋:
(1)C++允許在聲明函數時省略extern.
(2)#include指令的應用:利用函數原型擴充函數作用域。
還應該指出的是,在C語言中,所有的函數定義,包括主函數main在内,都是平行的。也就是說,在一個函數的函數體内,不能再定義另一個函數,即不能嵌套定義。但是函數之間允許互相調用,也允許嵌套調用。習慣上把調用者稱為主調函數。函數還可以自己調用自己,稱為遞歸調用。
main 函數是主函數,它可以調用其它函數,而不允許被其它函數調用。是以,C程式的執行總是從main函數開始,完成對其它函數的調用後再傳回到main函數,最後由main函數結束整個程式。一個C源程式必須有,也隻能有一個主函數main。
2. 函數定義
2.1 無參函數的定義形式
類型辨別符 函數名()
{聲明部分
語句
}
其中類型辨別符和函數名稱為函數頭。類型辨別符指明了本函數的類型,函數的類型實際上是函數傳回值的類型。該類型辨別符與前面介紹的各種說明符相同。函數名是由使用者定義的辨別符,函數名後有一個空括号,其中無參數,但括号不可少。
{}中的内容稱為函數體。在函數體中聲明部分,是對函數體内部所用到的變量的類型說明。在很多情況下都不要求無參函數有傳回值,此時函數類型符可以寫為void。
2.2 有參函數定義的一般形式
類型辨別符 函數名(形式參數表列)
{聲明部分
語句
}
有參函數比無參函數多了一個内容,即形式參數表列。在形參表中給出的參數稱為形式參數,它們可以是各種類型的變量,各參數之間用逗号間隔。在進行函數調用時,主調函數将賦予這些形式參數實際的值。形參既然是變量,必須在形參表中給出形參的類型說明。
2.3 函數的形式參數和實際參數
形參出現在函數定義中,在整個函數體内都可以使用,離開該函數則不能使用。實參出現在主調函數中,進入被調函數後,實參變量也不能使用。形參和實參的功能是作資料傳送。發生函數調用時,主調函數把實參的值傳送給被調函數的形參進而實作主調函數向被調函數的資料傳送。
函數的形參和實參具有以下特點:
1. 形參變量隻有在被調用時才配置設定記憶體單元,在調用結束時,即刻釋放所配置設定的記憶體單元。是以,形參隻有在函數内部有效。函數調用結束傳回主調函數後則不能再使用該形參變量。
2. 實參可以是常量、變量、表達式、函數等,無論實參是何種類型的量,在進行函數調用時,它們都必須具有确定的值,以便把這些值傳送給形參。是以應預先用指派,輸入等辦法使實參獲得确定值。
3. 實參和形參在數量上,類型上,順序上應嚴格一緻,否則會發生類型不比對”的錯誤。
4. 函數調用中發生的資料傳送是單向的。即隻能把實參的值傳送給形參,而不能把形參的值反向地傳送給實參。是以在函數調用過程中,形參的值發生改變,而實參中的值不會變化。
2.4 函數的傳回值
函數的值是指函數被調用之後,執行函數體中的程式段所取得的并傳回給主調函數的值。對函數的值(或稱函數傳回值)有以下一些說明:
1) 函數的值隻能通過return語句傳回主調函數。
return 語句的一般形式為:
return 表達式;
或者為:
return (表達式);
該語句的功能是計算表達式的值,并傳回給主調函數。在函數中允許有多個return語句,但每次調用隻能有一個return 語句被執行,是以隻能傳回一個函數值。
2) 函數值的類型和函數定義中函數的類型應保持一緻。如果兩者不一緻,則以函數類型為準,自動進行類型轉換。
3) 如函數值為整型,在函數定義時可以省去類型說明。
4) 不傳回函數值的函數,可以明确定義為“空類型”,類型說明符為“void”。如:
void s(int n)
{ ……
}
一旦函數被定義為空類型後,就不能在主調函數中使用被調函數的函數值了。例如,在定義s為空類型後,在主函數中寫下述語句
sum=s(n);就是錯誤的。為了使程式有良好的可讀性并減少出錯, 凡不要求傳回值的函數都應定義為空類型。
3. 函數的調用
3.1 函數調用的一般形式
C/C++語言中,函數調用的一般形式為:
函數名(實際參數表)
對無參函數調用時則無實際參數表。實際參數表中的參數可以是常數,變量或其它構造類型資料及表達式。各實參之間用逗号分隔。
3.2 函數調用的方式
在C/C++語言中,可以用以下幾種方式調用函數:
1. 函數表達式:函數作為表達式中的一項出現在表達式中,以函數傳回值參與表達式的運算。這種方式要求函數是有傳回值的。例如:z=max(x,y)是一個指派表達式,把max的傳回值賦予變量z。
2. 函數語句:函數調用的一般形式加上分号即構成函數語句。例如: printf("%d",a);scanf ("%d",&b);都是以函數語句的方式調用函數。
3. 函數實參:函數作為另一個函數調用的實際參數出現。這種情況是把該函數的傳回值作為實參進行傳送,是以要求該函數必須是有傳回值的。例如: printf("%d",max(x,y)); 即是把max調用的傳回值又作為printf函數的實參來使用的。在函數調用中還應該注意的一個問題是求值順序的問題。所謂求值順序是指對實參表中各量是自左至右使用呢,還是自右至左使用。對此,各系統的規定不一定相同。介紹printf 函數時已提到過,這裡從函數調用的角度再強調一下。
【例6.1】
main()
{
int i=8;
printf("%d\n%d\n%d\n%d\n",++i,--i,i++,i--);
}
如按照從右至左的順序求值。運作結果應為:
8
7
7
8
如對printf語句中的++i,--i,i++,i--從左至右求值,結果應為:
9
8
8
9
應特别注意的是,無論是從左至右求值, 還是自右至左求值,其輸出順序都是不變的,即輸出順序總是和實參表中實參的順序相同。由于Turbo C現定是自右至左求值,是以結果為8,7,7,8。
3.3 對被調用函數的聲明和函數原型
函數聲明:在函數尚未定義的情況下,事先将該函數的有關資訊通知編譯系統,以便使得編譯能正常運作。
在主調函數中調用某函數之前應對該被調函數進行說明(聲明),這與使用變量之前要先進行變量說明是一樣的。在主調函數中對被調函數作說明的目的是使編譯系統知道被調函數傳回值的類型,以便在主調函數中按此種類型對傳回值作相應的處理。
函數原型的一般形式為:
類型說明符 被調函數名(類型 形參,類型 形參…);
或為:
類型說明符 被調函數名(類型,類型…);
括号内給出了形參的類型和形參名,或隻給出形參類型。這便于編譯系統進行檢錯,以防止可能出現的錯誤。
函數原型的主要作用:根據函數原型在程式編譯階段對調用函數的合法性進行全面檢查。
C語言中又規定在以下幾種情況時可以省去主調函數中對被調函數的函數說明。
1) 當被調函數的函數定義出現在主調函數之前時,在主調函數中也可以不對被調函數再作說明而直接調用。
2) 函數聲明的位置可以在調用函數所在的函數中,也可以在函數外。如在所有函數定義之前,在函數外預先說明了各個函數的類型,則在以後的各主調函數中,可不再對被調函數作說明。例如:
char str(int a);
float f(float b);
main()
{
……
}
char str(int a)
{
……
}
float f(float b)
{
……
}
其中第一,二行對str函數和f函數預先作了說明。是以在以後各函數中無須對str和f函數再作說明就可直接調用。
3) 對庫函數的調用不需要再作說明,但必須把該函數的頭檔案用include指令包含在源檔案前部。
3.4 函數的嵌套調用
C語言中不允許作嵌套的函數定義。是以各函數之間是平行的,不存在上一級函數和下一級函數的問題。但是C語言允許在一個函數的定義中出現對另一個函數的調用。這樣就出現了函數的嵌套調用。即在被調函數中又調用其它函數。這與其它語言的子程式嵌套的情形是類似的。其關系可表示如圖。
圖表示了兩層嵌套的情形。其執行過程是:執行main函數中調用a函數的語句時,即轉去執行a函數,在a函數中調用b 函數時,又轉去執行b函數,b函數執行完畢傳回a函數的斷點繼續執行,a函數執行完畢傳回main函數的斷點繼續執行。
3.5 函數的遞歸調用
一個函數在它的函數體内調用它自身稱為遞歸調用。這種函數稱為遞歸函數。C語言允許函數的遞歸調用。在遞歸調用中,主調函數又是被調函數。執行遞歸函數将反複調用其自身,每調用一次就進入新的一層。
例如有函數f如下:
int f(int x)
{
int y;
z=f(y);
return z;
}
這個函數是一個遞歸函數。但是運作該函數将無休止地調用其自身,這當然是不正确的。為了防止遞歸調用無終止地進行,必須在函數内有終止遞歸調用的手段。常用的辦法是加條件判斷,滿足某種條件後就不再作遞歸調用,然後逐層傳回。下面舉例說明遞歸調用的執行過程。
【例6.2】用遞歸法計算n!
用遞歸法計算n!可用下述公式表示:
n!=1 (n=0,1)
n×(n-1)! (n>1)
按公式可程式設計如下:
long ff(int n)
{
long f;
if(n<0) printf("n<0,input error");
else if(n==0||n==1) f=1;
else f=ff(n-1)*n;
return(f);
}
main()
{
int n;
long y;
printf("\ninput a inteager number:\n");
scanf("%d",&n);
y=ff(n);
printf("%d!=%ld",n,y);
}
【例6.3】Hanoi塔問題
一塊闆上有三根針,A,B,C。A針上套有64個大小不等的圓盤,大的在下,小的在上。要把這64個圓盤從A針移動C針上,每次隻能移動一個圓盤,移動可以借助B針進行。但在任何時候,任何針上的圓盤都必須保持大盤在下,小盤在上。求移動的步驟。
本題算法分析如下,設A上有n個盤子。
如果n=1,則将圓盤從A直接移動到C。
如果n=2,則:
1.将A上的n-1(等于1)個圓盤移到B上;
2.再将A上的一個圓盤移到C上;
3.最後将B上的n-1(等于1)個圓盤移到C上。
如果n=3,則:
A. 将A上的n-1(等于2,令其為n`)個圓盤移到B(借助于C),步驟如下:
(1)将A上的n`-1(等于1)個圓盤移到C上。
(2)将A上的一個圓盤移到B。
(3)将C上的n`-1(等于1)個圓盤移到B。
B. 将A上的一個圓盤移到C。
C. 将B上的n-1(等于2,令其為n`)個圓盤移到C(借助A),步驟如下:
(1)将B上的n`-1(等于1)個圓盤移到A。
(2)将B上的一個盤子移到C。
(3)将A上的n`-1(等于1)個圓盤移到C。
到此,完成了三個圓盤的移動過程。
從上面分析可以看出,當n大于等于2時,移動的過程可分解為三個步驟:
第一步 把A上的n-1個圓盤移到B上;
第二步 把A上的一個圓盤移到C上;
第三步 把B上的n-1個圓盤移到C上;其中第一步和第三步是類同的。
當n=3時,第一步和第三步又分解為類同的三步,即把n`-1個圓盤從一個針移到另一個針上,這裡的n`=n-1。 顯然這是一個遞歸過程,據此算法可程式設計如下:
move(int n,int x,int y,int z)
{
if(n==1)
printf("%c-->%c\n",x,z);
else
{
move(n-1,x,z,y);
printf("%c-->%c\n",x,z);
move(n-1,y,x,z);
}
}
main()
{
int h;
printf("\ninput number:\n");
scanf("%d",&h);
printf("the step to moving %2d diskes:\n",h);
move(h,'a','b','c');
}
4. C++的函數特性
4.1 内聯函數
程式通過一組函數實作是一種好的設計方法。但是函數調用涉及執行時間的開銷。C++提供的内聯函數可以減少函數調用的開銷。内聯函數的定義格式:
inline <函數值類型> <函數名>(<形式參數表>)
{
函數體
}
(1)對使用者來說,内聯函數的定義與調用與普通函數的使用方法是相似的。
(2)作為編譯系統,它将程式中調用内聯函數的語句(或表達式)用内聯函數體中的代碼進行替換。這樣在執行時就避免了對内聯函數的調用,進而減少了因函數調用所增加的時間開銷,提高了程式運作的效率。
(3)使用内聯函數可以節省運作時間,但卻增加了目标程式的的長度。是以一般隻将規模很小而使用頻繁的簡單函數聲明為内聯函數。
4.2 函數重載
定義:對一個函數名重新賦予它新的含義,使得一個函數名可以多用。所謂重載,就是一物多用。函數可以重載,運算符也可以重載。
重載函數的參數個數、參數類型和參數順序中至少有一種不同,函數傳回值類型可以相同也可以不同。
4.3 函數模闆
模闆又叫泛型程式設計,分為函數模闆與類模闆兩部分。
函數模闆不是一個實在的函數,編譯器不能為其生成可執行代碼。定義函數模闆後隻是一個對函數功能架構的描述,當它具體執行時,将根據傳遞的實際參數決定其功能。
(1)函數模闆的含義:建立一個一個通用函數,其函數類型和形參類型不具體指定,用一個虛拟的類型代替,這個通用函數就稱為函數模闆。
(2)函數模闆定義的一般形式:
template<typename T> 傳回類型 函數名(形式參數表) { ... //函數體 }
或者:
template<class T> 傳回類型 函數名(形式參數表) { ... //函數體 }
類型參數可以不止一個,可以根據 需要确定個數。如:
template <class T1, typename T2>
參數類型T,T1.T2是一個虛拟的類型名,表示模闆中出現的是一個類型名。
(3) 函數模闆隻适合于函數體相同、函數參數個數相同而類型不同的情況。如果參數個數不同,不能使用函數模闆。
4.4 有預設參數的函數
(1)含義:給形式參數一個預設的值,形參不一定必須從實參取值;
(2)可以給任意多個形參以預設值。實參與形參的結合順序是從左往右,含有預設值的形參必須在形參清單中的最右端,否則會出錯。
如:void f1(float a, int c, int b=0, char d='a');
(3)如果函數的定義在函數的調用之前,則應在函數定義中給出預設值。為了避免混淆,最好隻在函數聲明時制定預設值。
(4)一個函數不能即作為重載函數,又作為有預設參數的函數。否則編譯器因無法判定調用哪一個函數而報錯。