




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)
文檔簡介
第6章函數(shù)6.1函數(shù)的基本語法知識6.2函數(shù)調(diào)用6.3函數(shù)的嵌套調(diào)用與遞歸調(diào)用6.4函數(shù)分解6.5變量的存儲類別與作用域6.6程序結(jié)構(gòu)6.7庫函數(shù)本章小結(jié)
6.1函數(shù)的基本語法知識
在前面已經(jīng)介紹過,C源程序是由函數(shù)組成的。C語言不僅提供了極為豐富的庫函數(shù)(如TurboC、MSC都提供了三百多個庫函數(shù)),還允許用戶定義自己的函數(shù)。用戶可以把自己的算法編寫成一個個相對獨立的函數(shù)模塊,然后用調(diào)用的方法來使用函數(shù)??梢哉f,C程序的全部工作都是由各式各樣的函數(shù)完成的,所以也把C語言稱為函數(shù)式語言。
C語言中函數(shù)的基本語法知識包括函數(shù)定義的一般形式、return語句、函數(shù)調(diào)用及函數(shù)聲明。6.1.1函數(shù)定義
雖然“函數(shù)”這個術(shù)語來自數(shù)學(xué),但C語言中函數(shù)的概念不完全等同于數(shù)學(xué)中函數(shù)的概念。在數(shù)學(xué)領(lǐng)域,函數(shù)是一種關(guān)系,這種關(guān)系使一個集合里的每一個元素對應(yīng)到另一個(可能相同的)集合里的唯一元素;在C語言中,函數(shù)是指能夠完成特定功能的一段代碼,它不一定要有參數(shù),也不一定要有計算結(jié)果。函數(shù)定義的一般形式為
類型說明符函數(shù)名(形式參數(shù)表)
{
聲明部分
語句部分
}其中:
(1)類型說明符定義了該函數(shù)的類型,即函數(shù)執(zhí)行完后其返回值的類型。它遵循以下規(guī)則:可以為除數(shù)組外的任意類型,包括基本數(shù)據(jù)類型(整型、字符型等)、組合類型(結(jié)構(gòu))和指針類型。此外,類型說明符還可以為void(空)類型,表示該函數(shù)無返回值,稱為無返回值函數(shù)。類型說明符也可以省略不寫,缺省時默認的函數(shù)類型為整型。
(2)函數(shù)名是一個用戶自定義的標(biāo)識符,其命名規(guī)則同變量名完全一樣。函數(shù)名中存放的是函數(shù)的入口地址值。
(3)形式參數(shù)簡稱形參,是函數(shù)接收外部數(shù)據(jù)的接口。當(dāng)實際執(zhí)行到該函數(shù)時,各個形參已經(jīng)對應(yīng)了實際數(shù)值,無需在函數(shù)體內(nèi)部再對其賦值。在形參表中必須逐個說明各參數(shù)的類型,說明方式為
(類型1形參1,類型2形參2,…,類型n形參n)
即使幾個形參具有相同的數(shù)據(jù)類型,也必須對每個形參分別進行類型說明。當(dāng)沒有形參時,稱為無參函數(shù),此時形參表雖然為空,但左右圓括號不能省去。
(4)由“{”、“}”括起來的部分稱為函數(shù)體,它由聲明部分和語句部分組成。聲明部分主要包括在函數(shù)內(nèi)部用到的變量或函數(shù)的聲明語句;語句部分由若干條語句組成。對于返回類型為void的函數(shù),其函數(shù)體可以為空。
【例6.1】各類函數(shù)舉例如表6-1所示。通常我們將函數(shù)定義中除去函數(shù)體的其余部分稱為函數(shù)頭。這樣,一個函數(shù)定義就包括了函數(shù)頭和函數(shù)體兩部分。函數(shù)頭主要對函數(shù)的名字、輸入量及輸出量的類型進行定義,是函數(shù)與外界(程序其余部分)的接口。函數(shù)體主要用來實現(xiàn)本函數(shù)的功能,在函數(shù)體內(nèi)部看來形參是已知量,對于有返回值的函數(shù),其最終的計算結(jié)果必須用return語句(參見6.1.3節(jié))帶回。在函數(shù)定義時函數(shù)頭的設(shè)計尤為重要。函數(shù)名最好用能反映函數(shù)功能的英文單詞;函數(shù)的類型就是函數(shù)返回值(輸出量)的類型;函數(shù)的形參列表包括了完成本函數(shù)功能必須已知的數(shù)據(jù)(輸入量)的名字及類型。在定義函數(shù)時,定義者必須從具體問題中分析得到以上三類信息。另外,設(shè)計函數(shù)體時,首先要記住在函數(shù)體內(nèi)部形參是已知量,其次,要注意返回值的類型最好與函數(shù)的類型保持一致。
【例6.2】
編寫函數(shù)求n!,n≥0。
分析:
首先確定函數(shù)的類型,即n!?最終的計算結(jié)果應(yīng)該為什么類型。顯然,int型的長度不夠,這里我們選擇long型,根據(jù)需要也可以選擇double型。然后確定形參類型及個數(shù),完成本函數(shù)我們只需已知n即可,因此,形參為int型變量n。最后確定函數(shù)名為fac。
程序如下:
longfac(intn)
{
inti;
longa=1;
for(i=2;i<=n;i++)
a*=i;
return(a);
}
【例6.3】編寫函數(shù),打印邊長為n的空心正方形。
程序如下:
voidprn(intn)
{
inti,j;
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
if(i==0||i==n-1)
printf(“*”);
elseif(j==0||j==n-1)
printf(“*”);
else
printf(“”);
printf(“\n”);
}
}6.1.2函數(shù)調(diào)用
一個程序的執(zhí)行總是從main函數(shù)開始的,在main函數(shù)內(nèi)部逐條語句順序執(zhí)行,直至main函數(shù)結(jié)束,則整個程序的執(zhí)行也結(jié)束。那什么時候程序才能執(zhí)行到定義在主函數(shù)外部的其他函數(shù)呢?這就要求我們在主函數(shù)內(nèi)部書寫函數(shù)調(diào)用了。
習(xí)慣上我們將包含函數(shù)調(diào)用的函數(shù)稱為主調(diào)函數(shù),而將被調(diào)用的函數(shù)稱為被調(diào)函數(shù)。
函數(shù)調(diào)用的一般形式為
函數(shù)名(實際參數(shù)表)其中:
(1)函數(shù)名為已經(jīng)定義了的被調(diào)函數(shù)的函數(shù)名。
(2)實際參數(shù)簡稱實參,是主調(diào)函數(shù)傳遞給被調(diào)函數(shù)的數(shù)據(jù)。函數(shù)調(diào)用中的實參表與函數(shù)定義中的形參表必須在類型、順序及個數(shù)上一一對應(yīng)。實參可為常量、變量或表達式,發(fā)生調(diào)用時實參必須已經(jīng)有確定的值。
函數(shù)調(diào)用語句應(yīng)該出現(xiàn)在主調(diào)函數(shù)中的什么位置呢?根據(jù)函數(shù)有無返回值,可將函數(shù)調(diào)用語句在主調(diào)函數(shù)中的使用情況分為以下兩種:
(1)
void函數(shù)的調(diào)用不返回任何值,其作用與執(zhí)行一段代碼無異,因此void函數(shù)的調(diào)用獨立成語句,在函數(shù)調(diào)用后緊跟分號。
【例6.4】對例6.1中的無返回值函數(shù)example2調(diào)用的代碼段為
main()
{
/*void函數(shù)的調(diào)用獨立成語句*/
example2(5);
}
(2)非void函數(shù)調(diào)用會產(chǎn)生一個返回值,該值可以存儲在變量中,還可以進行測試、顯示或者用于其他用途。因此非void函數(shù)的調(diào)用與同類型(函數(shù)類型)常量的使用無異,它可以出現(xiàn)在同類型常量能夠出現(xiàn)的所有位置,如表達式、輸出表、實參表中等。如果不需要非void函數(shù)返回的值,也可以將其丟棄。
【例6.5】
對例6.1中的帶參函數(shù)example調(diào)用的代碼段為
main()
{
/*非void函數(shù)的調(diào)用可以出現(xiàn)在同類型常量能夠出現(xiàn)的所有位置*/
printf("\n%d\n",example(5));
}
另外,在C語言中函數(shù)之間允許相互調(diào)用,也允許嵌套調(diào)用。函數(shù)還可以自己調(diào)用自己,稱為遞歸調(diào)用。6.1.3函數(shù)返回
函數(shù)完成指定的功能后就應(yīng)返回到主調(diào)函數(shù)中函數(shù)調(diào)用的下一條語句處執(zhí)行,并不一定要執(zhí)行完函數(shù)體中的所有語句才返回。怎樣使程序在適當(dāng)?shù)臅r候返回到調(diào)用處繼續(xù)執(zhí)行呢?這就要用return語句來實現(xiàn)了。該語句的功能是計算表達式的值,并返回給主調(diào)函數(shù)。
return語句有下列三種書寫形式(第一種用于void型函數(shù),后兩種用于非void型函數(shù)):
return;
return(表達式);
return表達式;
【例6.6】
返回值與函數(shù)類型不一致時的自動轉(zhuǎn)換。
程序如下:
/*求兩個數(shù)中的最大數(shù)*/
intmax(floatx,floaty)
{
floatz;
z=x>y?x:y;
return(z);/*float類型自動轉(zhuǎn)換為int類型*/
}
當(dāng)x=1.5,y=2.6時,雖然z的值計算后為2.6,但最終函數(shù)調(diào)用的結(jié)果會自動轉(zhuǎn)換為2。
在函數(shù)定義中允許有多個return語句,但每次調(diào)用只能有一個return語句被執(zhí)行,因此只能返回一個函數(shù)值。
【例6.7】
函數(shù)定義中包含多條return語句。
程序如下:
/*判斷奇偶數(shù)*/
intfun(intx)
{
if(x%2==0)return(1); /*x為偶數(shù),返回1*/
elsereturn(0);
/*x為奇數(shù),返回0*/
}
在函數(shù)定義中也可以不包含任何返回語句。函數(shù)執(zhí)行完最后一條語句會自動返回到調(diào)用處。對非void函數(shù),返回一個與其類型相同的隨機數(shù),對void函數(shù)則不返回值。
【例6.8】非void函數(shù)的函數(shù)定義中不包含return語句,返回同類型隨機數(shù)。
程序如下:
intfun(intx)
{/*函數(shù)體中不包含返回語句*/
if(x%2==0)x=1;
elsex=0;
}
main()
{
printf("\n%d",fun(5));
}
程序運行的結(jié)果并不是我們期望中的0,而是一個隨機數(shù)。6.1.4函數(shù)聲明
函數(shù)的定義和函數(shù)的調(diào)用可能分別出現(xiàn)在不同的源程序文件中,即使在同一源程序文件,也可能是調(diào)用語句在先,而函數(shù)定義在后。在這種情況下,執(zhí)行到函數(shù)調(diào)用時,編譯器沒有任何關(guān)于被調(diào)函數(shù)的信息:編譯器不知道被調(diào)函數(shù)有多少形參以及形參的類型是什么,也不知道被調(diào)函數(shù)的返回值是什么類型。這使得編譯器只能默認函數(shù)的類型為整型,也無法檢查傳遞給被調(diào)函數(shù)的實參個數(shù)及類型,只能進行默認的實參提升(參見6.2.3節(jié))并期待最好的情況發(fā)生。為了避免定義前調(diào)用的問題,一種方法是使每個函數(shù)的定義都出現(xiàn)在其調(diào)用之前,另一種方法是在調(diào)用前聲明每個函數(shù)。函數(shù)聲明使得編譯器可以先對函數(shù)進行概要瀏覽,而函數(shù)的完整定義以后再給出。
函數(shù)聲明的一般形式為
類型說明符函數(shù)名(類型形參,類型形參,…);
也可省略形參變量名而簡寫為
類型說明符被調(diào)函數(shù)名(類型,類型,…);注意:函數(shù)聲明一般獨立成語句,分號不可省。在C語言中也允許其與同類型的變量或函數(shù)一起聲明,此時的寫法如下:
類型說明符變量名,函數(shù)名1(類型,類型,…),變量名,…,函數(shù)名2(類型,…);
例如:
longa,reverse(long),max(long,long),b;
C語言中規(guī)定在以下幾種情況下可以省去函數(shù)聲明:
(1)如果被調(diào)函數(shù)的返回值是整型或字符型,則可以不對被調(diào)函數(shù)作說明,而直接調(diào)用。這時系統(tǒng)將自動對被調(diào)函數(shù)返回值按整型處理。
(2)當(dāng)被調(diào)函數(shù)的函數(shù)定義出現(xiàn)在主調(diào)函數(shù)之前,在主調(diào)函數(shù)中也可以不對被調(diào)函數(shù)再作說明而直接調(diào)用。
(3)如在所有函數(shù)定義之前,在函數(shù)外預(yù)先說明了各個函數(shù)的類型,則在以后的各主調(diào)函數(shù)中,可不再對被調(diào)函數(shù)作說明。當(dāng)程序中包含大量函數(shù)聲明時,建議使用第三種方法。具體做法是將函數(shù)的說明和一些公用類型(如結(jié)構(gòu)類型)的說明集中起來并單獨放在一個頭文件(.h文件)中,當(dāng)需要在某個源程序文件中調(diào)用這些函數(shù)時,只需在該文件的首部用一條預(yù)處理命令#include包含該頭文件即可。這種方法對函數(shù)的調(diào)用者來說非常方便,而且還可以減少編譯量。注意
定義和聲明是兩個完全不同的概念。定義是指建立被定義的對象,而聲明只是說明某個對象的存在,被聲明的對象必須在其他地方定義,否則這個聲明就是無效的、無意義的。兩者最明顯的區(qū)別就在于定義時系統(tǒng)會為對象分配存儲空間,而聲明時則不分配空間。 6.2函數(shù)調(diào)用
6.2.1函數(shù)的存儲
函數(shù)是C程序的一部分,根據(jù)馮·諾依曼存儲程序的思想,一個程序在執(zhí)行時會整個或部分被裝入內(nèi)存的代碼段,當(dāng)然函數(shù)也在其中,如圖6-1(a)所示。對整個程序進行編譯后,產(chǎn)生一張標(biāo)識符與其內(nèi)存地址的對應(yīng)表,其中包括各函數(shù)名與其入口地址的對應(yīng)記錄,如圖6-1(b)所示。這為之后的函數(shù)調(diào)用提供了基礎(chǔ)。圖6-1程序被裝入內(nèi)存的代碼段6.2.2函數(shù)調(diào)用的執(zhí)行過程
函數(shù)調(diào)用的過程從宏觀上可以用圖6-2表示,在主調(diào)函數(shù)順序執(zhí)行的過程中遇到函數(shù)調(diào)用時,轉(zhuǎn)去被調(diào)函數(shù)的入口地址處順序往下執(zhí)行,當(dāng)執(zhí)行到被調(diào)函數(shù)的最后一條語句或遇到返回語句時,返回主調(diào)函數(shù)中產(chǎn)生本次函數(shù)調(diào)用的地方繼續(xù)順序向下執(zhí)行。
這個過程看似簡單,但事實上系統(tǒng)在背后完成了很多用戶看不到的工作。為了保證在函數(shù)調(diào)用結(jié)束后能精確返回到函數(shù)調(diào)用的下一條語句處繼續(xù)執(zhí)行,每次函數(shù)調(diào)用時系統(tǒng)都要進行保護“現(xiàn)場”的動作,而函數(shù)返回時又要恢復(fù)“現(xiàn)場”,并且必須保證這兩個“現(xiàn)場”的一致性。為了完成上述工作,系統(tǒng)需要維護一個“?!薄1举|(zhì)上是由若干內(nèi)存單元組成的一段空間(靜態(tài)或動態(tài)),它的最大特點就是“先進后出”。這個特點剛好符合函數(shù)調(diào)用與返回的要求,即最先被調(diào)用的最后返回,最后被調(diào)用的最先返回。這一特點在嵌套調(diào)用及遞歸調(diào)用中體現(xiàn)得更為明顯。圖6-2函數(shù)調(diào)用過程
1.保護現(xiàn)場
當(dāng)程序執(zhí)行到函數(shù)調(diào)用時,系統(tǒng)需要首先完成保護現(xiàn)場的任務(wù),即將主調(diào)函數(shù)中當(dāng)前正在執(zhí)行的語句地址和正在使用的臨時變量壓棧。然后為被調(diào)函數(shù)中使用的形參及其內(nèi)部使用的變量動態(tài)分配存儲空間,并將實參值對照寫入形參后壓棧,根據(jù)調(diào)用中的被調(diào)函數(shù)名對照表進行查找,找到后取出被調(diào)函數(shù)入口地址,根據(jù)地址值獲取該函數(shù)的第一條語句并執(zhí)行,至此,就完成了從主調(diào)函數(shù)到被調(diào)函數(shù)的跳轉(zhuǎn)。
2.恢復(fù)現(xiàn)場
當(dāng)需要從被調(diào)函數(shù)返回主調(diào)函數(shù)時,系統(tǒng)又要恢復(fù)現(xiàn)場。此時首先彈出發(fā)生本次函數(shù)調(diào)用時的壓棧內(nèi)容,其中不僅包括返回值,還包括返回地址。將返回值記入主調(diào)函數(shù)中相應(yīng)的變量,根據(jù)返回地址找到下一條語句繼續(xù)執(zhí)行。
由此,我們可以看到,函數(shù)調(diào)用是有開銷的,即每調(diào)用一次函數(shù),系統(tǒng)都必須為形參臨時分配一定的空間,用于實參與形參的參數(shù)傳遞,保存調(diào)用處的現(xiàn)場信息(如返回地址)也需要臨時分配空間,只有當(dāng)函數(shù)返回時才釋放這些空間。因此,雖然C語言不限制函數(shù)嵌套調(diào)用的深度,但實際上其深度是受系統(tǒng)內(nèi)存資源限制的。6.2.3函數(shù)間的數(shù)據(jù)傳遞
對于主調(diào)函數(shù)而言,被調(diào)函數(shù)就像封裝了各種不同功能的黑盒子。主調(diào)函數(shù)并不關(guān)心黑盒子的內(nèi)部結(jié)構(gòu),而只關(guān)心其外部接口的設(shè)置(數(shù)據(jù)類型)。因此,不同函數(shù)間的數(shù)據(jù)傳遞方式是我們研究函數(shù)調(diào)用的關(guān)鍵,下面我們就從參數(shù)傳遞和返回值這兩個方面做進一步的分析。
1.主調(diào)函數(shù)向被調(diào)函數(shù)傳遞數(shù)據(jù)(實參傳遞數(shù)據(jù)給形參)
形參出現(xiàn)在函數(shù)定義中,且只在被調(diào)函數(shù)內(nèi)部有效;實參出現(xiàn)在主調(diào)函數(shù)中,且只在主調(diào)函數(shù)中有效(全局變量除外)。函數(shù)的形參和實參具有以下特點:
(1)形參變量只有在函數(shù)被調(diào)用時才分配內(nèi)存單元,在調(diào)用結(jié)束時,即刻釋放所分配的內(nèi)存單元。因此,形參只在函數(shù)內(nèi)部有效,函數(shù)調(diào)用結(jié)束返回主調(diào)函數(shù)后則不能再使用該形參變量。
(2)實參可以是常量、變量、表達式、函數(shù)調(diào)用等,無論實參是何種類型的量,在進行函數(shù)調(diào)用時,它們都必須具有確定的值,以便把這些值傳送給形參。因此應(yīng)預(yù)先用賦值、輸入等辦法使實參獲得確定值。
(3)實參和形參在數(shù)量、類型、順序上應(yīng)嚴格一致,否則會發(fā)生類型不匹配的錯誤。
在C語言中,實參向形參傳遞數(shù)據(jù)遵循“單向值傳遞”的原則,即實參與形參占用不同的存儲空間,調(diào)用發(fā)生時只是將實參中的值賦給形參變量。在函數(shù)執(zhí)行過程中,對形參的賦值不會影響實參的值。從效果上來說,“單向值傳遞”的結(jié)果就好像是把每個形參初始化成與之匹配的實參的值。
【例6.9】
編寫一個函數(shù),對末尾數(shù)非0的正整數(shù)求它的逆序數(shù),如:reverse(3407)=7043。在主函數(shù)中輸入正整數(shù)。
程序如下:
#include<stdio.h>
voidmain()
{
longa,reverse(long); /*聲明reverse函數(shù)*/
scanf(“%ld”,&a);
printf(“調(diào)用reverse前:a=%ld\n”,a);
printf(“函數(shù)值:%ld\n”,reverse(a)); /*調(diào)用long型reverse函數(shù)*/
printf("調(diào)用reverse后:a=%ld\n",a);
}
longreverse(longn) /*reverse函數(shù)定義*/
{
longk=0;
while(n){
k=k*10+n%10;
n/=10;
}
returnk;
/*返回語句*/
}
程序運行結(jié)果:
37082↙
調(diào)用reverse前:a=37082
函數(shù)值:28073
調(diào)用reverse后:a=37082
說明
調(diào)用reverse函數(shù)時,實參a的值37082傳給形參變量n,在reverse函數(shù)執(zhí)行過程中,形參n值不斷改變,最終成為0,但并沒有使實參a的值隨之改變。形參變量和實參變量是各自獨立的變量,占有不同的存儲空間,在函數(shù)reverse中對形參的更新,只是對形參本身進行,與實參無關(guān),不論形參名與實參名是否相同都不影響實參值。
實參按值傳遞既有利也有弊。因為形參的修改不會影響到相應(yīng)的實參,所以可以把形參作為函數(shù)內(nèi)的變量來使用,這樣可以減少真正需要的變量的數(shù)量。
【例6.10】
計算數(shù)x的n次冪。因為n只是原始指數(shù)的副本,所以可以在函數(shù)體內(nèi)修改它,因此就不需要使用變量i了,簡化后的程序見程序2。
【例6.11】
假設(shè)我們需要一個函數(shù),它把double型的值分解成整數(shù)部分和小數(shù)部分。因為函數(shù)無法返回兩個數(shù),所以可以嘗試把兩個變量傳遞給函數(shù)并且修改它們。
程序如下:
voiddecompose(doublex,longint_part,doublefrac_part)
{
int_part=(long)x;
frac_part=x-int_part;
}
假設(shè)采用下面的方法調(diào)用這個函數(shù):
decompose(3.14159,i,d);在調(diào)用開始,程序把3.141?59賦值給x,把i的值賦值給int_part,而且把d的值賦值給frac_part。然后,decompose函數(shù)內(nèi)的語句把3賦值給int_part,而把.141?59賦值給frac_part,接著函數(shù)返回??上У氖?,變量i和d不會因為賦值給int_part和frac_part而受到影響,所以它們在函數(shù)調(diào)用前后的值是完全一樣的。當(dāng)然我們在學(xué)習(xí)完更多的C語法技巧后,上述問題是完全可以解決的。
C語言允許在實參類型與形參類型不匹配的情況下進行函數(shù)調(diào)用。其數(shù)據(jù)類型轉(zhuǎn)換遵循以下原則:
(1)編譯器在調(diào)用前遇到函數(shù)原型,即函數(shù)定義或函數(shù)聲明在函數(shù)調(diào)用之前。就像使用賦值一樣,每個實參的值被隱式地轉(zhuǎn)換成相應(yīng)形參的類型。例如,如果把int類型的實參傳遞給期望得到double類型數(shù)據(jù)的函數(shù),那么實參會被自動轉(zhuǎn)換成double類型。
(2)編譯器在調(diào)用前沒有遇到函數(shù)原型,即在函數(shù)調(diào)用之前沒有函數(shù)定義或函數(shù)聲明。編譯器執(zhí)行默認的實參提升:①把float類型的實參轉(zhuǎn)換成double類型;②執(zhí)行整值提升,即把char類型和short類型的實參轉(zhuǎn)換成int類型(C99實現(xiàn)了整數(shù)提升)。默認的實參提升可能無法產(chǎn)生期望的結(jié)果。
【例6.12】在如下程序中調(diào)用square函數(shù)時,編譯器沒有遇到原型,所以它不知道square函數(shù)期望有int類型的實參,但是獲得了double類型值,所以square函數(shù)將產(chǎn)生無效的結(jié)果。
程序如下:
#include<stdio.h>
intmain()
{
doublex=3.0;
printf(“Square:%d\n”,square(x)) /*實參為double型*/
return0;
}
intsquare(intn)
/*形參為int型*/
{
returnn*n;
}
2.被調(diào)函數(shù)向主調(diào)函數(shù)傳遞數(shù)據(jù)(返回值)
函數(shù)的返回值是指函數(shù)被調(diào)用之后,執(zhí)行函數(shù)體中的程序段所取得的并最終返回給主調(diào)函數(shù)的值,如調(diào)用正弦函數(shù)取得正弦值,調(diào)用例6.6的max函數(shù)取得最大數(shù)等。對函數(shù)的值(或稱函數(shù)返回值)有以下一些說明:
(1)函數(shù)的值只能通過return語句返回主調(diào)函數(shù)。因此,一個函數(shù)調(diào)用最多只能返回一個值。
(2)返回值的類型和函數(shù)定義中函數(shù)的類型應(yīng)保持一致。如果兩者不一致,則以函數(shù)類型為準,自動進行類型轉(zhuǎn)換。
(3)如返回值為整型,則在函數(shù)定義時可以省去類型說明。
(4)不返回函數(shù)值的函數(shù),可以明確定義為“空類型”,類型說明符為“void”。 6.3函數(shù)的嵌套調(diào)用與遞歸調(diào)用
6.3.1嵌套調(diào)用
C語言中,函數(shù)是獨立的功能模塊,一個函數(shù)既可以被其他函數(shù)調(diào)用(主函數(shù)除外,它只能由系統(tǒng)調(diào)用),同時它也可以調(diào)用其他函數(shù)。這種主函數(shù)調(diào)用函數(shù)A,函數(shù)A調(diào)用函數(shù)B,…,函數(shù)n-1調(diào)用函數(shù)n的調(diào)用關(guān)系,稱為函數(shù)的嵌套調(diào)用。
函數(shù)嵌套調(diào)用中的每次函數(shù)調(diào)用都遵循函數(shù)調(diào)用的執(zhí)行過程,同樣也包括參數(shù)傳遞、向被調(diào)函數(shù)跳轉(zhuǎn)、執(zhí)行和返回這些步驟。函數(shù)嵌套調(diào)用的執(zhí)行過程可用圖6-3簡單描述。圖6-3函數(shù)嵌套調(diào)用的執(zhí)行過程
【例6.13】
輸入4個整數(shù),找出其中最大的數(shù)。用函數(shù)的嵌套調(diào)用來處理。
程序如下:
#include<stdio.h>
voidmain()
{
intmax_4(inta,intb,intc,intd);
inta,b,c,d,max;
printf(“Pleaseenter4intergernumbers:”);
scanf(“%d%d%d%d”,&a,&b,&c,&d);
max=max_4(a,b,c,d); /*主函數(shù)中調(diào)用max_4函數(shù)*/
printf("max=%d\n",max);
}
intmax_4(inta,intb,intc,intd)
{/*返回四個數(shù)中的最大數(shù)*/
intmax_2(inta,intb),m;
m=max_2(a,b); /*max_4函數(shù)中調(diào)用max_2函數(shù)*/
m=max_2(m,c); /*max_4函數(shù)中調(diào)用max_2函數(shù)*/
m=max_2(m,d); /*max_4函數(shù)中調(diào)用max_2函數(shù)*/
return(m);
}
intmax_2(inta,intb) /*max_2函數(shù)定義*/
{/*返回兩個數(shù)中的最大數(shù)*/
if(a>b)returna;
elsereturnb;
}6.3.2遞歸調(diào)用
函數(shù)的遞歸調(diào)用是指從某函數(shù)A的調(diào)用出發(fā),經(jīng)過一次或多次函數(shù)調(diào)用后又調(diào)用到函數(shù)A,即函數(shù)調(diào)用了自身。函數(shù)的遞歸調(diào)用有兩種形式:直接遞歸調(diào)用和間接遞歸調(diào)用。直接遞歸調(diào)用是指在一個函數(shù)體內(nèi)直接調(diào)用該函數(shù)本身。間接遞歸調(diào)用是指在一個函數(shù)A的函數(shù)體內(nèi)調(diào)用另一個函數(shù)B,再從函數(shù)B的函數(shù)體內(nèi)出發(fā),經(jīng)過一次或多次調(diào)用,又調(diào)用到函數(shù)A。
利用遞歸函數(shù)我們很容易求解結(jié)構(gòu)自相似性的問題。所謂結(jié)構(gòu)自相似性,是指對象的子部分與對象本身在結(jié)構(gòu)上相似。這里的對象可以指數(shù)據(jù)結(jié)構(gòu),也可以指計算過程或操作過程,比如,一些遞歸定義的數(shù)據(jù)結(jié)構(gòu)(如鏈表、二叉樹等),還有數(shù)學(xué)中存在的具有自相似性的計算過程(n!、)。
【例6.14】編寫遞歸函數(shù)求n!,n≥0。分析:函數(shù)頭的寫法與例6.2完全一樣,這里不再贅述。上述公式可看做是分段函數(shù),因此很自然想到用if-else結(jié)構(gòu)構(gòu)建函數(shù)體,根據(jù)公式可直接寫出遞歸函數(shù)。程序如下:
longfac(intn)
{
longresult;
/*定義double類型變量result保存結(jié)果值*/
if(n==0)
result=1;
/*存在已知解的情況*/
else
result=n*fac(n-1);/*問題的自相似分解*/
returnresult;
}
main()
{
intn;
longy;
printf(“\ninputainteagernumber:\n”);
scanf(“%d”,&n);
y=fac(n);
printf("%d!=%ld",n,y);
}由于函數(shù)每調(diào)用一次都需要占用一定數(shù)量的內(nèi)存資源,因此,遞歸的次數(shù)(即遞歸深度)不能無限制,否則會因為系統(tǒng)資源耗盡而導(dǎo)致程序終止。為了控制遞歸深度,在任何一個遞歸函數(shù)體內(nèi),都必須有能使遞歸終止的語句,即包含一個遞歸終止條件的判斷語句。如果終止條件成立,則返回計算結(jié)果;否則繼續(xù)遞歸,并且遞歸的方向應(yīng)該是朝著存在已知解的方向。因此,在遞歸函數(shù)的設(shè)計過程中我們應(yīng)把握兩點:
(1)問題的自相似分解:將問題分解為類型相同且規(guī)模不斷縮小的子問題并最終分解成存在已知解的子問題。
(2)終止條件:滿足終止條件時存在已知解。
【例6.15】兔子繁殖問題。1202年,意大利數(shù)學(xué)家斐波那契提出了“兔子繁殖問題”:假設(shè)一對成熟的兔子每月能生一對小兔(一雌一雄),小兔一個月后長為成熟的兔子,假定兔子不會死亡,那么由一對剛出生的小兔開始,n個月后會有多少對兔子?請用遞歸函數(shù)編程求解。
分析:
問題的自相似分解:第n個月后兔子數(shù)怎么得到呢?顯然,應(yīng)該等于第n?-?1個月的兔子數(shù)再加上第n個月新生的兔子數(shù),而第n個月新生的兔子數(shù)應(yīng)該等于第n?-?2個月的兔子數(shù),即f(n)?=?f(n?-?1)?+?f(n?-?2)。
終止條件:顯然,第一個月后兔子數(shù)為1對,第二個月后兔子數(shù)為2對。因此得到公式:根據(jù)公式不難得到如下遞歸函數(shù):
longf(intn)
{
longresult;
if(n==1)
result=1; /*存在已知解的情況*/
elseif(n==2)
result=2; /*存在已知解的情況*/
else
result=f(n-1)+f(n-2); /*問題的自相似分解*/
returnresult;
}
【例6.16】Hanoi塔問題。一塊板上有三根針A、B、C。A針上套有64個大小不等的圓盤,大的在下,小的在上。要把這64個圓盤從A針移到C針上,每次只能移動一個圓盤,移動可以借助B針進行。但在任何時候,任何針上的圓盤都必須保持大盤在下,小盤在上。求移動的步驟。
分析:
設(shè)A上有n個盤子。
(1)如果n=1,則將圓盤從A直接移動到C。
(2)如果n=2,則:
①將A上的n?-?1(等于1)個圓盤移到B上;
②將A上的1個圓盤移到C上;
③將B上的n?-?1(等于1)個圓盤移到C上。
(3)如果n=3,則:
①將A上的n?-?1(等于2,令其為n')個圓盤移到B(借助于C),步驟如下:
a.將A上的n'?-?1(等于1)個圓盤移到C上。
b.將A上的1個圓盤移到B。
c.將C上的n'?-?1(等于1)個圓盤移到B。
②將A上的1個圓盤移到C。
③將B上的n?-?1(等于2,令其為n')個圓盤移到C(借助A),步驟如下:
a.將B上的n'?-?1(等于1)個圓盤移到A。
b.將B上的1個圓盤移到C。
c.將A上的n'?-?1(等于1)個圓盤移到C。至此,完成了3個圓盤的移動過程。
從上面的分析可以看出,當(dāng)n大于等于2時,移動的過程可分解為三個步驟:
第一步:把A上的n?-?1個圓盤移到B上;
第二步:把A上的1個圓盤移到C上;
第三步:把B上的n?-?1個圓盤移到C上。
其中第一步和第三步是類同的。當(dāng)n=3時,第一步和第三步又分解為類同的三步,即把n‘?-?1個圓盤從一個針移到另一個針上,這里的n’?=?n?-?1。顯然,這是一個遞歸過程,據(jù)此算法可編程如下:
move(intn,intx,inty,intz)
{
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()
{
inth;
printf(“\ninputnumber:\n”);
scanf(“%d”,&h);
printf(“thesteptomoving%2ddiskes:\n”,h);
move(h,'a','b','c');
}從程序中可以看出,move函數(shù)是一個遞歸函數(shù),它有四個形參n、x、y、z。n表示圓盤數(shù),x、y、z分別表示三根針。move函數(shù)的功能是把x上的n個圓盤移動到z上。當(dāng)n==1時,直接把x上的圓盤移至z上,輸出x→z。如n!=1,則分為三步:遞歸調(diào)用move函數(shù),把n-1個圓盤從x移到y(tǒng);輸出x→z;遞歸調(diào)用move函數(shù),把n?-?1個圓盤從y移到z。在遞歸調(diào)用過程中,n=n-1,故n的值逐次遞減,最后n=1時,終止遞歸,逐層返回。當(dāng)n=4時程序運行結(jié)果如下:
inputnumber:
4
thesteptomoving4diskes:
a→b
a→c
b→c
a→b
c→a
c→b
a→b
a→c
b→c
b→a
c→a
b→c
a→b
a→c
b→c 6.4函數(shù)分解
在程序設(shè)計中使用函數(shù)可以為我們帶來諸多好處,不僅能夠避免重復(fù)代碼,使程序結(jié)構(gòu)清晰,提高程序可讀性及可維護性,還能更有效地累積常用代碼,提高代碼復(fù)用性及編程效率。
那么,是不是一個程序中包含的函數(shù)越多越好呢?解決具體問題時,什么情況下需要編寫函數(shù),怎么完成函數(shù)設(shè)計,怎么調(diào)用呢?本節(jié)我們將通過實例來對函數(shù)設(shè)計方法進行深入探討。處理復(fù)雜問題的基本方法就是設(shè)法將其分解為一些相對簡單的子問題,分別處理這些子問題,然后用各子問題的解去構(gòu)造整個問題的解。其分解的實質(zhì)即為功能分解,對C語言而言主要是函數(shù)分解。也就是說,應(yīng)該把程序?qū)懗梢唤M函數(shù),通過其互相調(diào)用來完成所需工作。
什么樣的程序片段應(yīng)該定義為函數(shù)呢?沒有萬能的準則,需要程序設(shè)計人員認真分析需求,總結(jié)經(jīng)驗。這里給出兩條建議,供讀者參考:
(1)程序中重復(fù)出現(xiàn)的相同或相似的片段。將這樣的代碼段提取出來定義成函數(shù)不但可以縮短程序的代碼長度,還可同時提高程序的可讀性及可維護性。
(2)程序中具有邏輯獨立性的片段。即使這種片段只出現(xiàn)一次,也可將其定義為獨立的函數(shù),這樣做可以分解程序的復(fù)雜性,方便程序設(shè)計人員理清設(shè)計思路。
“分解”可以說是程序設(shè)計的精髓,一個程序可能有多種可行的分解方案,很難說哪種分解是程序的最佳分解,但可以肯定的是無論多么復(fù)雜的問題,經(jīng)過一系列合理的分解后最終的各個子問題都是容易求解并用程序?qū)崿F(xiàn)的。函數(shù)就是對應(yīng)于這些子問題的程序解。在函數(shù)的使用者和函數(shù)的設(shè)計者眼中同一個函數(shù)有著完全不同的兩個方面:函數(shù)的使用者只關(guān)心函數(shù)的使用問題,即函數(shù)實現(xiàn)什么功能,應(yīng)該在什么時候調(diào)用及怎樣書寫函數(shù)調(diào)用,對函數(shù)的返回值如何處理等;函數(shù)的設(shè)計者只關(guān)心函數(shù)的實現(xiàn)問題,即采用什么算法,采用什么語句實現(xiàn),怎樣得到計算結(jié)果等。
函數(shù)的封裝性把函數(shù)的內(nèi)部和外部完全分開,而只通過函數(shù)頭完成內(nèi)部與外部的數(shù)據(jù)通信。實際上函數(shù)頭定義了函數(shù)內(nèi)部和外部都需要遵守的共同規(guī)范。這恰恰為我們設(shè)計大規(guī)模程序提供了方便,即設(shè)計初期我們可以站在函數(shù)使用者的角度,忽略函數(shù)的內(nèi)部細節(jié),而只考慮函數(shù)的功能及接口設(shè)計,到了設(shè)計后期則站在函數(shù)設(shè)計者的角度具體實現(xiàn)各個函數(shù),逐個調(diào)試,最后再利用函數(shù)調(diào)用將各個函數(shù)按照一定的邏輯組合進行調(diào)試并得到最終結(jié)果。
由此,函數(shù)分解被引入到程序設(shè)計的步驟中,而且其重要性日益突顯,成為程序設(shè)計人員的首要工作。面對具體問題時,首先必須在問題分析的基礎(chǔ)上完成有效的函數(shù)分解,設(shè)計好各函數(shù)接口(函數(shù)頭),即規(guī)定好公共規(guī)范。此后,就可以逐個完成函數(shù)設(shè)計工作了,當(dāng)然也可以分工合作完成,可以讓不同的人完成不同函數(shù)的設(shè)計工作,只要他們都能遵循共同規(guī)范并對函數(shù)的功能有共同理解即可。圖6-4簡要體現(xiàn)了函數(shù)分解在整個程序設(shè)計過程中的位置。圖6-4程序設(shè)計過程示意圖
【例6.17】
用弦截法求方程f(x)=x3-5x2+16x-80=0的根。要求:給出分解方案并闡明分析過程。
弦截法求方程根的基本思想是:設(shè)被求根的函數(shù)是f(x),而且給定了一個區(qū)間?[x1,x2]。給定區(qū)間的兩端點x1和x2,我們可以作一條弦使之通過函數(shù)圖形上的兩個端點(x1,f(x1))和(x2,f(x2)),如圖6-5所示。如果值f(x1)和f(x2)異號,上述弦必定與x軸有一個交點。交點的x坐標(biāo)可用下列公式求出:圖6-5弦截法求方程根示意圖首先考慮定義幾個函數(shù):
(1)f(x)的計算是獨立的邏輯實體,并且在整個求解過程中很多地方都要計算f(x)的值;
(2)求弦與x軸的交點可看做獨立的邏輯實體,但考慮程序的執(zhí)行效率問題,也可不將它抽取出來設(shè)計成函數(shù);
(3)弦截法求根的過程也可定義為一個函數(shù),這樣該函數(shù)就可用在任何程序中,只要提供了具體的需要求根的函數(shù)f(x)和求根區(qū)間就可得到所需結(jié)果;
(4)主函數(shù)完成輸入、函數(shù)調(diào)用及輸出等功能。
綜合上述考慮,我們應(yīng)該定義3個函數(shù):主函數(shù)、f(x)函數(shù)及弦截法求根函數(shù)。設(shè)計f(x)函數(shù)可以避免程序中重復(fù)出現(xiàn)的相同代碼;設(shè)計求根函數(shù)不僅可以使程序結(jié)構(gòu)清晰,而且還可以為日后的程序設(shè)計增加儲備代碼,提高代碼復(fù)用性。程序如下:
#include“math.h”
main()
{
floatx1,x2,f1,f2,x;
floatf(floatx);
floatroot(floatx1,floatx2,floaty1,floaty2); /*函數(shù)聲明*/
do{
printf(“inputx1,x2:”);
scanf(“%f,%f”,&x1,&x2);
f1=f(x1);f2=f(x2);
}while(f1*f2>=0);
/*找到使f(x1)、f(x2)異號的兩點*/
x=root(x1,x2,f1,f2);
/*調(diào)用root函數(shù)求方程根*/
printf("rootis%8.4f\n",x);
}
float
root(floatx1,floatx2,floaty1,floaty2)
/*函數(shù)定義:弦截法求方程根*/
{
floatx,y;
floatf(floatx); /*函數(shù)聲明*/
do{
x=(x1*y2-x2*y1)/(y2-y1); /*求弦與x軸的交點*/
y=f(x); /*root函數(shù)中調(diào)用函數(shù)f*/
if(y*y1>0){y1=y;x1=x;}
else{y2=y;x2=x;} /*用x替代x1或x2,保證f(x1)和f(x2)異號*/
}while(fabs(y)>=0.000001);
return(x);
}
floatf(floatx) /*函數(shù)定義:求f*/
{
floaty;
y=x*(x*(x-5.0)+16.0)-80.0;
return(y);
}
6.5變量的存儲類別與作用域
變量的作用域是指變量的有效使用范圍,這個范圍指的是源程序里的一段代碼范圍,可在源程序中明確指出,它是一個靜態(tài)概念。作用域的邊界有三種:程序塊、函數(shù)、文件。C語言中的變量,按作用域范圍可分為局部變量和全局變量。局部變量是指在函數(shù)內(nèi)部定義的變量,它僅在函數(shù)內(nèi)部有效。全局變量是指在函數(shù)外部定義的變量,它不屬于哪一個函數(shù),而屬于一個源程序文件。
變量的生存期是指變量占用的內(nèi)存空間從分配到釋放的這段時間,它是一個動態(tài)概念。C語言中的變量按生存期的不同,可以分為靜態(tài)存儲變量和動態(tài)存儲變量。變量根據(jù)其作用域和生命周期不同而分為不同的存儲類別。C語言為我們提供了四種存儲類別說明符:auto、static、extern、register。
在C語言中,每個變量都有兩個屬性:變量類型和變量的存儲類別。變量類型決定了系統(tǒng)為變量分配的內(nèi)存單元的長度、數(shù)據(jù)的存放形式等,而變量的存儲類別決定了何時及在內(nèi)存的什么位置為變量分配存儲空間,何時釋放已分配的存儲空間。變量說明的一般形式為
[存儲類別說明符]類型說明符變量名列表;
其中,存儲類別說明符可缺省,缺省時默認為auto類型。存儲類別說明符出現(xiàn)在類型說明符的左側(cè)或者右側(cè)均合法。例如,“intautoi;”和“autointi;”兩條語句是等價的。用戶存儲空間可以分為三個部分:程序代碼區(qū)、靜態(tài)存儲區(qū)和動態(tài)存儲區(qū),如圖6-6所示。
全局變量全部存放在靜態(tài)存儲區(qū),在程序開始執(zhí)行時給全局變量分配存儲區(qū),程序執(zhí)行完畢就釋放。在程序執(zhí)行過程中它們占據(jù)固定的存儲單元,而不動態(tài)地進行分配和釋放。
動態(tài)存儲區(qū)存放以下數(shù)據(jù):
(1)函數(shù)的形式參數(shù);
(2)自動變量(未加static聲明的局部變量);
(3)函數(shù)調(diào)用時的現(xiàn)場保護和返回地址。
對以上這些數(shù)據(jù),在函數(shù)開始調(diào)用時分配動態(tài)存儲空間,函數(shù)結(jié)束時釋放這些空間。圖6-6用戶存儲空間劃分示意圖6.5.1自動變量(auto)
函數(shù)中的局部變量如不專門聲明為static存儲類別,則動態(tài)地分配存儲空間,數(shù)據(jù)存儲在動態(tài)存儲區(qū)中。函數(shù)中的形參和在函數(shù)中定義的變量(包括在復(fù)合語句中定義的變量)都屬此類。在調(diào)用該函數(shù)時系統(tǒng)會給它們分配存儲空間,在函數(shù)調(diào)用結(jié)束時就自動釋放這些存儲空間,這類局部變量稱為自動變量。
自動變量實質(zhì)上是一個函數(shù)內(nèi)部的局部變量,只有在函數(shù)被調(diào)用時才存在,從函數(shù)中返回時即消失,其作用域僅限于說明它的函數(shù),在其他函數(shù)中不能存取。由于自動變量具有局部性,所以在兩個不同的函數(shù)中可以分別使用同名的變量而互不影響。
自動變量用關(guān)鍵字auto作存儲類別的聲明,關(guān)鍵字auto可以省略,auto不寫則隱含定義為“自動存儲類別”,屬于動態(tài)存儲方式。
【例6.18】用auto聲明自動變量。
intf(inta) /*定義f函數(shù),a為參數(shù)*/
{
autointb,c=3; /*定義b,c自動變量*/
…
}
a是形參,b、c是自動變量,對c賦初值3。執(zhí)行完f函數(shù)后,自動釋放a、b、c所占的存儲單元。6.5.2外部變量(extern)
外部變量(即全局變量)是在函數(shù)的外部定義的,它的作用域為從變量定義處開始,到本程序文件的末尾。
如果外部變量不在文件的開頭定義,則其有效的作用范圍只限于定義處到文件結(jié)束。如果在定義點之前的函數(shù)想引用該外部變量,則應(yīng)該在引用之前用關(guān)鍵字extern對該變量作“外部變量聲明”,表示該變量是一個已經(jīng)定義的外部變量。有了此聲明,就可以從“聲明”處起,合法地使用該外部變量。因此,在不同函數(shù)之間也可以用外部變量傳遞數(shù)據(jù)。
【例6.19】用extern聲明外部變量,擴展程序文件中的作用域。
程序如下:
intmax(intx,inty)
{
intz;
z=x>y?x:y;
return(z);
}
main()
{
externA,B; /*聲明外部變量A和B,省略類型說明符時默認為int類型*/
printf("%d\n",max(A,B));
}
intA=13,B=-8; /*定義A和B全局變量*/外部變量與自動變量的區(qū)別如下:
(1)外部變量在編譯時由系統(tǒng)分配永久性的存儲空間;自動變量則是在函數(shù)被調(diào)用時才分配臨時性的存儲空間。
(2)外部變量如果沒有明確賦初值,則初值為0;自動變量如果沒有明確賦初值,則其值不定,是隨機數(shù)。對于大系統(tǒng)而言,可將一個程序分割為多個文件,通過工程文件,將整個系統(tǒng)連為一個整體。此時,外部變量的說明與使用它的函數(shù)就不一定在同一個文件中了。如果外部變量的說明與使用在同一個文件中,則該文件中的函數(shù)在使用外部變量時,不需要再進行說明,可直接使用。當(dāng)外部變量的說明與使用在不同的文件時,要使用在其他文件中說明的外部變量,就必須在使用該外部變量之前,使用“extern”存儲類型說明符對變量進行“外部”說明。注意
extern僅僅說明變量是“外部的”,以及它的類型,并不真正分配存儲空間。在將若干個文件連接生成一個完整的可運行程序時,系統(tǒng)會將不同文件中使用的同一外部變量連在一起,使用同一個系統(tǒng)分配的存儲單元。
另外,對于函數(shù)也是如此,如果被調(diào)用的函數(shù)在另一個文件中,則在調(diào)用該函數(shù)時,無論被調(diào)用的函數(shù)是什么類型,都必須用extern說明符說明被調(diào)用函數(shù)是“外部函數(shù)”。6.5.3靜態(tài)變量(static)
靜態(tài)變量的說明是在變量說明前加static。靜態(tài)變量又分為外部靜態(tài)變量和內(nèi)部靜態(tài)變量。
有時我們希望函數(shù)中局部變量的值在函數(shù)調(diào)用結(jié)束后不消失而保留原值,這時就應(yīng)該指定局部變量為“內(nèi)部靜態(tài)變量”。內(nèi)部靜態(tài)變量的作用域與局部變量一樣,僅限在定義它的函數(shù)內(nèi)部有效。
【例6.20】
考察內(nèi)部靜態(tài)變量的值。
程序如下:
f(inta)
{
autob=0; /*聲明自動變量b,省略類型說明符時默認為int類型*/
staticc=3; /*聲明內(nèi)部靜態(tài)變量c,省略類型說明符時默認為int類型*/
b=b+1;
c=c+1;
return(a+b+c);
}
main()
{
inta=2,i;
for(i=0;i<3;i++)
printf("%d",f(a));
}執(zhí)行上述程序時:第一次調(diào)用函數(shù)f后b=1,c=4,輸出7;第二次調(diào)用f后b=1,c=5,輸出8;第三次調(diào)用f后b=1,c=6,輸出9。由此可見,內(nèi)部靜態(tài)變量的值在函數(shù)調(diào)用結(jié)束后不消失而保留原值。
對內(nèi)部靜態(tài)變量的幾點說明:
(1)內(nèi)部靜態(tài)變量屬于靜態(tài)存儲類別,在靜態(tài)存儲區(qū)內(nèi)分配存儲單元,在程序整個運行期間都不釋放;自動變量(即動態(tài)局部變量)屬于動態(tài)存儲類別,占動態(tài)存儲空間,函數(shù)調(diào)用結(jié)束后即釋放。
(2)內(nèi)部靜態(tài)變量在編譯時賦初值,即只賦初值一次;對自動變量賦初值是在函數(shù)調(diào)用時進行,每調(diào)用一次函數(shù)重新賦一次初值,相當(dāng)于執(zhí)行一次賦值語句。
(3)對內(nèi)部靜態(tài)變量來說,如果在定義局部變量時不賦初值,編譯時自動賦初值0(對數(shù)值型變量)或空字符(對字符變量);對自動變量來說,如果不賦初值,則它的值是一個不確定的值。
【例6.21】考察外部靜態(tài)變量的值。
程序如下:
/*文件一*/
staticintx=2; /*說明外部靜態(tài)變量x*/
inty=3; /*說明外部變量y*/
externvoidadd2(); /*說明外部函數(shù)add2*/
voidadd1();
main()
{
add1();
add2();
add1();
add2();
printf("x=%d;y=%d\n",x,y);
}
voidadd1(void) /*定義函數(shù)add1*/
{
x+=2;y+=3;printf(“inadd1x=%d\n”,x);
}
/*文件二*/
staticintx=10; /*說明外部靜態(tài)變量x*/
voidadd2(void) /*定義函數(shù)add2*/
{
externinty;
/*說明另一個文件中的外部變量y*/
x+=10;y+=2;
printf("inadd2x=%d\n",x);
}對外部靜態(tài)變量的說明:
(1)靜態(tài)變量與外部變量的相同點:具有永久的存儲空間,由編譯器進行初始化。
(2)外部靜態(tài)變量與外部變量的區(qū)別:外部靜態(tài)變量僅僅在定義它的一個文件中有效,而外部變量作用于整個程序。
(3)內(nèi)部靜態(tài)變量與外部靜態(tài)變量的區(qū)別:內(nèi)部靜態(tài)變量作用于定義它的當(dāng)前函數(shù),外部靜態(tài)變量可以作用于程序中的所有函數(shù)。
(4)內(nèi)部靜態(tài)變量與自動變量的區(qū)別:內(nèi)部靜態(tài)變量占用永久性的存儲單元,在每次調(diào)用的過程中能夠保持數(shù)據(jù)的連續(xù)性,而自動變量不能。6.5.4寄存器變量(register)
為了提高效率,C語言允許將局部變量的值放在CPU中的寄存器中,這種變量叫寄存器變量,用關(guān)鍵字register作聲明。
計算機從寄存器中存取數(shù)據(jù)的速度要遠遠快于從內(nèi)存中存取數(shù)據(jù),所以當(dāng)變量使用非常頻繁時,將變量定義為寄存器變量可以提高程序運行速度。由于一個計算機系統(tǒng)中的寄存器數(shù)目有限,因此不能定義任意多個寄存器變量。寄存器是與機器硬件密切相關(guān)的。不同的計算機,寄存器的數(shù)目不一樣,通常為2到3個。若在一個函數(shù)中說明多于2到3個寄存器變量,編譯程序會自動地將它們變?yōu)樽詣幼兞俊?/p>
【例6.22】使用寄存器變量。
程序如下:
intfac(intn)
{
registerinti,f=1; /*聲明寄存器變量i,f*/
for(i=1;i<=n;i++)
f=f*i
return(f);
}
main()
{
inti;
for(i=0;i<=5;i++)
printf(“%d!=%d\n”,i,fac(i));
}
注意
(1)寄存器說明符只能用于說明函數(shù)中的變量和函數(shù)中的形式參數(shù),外部變量或靜態(tài)變量不能是register。
(2)由于受硬件寄存器長度的限制,所以寄存器變量只能是char、int或指針型。
(3)寄存器變量的作用域僅限在定義它的函數(shù)內(nèi)部。
6.6程序結(jié)構(gòu)
6.6.1單文件程序結(jié)構(gòu)
迄今為止,已經(jīng)知道程序可以包含:
(1)預(yù)處理指令,如:#include、#define;
(2)類型定義;
(3)外部變量聲明;
(4)函數(shù)聲明;
(5)函數(shù)定義。
C語言對上述這些項的順序要求極少:執(zhí)行到預(yù)處理指令所在的代碼行時,預(yù)處理指令才會起作用;類型名定義后才可以使用;變量聲明后才可以使用。雖然C語言對函數(shù)沒有什么要求,但是建議在第一次調(diào)用函數(shù)前要對每個函數(shù)進行定義或聲明。
為了遵守這些規(guī)則,一般按照如下的編排順序組織程序:
(1)#include指令;
(2)#define指令;
(3)類型定義;
(4)外部變量的聲明;
(5)除main函數(shù)之外的函數(shù)的原型;
(6)main函數(shù)的定義;
(7)其他函數(shù)的定義。因為?#include指令帶來的信息可能在程序中的好幾個地方都需要,所以先放置這條指令是合理的。#define指令創(chuàng)建宏,對這些宏的使用通常遍布整個程序。類型定義放置在外部變量聲明的前面是合乎邏輯的,因為這些外部變量的聲明可能會引用剛剛定義的類型名。接下來,聲明外部變量使得它們對于跟隨在其后的所有函數(shù)都是可用的。在編譯器編譯原型之前調(diào)用函數(shù),可能會產(chǎn)生問題,而此時聲明除了main函數(shù)以外的所有函數(shù)可以避免這些問題。這種方法也使得無論用什么順序編排函數(shù)定義都是可能的。例如,根據(jù)函數(shù)名的字母順序編排,或者把相關(guān)函數(shù)組合在一起進行編排。在其他函數(shù)前定義main函數(shù)可使得閱讀程序的人容易定位程序的起始點。
因此,在每個函數(shù)定義前可使用注釋給出函數(shù)名,描述函數(shù)的目的,討論每個形式參數(shù)的含義,描述返回值并羅列所有的副作用(如修改外部變量的值)。6.6.2多文件程序結(jié)構(gòu)
雖然我們前面提到的程序都是放在一個單獨的文件里,但在實際問題中大多數(shù)程序都是由若干個源文件和若干個頭文件構(gòu)成的。源文件包含函數(shù)的定義和外部變量。一個源文件必須包含一個main函數(shù),其擴展名為?.c;頭文件包含可以在源文件之間共享的信息,其擴展名為.h。多文件程序的編譯過程可用圖6-7表示。圖6-7多文件程序結(jié)構(gòu)圖6-7中,源文件中含有包含頭文件的預(yù)編譯語句,經(jīng)過預(yù)編譯后,產(chǎn)生翻譯單元,該翻譯單元以臨時文件的形式存放在計算機中。之后在編譯過程中進行語法檢查,產(chǎn)生目標(biāo)文件(.obj)。最后將若干個目標(biāo)文件及C標(biāo)準庫函數(shù)連接,產(chǎn)生可執(zhí)行文件(.exe)。
小程序可以由單個源文件建立,而大規(guī)模程序則傾向于分成多個源文件。把程序分成多個源文件的優(yōu)點如下:
(1)可以單獨對每一個源文件進行編譯,避免一而再、再而三地重復(fù)編譯沒有修改的函數(shù)。編譯器總是以文件為單位工作的,如果一個文件中包含的函數(shù)太多,則由于被修改的函數(shù)總是少數(shù)的幾個,大多數(shù)正確的函數(shù)都得重新編譯一次。
(2)把相關(guān)的函數(shù)和變量放到同一個特定的源文件中有助于使程序結(jié)構(gòu)清晰明了,更使程序容易管理,可以將程序按邏輯功能劃分,分解成各個源文件,便于程序員的任務(wù)安排以及程序調(diào)試。
(3)把函數(shù)集合在單獨的源文件中便于在其他程序中重新使用這些函數(shù)。
把程序分割為幾個源文件后,隨之也產(chǎn)生了一些問題:如何調(diào)用在其他文件中定義的函數(shù)?如何訪問其他文件中定義的外部變量?兩個文件如何共享同一個宏定義或類型定義?這時就要用到?#include命令,該命令使得在任意數(shù)量的源文件中共享信息成為可能。
#include命令告訴預(yù)處理器打開指定的文件,并且把此文件的內(nèi)容插入到當(dāng)前文件中。因此,可以將需要在幾個源文件中共享的程序代碼放入一個文件(頭文件)中,需要使用這些代碼時再用#include命令將其插入不同的源文件。頭文件一般可以包含:
(1)類型聲明,如“enumCOLOR{//…}”。
(2)函數(shù)聲明,如“externintfn(chars);”。
(3)內(nèi)聯(lián)函數(shù)定義,如“inlinecharfn(charp){return*p++;}”。
(4)常量定義,如“constfloatpi=3.14;”。
(5)數(shù)據(jù)聲明,如“externintm;externinta[];”。
(6)枚舉,如“enumB00LEAN{false,true};”。
(7)包含指令(可嵌套),如“#include<iostream.H>”。
(8)宏定義,如“#defineCasebreak;case”。
(9)注釋,如“/*checkforendoffile*/”。但頭文件不宜包含:
(1)一般函數(shù)定義,如“charfn(charp){return*p++;}”。
(2)數(shù)據(jù)定義,如“inta;intb[5];”。
(3)常量聚集定義,如“constintc[]={1,2,3};”。當(dāng)我們已經(jīng)設(shè)計好各個函數(shù),并將各個函數(shù)按照一定的規(guī)則進行了分組時,就可以考慮構(gòu)建多文件程序了。首先,將每組函數(shù)集合放入單獨的源文件(比如list.c)中,創(chuàng)建和源文件同名的頭文件(list.h)并在其中放置源文件中各個函數(shù)的原型。然后,在每個需要調(diào)用list.c中所定義的函數(shù)的源文件中包含頭文件list.h,注意list.c中也應(yīng)包含list.h。最后,main函數(shù)應(yīng)出現(xiàn)在與程序名字相同的源文件中。如程序名為search,則main函數(shù)就應(yīng)在search.c文件中。
6.7庫函數(shù)
6.7.1靜態(tài)鏈接庫
事實上,在前面的許多例子中,我們已經(jīng)大量調(diào)用了C語言編譯系統(tǒng)提供的庫函數(shù),如輸入/輸出庫函數(shù)(printf、scanf)和數(shù)學(xué)庫函數(shù)(sqrt、abs)等。當(dāng)需要在某源程序文件中使用某庫函數(shù)時,必須在該源程序文件首部用#include命令包含該庫函數(shù)所在的頭文件(可以用文本編譯器打開)。例如,要使用printf函數(shù),就必須增加#include"stdio.h"命令。在stdio.h文件中包括了系統(tǒng)提供的用于標(biāo)準輸入/輸出的全部函數(shù)的說明、這些函數(shù)中用到的符號常量、自定義的數(shù)據(jù)類型等。源程序編譯時,編譯器可以根據(jù)此文件對調(diào)用處函數(shù)的類型、實參的類型及個數(shù)等與庫函數(shù)的定義作一致性檢查。當(dāng)鏈接時,鏈接器可以根據(jù)調(diào)用的函數(shù)名找到相應(yīng)的庫文件,并把它和用戶編寫的程序鏈接到一起,形成一個完整的可執(zhí)行程序。自己編寫好的一個實用程序,也可以用庫函數(shù)的方式提供給其他用戶調(diào)用。一般做法是:首先把自己寫的函數(shù)(假設(shè)存放在libTest.cpp文件中)調(diào)試通過,然后使用庫文件生成器把自己寫的函數(shù)變成庫函數(shù)形式,如libTest.lib,最后編輯一個庫函數(shù)的說明文件即頭文件,如lib.h,把libTest.lib和lib.h一起提供給用戶即可。若用戶欲調(diào)用你寫的庫函數(shù),則必須把這兩個文件復(fù)制到系統(tǒng)中,使其工作目錄中包含lib.h,而且在源文件中必須用預(yù)處理命令?#include"lib.h",鏈接時還必須鏈接libTest.lib庫文件。我們通過以下實例說明建立和調(diào)用靜態(tài)庫的過程。
【例6.23】用靜態(tài)鏈接庫實現(xiàn)一個add函數(shù),并在程序中調(diào)用該靜態(tài)庫。
在VC++?6.0中新建一個名稱為libTest的Staticlibrary工程,如圖6-8所示。圖6-8建立一個靜態(tài)鏈接庫新建lib.h和lib.cpp兩個文件。lib.h和lib.cpp的源代碼如下:編譯這個工程就得到了一個libTest.lib文件,這個文件就是一個函數(shù)庫,它提供了add的功能。將頭文件lib.h和libTest.lib文件提交給用戶后,用戶就可以直接使用其中的add函數(shù)了。常用的標(biāo)準C庫函數(shù)(scanf、printf、abs、sqrt等)就來自這種靜態(tài)庫。
下面來看看怎么使用這個庫。在VC中新建一個名為libCall的Win32ConsoleApplication工程,并將上面生成的頭文件lib.h和libTest.lib文件拷貝到libCall的工程子目錄下。libCall工程僅包含一個main.cpp文件,它演示了靜態(tài)鏈接庫的調(diào)用方法,其源代碼如下:代碼中?#pragmacomment(lib,"libTest.lib")的意思是本文件生成的?.obj文件應(yīng)與libTest.lib一起鏈接。
如果不用?#pragmacomment指定,則可以直接在VC++?中設(shè)置,即依次選擇Tools→Options→Sirectories→Libraryfiles菜單或選項,填入庫文件路徑,如圖6-9所示。圖6-9中加圈的部分為我們添加的libTest.lib文件的路徑。圖6-9在VC中設(shè)置庫文件路徑6.7.2動態(tài)鏈接庫
動態(tài)鏈接是相對于靜態(tài)鏈接而言的。所謂靜態(tài)鏈接,是指把要調(diào)用的函數(shù)或者過程鏈接到可執(zhí)行文件中,成為可執(zhí)行文件的一部分。換句話說,函數(shù)和過程的代碼就在程序的exe文件中,該文件包含了運行時所需的全部代碼。當(dāng)多個程序都調(diào)用相同函數(shù)時,內(nèi)存中就會存在這個函數(shù)的多個拷貝,這樣就浪費了寶貴的內(nèi)存資源。動態(tài)鏈接所調(diào)用的函數(shù)代碼并沒有被拷貝到應(yīng)用程序的可執(zhí)行文件中,而是僅僅在其中加入了所調(diào)用函數(shù)的描述信息(往往是一些重定位信息),僅當(dāng)應(yīng)用程序被裝入內(nèi)存開始運行時,在Windows的管理下,才在應(yīng)用程序與相應(yīng)的dll之間建立鏈接關(guān)系。當(dāng)要執(zhí)行所調(diào)用dll中的函數(shù)時,根據(jù)鏈接產(chǎn)生的重定位信息,Windows才轉(zhuǎn)去執(zhí)行dll中相應(yīng)的函數(shù)代碼。一般情況下,如果一個應(yīng)用程序使用了動態(tài)鏈接庫,Win32系統(tǒng)保證內(nèi)存中只有dll的一份復(fù)制品。采用動態(tài)鏈接庫的優(yōu)點:①更加節(jié)省內(nèi)存;②dll文件與exe文件獨立,只要輸出接口不變,更換dll文件不會對exe文件造成任何影響,因而極大地提高了可維護性和可擴展性。動態(tài)鏈接庫有兩種鏈接方法:
(1)裝載時動態(tài)鏈接(Load-timeDynamicLinking):即靜態(tài)調(diào)用方式。這種方法的前提是在編譯之前已經(jīng)明確知道要調(diào)用dll中的哪幾個函數(shù),編譯時在目標(biāo)文件中只保留必要的鏈接信息,而不含dll函數(shù)的代碼。當(dāng)程序執(zhí)行時,利用鏈接信息加載dll函數(shù)代碼并在內(nèi)存中將其鏈接入調(diào)用程序的執(zhí)行空間中,其主要目的是便于代碼共享。
(2)運行時動態(tài)鏈接(Run-timeDynamicLinking):即動態(tài)調(diào)用方式。這種方法是指在編譯之前并不知道將會調(diào)用哪些dll函數(shù),完全是在運行過程中根據(jù)需要決定應(yīng)調(diào)用哪個函數(shù),并用LoadLibrary和GetProcAddress動態(tài)獲得dll函數(shù)的入口地址。
我們通過下面的實例來學(xué)習(xí)如何創(chuàng)建和使用動態(tài)鏈接庫。
【例6.24】制作一個簡單的dll,用動態(tài)鏈接庫實現(xiàn)一個add函數(shù)。
如圖6-10所示,在VC中新建一個Win32Dynamic-LinkLibrary工程dllTest。注意不要選擇MFCAppWizard[dll],因為這兩種方式創(chuàng)建出來的動態(tài)庫格式不一樣。圖6-10建立一個動態(tài)鏈接庫在建立的工程中添加lib.h及l(fā)ib.cpp文件,源代碼如下:最后點擊Build按鈕(或菜單項Build→BuilddllTest.dll)。創(chuàng)建成功之后在工程所在子目錄的Debug子目錄下就可以找到dllTest.dll文件。另外,我們還可以看到一個dllTest.lib文件,
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 會場設(shè)備租賃合同范本
- 醫(yī)美針劑合同范本
- 創(chuàng)業(yè)課題申報書怎么寫好
- 廠房帶看合同范例
- 午休托管班合同范本
- 廠房排氣安裝合同范本
- 代加工燈具合同范本
- 包辦入學(xué)合同范本
- 單位委托印刷合同范本
- 推動農(nóng)村充電基礎(chǔ)設(shè)施發(fā)展計劃
- 2025年復(fù)工復(fù)產(chǎn)培訓(xùn)考核試卷及答案
- 2025年01月中國疾控中心信息中心公開招聘1人筆試歷年典型考題(歷年真題考點)解題思路附帶答案詳解
- 北京市豐臺區(qū)2024-2025學(xué)年高二上學(xué)期期末英語試題
- 2025年高三第二學(xué)期物理備課組教學(xué)工作計劃
- 丁香園:2024年12月全球新藥月度報告-數(shù)據(jù)篇
- 生產(chǎn)與運作管理-第5版 課件全套 陳志祥 第1-14章 生產(chǎn)系統(tǒng)與生產(chǎn)運作管理概述 -豐田生產(chǎn)方式與精益生產(chǎn)
- 2025年湖南理工職業(yè)技術(shù)學(xué)院高職單招職業(yè)技能測試近5年??及鎱⒖碱}庫含答案解析
- 2024年西安航空職業(yè)技術(shù)學(xué)院高職單招職業(yè)適應(yīng)性測試歷年參考題庫含答案解析
- 臨平區(qū)九年級上學(xué)期期末考試語文試題(PDF版含答案)
- 2024年港作拖輪項目可行性研究報告
- 課題申報書:“四新”建設(shè)與創(chuàng)新創(chuàng)業(yè)人才培養(yǎng)基本范式研究
評論
0/150
提交評論