《程序設計與C語言》課件第5章_第1頁
《程序設計與C語言》課件第5章_第2頁
《程序設計與C語言》課件第5章_第3頁
《程序設計與C語言》課件第5章_第4頁
《程序設計與C語言》課件第5章_第5頁
已閱讀5頁,還剩218頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第5章函數

5.1函數的定義5.2函數的調用5.3函數原型及函數聲明5.4數據存儲類5.5多文件程序中函數和變量的處理5.6遞歸5.7迭代5.8系統(tǒng)庫函數習題55.1函數的定義在C語言中的函數分為兩大類:一類是程序員定義的函數,另一類是C標準庫中預定義的函數。

C標準庫提供了豐富的函數集,這些函數集中的函數能完成常用的數學計算、字符串操作、字符操作、輸入/輸出等多種有用的操作。使用標準庫函數可以節(jié)省程序開發(fā)的時間,使程序具有更好的可移植性,因此應盡量多地熟悉和掌握ANSIC中的標準庫函數。但現實世界是生動復雜的,單用庫函數不可能解決所有問題,程序員還必須根據情況自已定義能解決專門問題的新函數,因此必須掌握函數的定義和使用方法。函數由函數頭和函數體兩部分組成,其格式如下:

〈返回值類型〉〈函數名〉(〈參數列表〉) {聲明部分 語句部分

}第一行為函數頭,其中的函數名可以是任何合法的標識符,它最好能直觀地反映出該函數所完成的任務,以增強程序的可讀性。返回值類型是返回給調用者結果的數據類型。如果不指定返回值類型,編譯器總假定返回的是int類型。即便如此,明顯地寫出返回int類型也是一種良好的習慣。參數列表是用逗號分開的參數說明,參數說明的形式是:

〈參數類型〉〈參數名〉對每一個參數都必須作這樣的說明,不能在一個類型名后跟多個參數名。函數體由大括號{}括起來,一般由兩部分組成:說明部分和語句部分。說明部分聲明用于函數內部的臨時變量。也可以沒有說明部分,只有語句部分。例如,定義求兩個浮點數之和的函數:

floatsum(floatx,floaty) {returnx+y; }函數的兩個參數x、y被定義成浮點型,返回值的類型也是浮點型。該函數體中沒有說明部分,只有語句部分。

【例5-1】編一個求整數位數的函數。intwsh(intn){inti;i=0;while(n){n/=10;i++;}returni;}函數wsh()有一個整型參數n,將返回這個整數n的位數。在該函數體中,i是在本函數內部定義的中間變量。定義函數時系統(tǒng)并不為中間變量分配內存空間,只有在函數被調用時,才為其分配內存,函數調用一結束,該內存即被收回。因此,在函數wsh()的外部不可以對變量i加以引用。函數不一定要有返回值,也可以用來完成某些特定的操作。

【例5-2】編一個函數,它能輸出由字母組成的平行四邊形,即

A

B

C

D B

C

D

E

C

D

E

F D

E

F

G編程思路:把要輸出的行數n作為函數的參數。在輸出的每一行中要先輸出一定數量的空格,然后輸出n個連續(xù)的字母,共輸出n行。每行行首的空格數隨行數而增加。函數定義如下:voidprintnc(intn){inti,j;charc,d;c=d=′A′;for(i=1;i<=n;i++){for(j=1;j<i;j++)putchar(′└┘′);for(j=1;j<=n;j++){putchar(c++); putchar(′└┘′);}putchar(′\n′);c=++d;}}函數的返回值類型void指該函數不返回數據,僅執(zhí)行打印平行四邊形的操作。5.2函數的調用5.2.1函數的參數傳遞定義函數時,在函數頭中出現的參數是形式參數,簡稱形參。函數調用時,主調函數應當根據被調函數形參的要求提供相應的真實數據,這稱為實在參數,簡稱實參。實參和形參要做到個數相等,類型對應一致。盡管實參和形參可以同名,但為了避免混淆,建議不要使用完全相同的實參和形參名。例如,調用上面定義的sum函數。#include<stdio.h>main(){floatf1,f2;scanf(″%f%f″,&f1,&f2);printf(″%f\n″,sum(f1,f2));return0;}這里main函數是主調函數,sum函數是被調函數,f1,f2是實參。main函數把f1,f2分別交給sum的x和y,sum函數開始工作,把x與y(即f1和f2)的和返回給main函數,并作為printf函數的參數打印出來。

C語言中參數傳送的機理是值傳送,即主調函數把實在參數的拷貝傳給形參后即與形參脫離關系,函數體中對形參的任何處理都已和實參無關了。為了說明這一點,請看下面關于交換兩個變量值的函數。

【例5-3】交換兩個變量值的函數。voidexchange(inti,intj){intk;printf(″i=%d,j=%d\n″,i,j);k=i;i=j;j=k;printf(″i=%d,j=%d\n″,i,j);}#include<stdio.h>main(){intm=1,n=10;printf(″m=%d,n=%d\n″,m,n);exchange(m,n);

printf(″m=%d,n=%d\n″,m,n);return0;}運行輸出:m=1,n=10(函數調用前)i=1,j=10 (函數中參數交換前)i=10,j=1 (函數中參數交換后)m=1,n=10 (函數調用后)可以看出,在exchange函數調用前后主函數中的變量m和n的值沒有改變。這說明雖然在函數中發(fā)生了參數的交換,但影響不到主調函數。該程序的函數調用示意圖如圖5-1所示。①k=i;②i=j;③j=k;圖5-1例5-3的調用示意圖調用時,m、n把值傳給i、j后即與之脫離關系,函數中i、j的交換對m、n沒有影響。如何讓函數實現實參的交換呢?在學習了指針類型之后,這個問題就可以解決了。5.2.2函數的返回值函數的返回值是由return語句實現的。對于任何函數,只要它的返回值類型不是void,那么它都要有個返回值。void是空類型,表示函數不返回值。通過return語句,被調函數將控制權及數值交給主調函數,返回到調用處。函數調用過程如圖5-2所示。圖5-2函數調用過程示意圖main() floatsum(floatx,floaty){ { returnx+y;printf(″…″,sum(f1,f2)); }}被調函數返回的數值類型如何決定呢?如果return語句的表達式類型和函數頭部返回值類型不一致時,應該返回哪一個類型呢?答案是應該返回函數頭部說明的類型。比如,若sum函數的定義作如下修改:intsum(floatx,floaty){returnx+y;}這時主調函數中接收的應該是int類型,因此printf函數就應該改成下面的形式:

printf(″%d\n″,sum(f1,f2);即輸出控制符由′%f′改為′%d′。既然類型為非void的函數都要返回值,main也是個函數,那么它返回一個什么值,又返回給誰呢?如果main函數名前面沒有返回類型,則隱含是int類型,所以也應當返回一個整數,不管什么整數都行,它只是表示把控制權返回給調用者,而其具體值并無太大的意義,這就是為什么我們都要在主函數main的最后寫上return0的原因。0表示成功返回。主函數的控制權最后返回到操作系統(tǒng)。5.2.3函數的調用方式按被調函數在主調函數中的位置,可以有以下幾種調用方式。

1.被調函數作為函數語句單獨出現一個函數可看作是一個函數表達式,在其后面加分號即構成函數語句,例如我們經常使用的輸入/輸出函數

printf(″a=%d,b=%d\n″,a,b);就是一個函數調用語句。前面例子中的

exchange(m,n);也是一個函數調用語句。

2.被調函數作為另一個表達式的一部分例如,求兩個數的平均值,可調用函數sum: moyen=sum(m,n)/2;

3.被調函數作為另外一個函數的參數例如,輸出兩個數的和:

printf(″sum=%f\n″,sum(a,b));如輸出四個數a、b、c、d的和,可寫為:

printf(″sum4=%f\n″,sum(sum(a,b),sum(c,d));在被調函數中含有返回值的return語句,如果在主調函數中把被調函數作為表達式的一部分,則此時被調函數的返回值是有意義的;而如果把被調函數作為過程語句處理,則主調函數對被調函數的返回值不予理睬,只是把控制權收回來,被調函數的返回值被舍棄。

【例5-4】把用公式求π近似值的計算編成函數,然后調用之。計算中的項數由用戶決定。

#include<stdio.h>

doublepi(intt) { inti;

doubles=1.0,sum=0,item=1; for(i=1;i<=t;i++) {sum+=item; s=-s; item=s/(2*i+1); }sum=sum*4;printf(″PI=%f\n″,sum);returnsum;}main(){intn;printf(″Inputtheitemnumber:\n″);

scanf(″%d″,&n);pi(n);return0;}運行輸出:Inputtheitemnumber:100↙

PI=3.131593再運行:Inputtheitemnumber:1000↙

PI=3.141493主調函數是將函數pi(n)作為過程語句調用的,因此它沒有接收pi()函數的返回值,函數中求得的值已在函數本身輸出了。若在主調函數中對pi的調用換一種形式,比如: printf(″pi=%f\n″,pi(n));則pi的返回值就有用了。

【例5-5】求奇特數。輸入兩個整數a、b,計算并求出一個整數x,使x+a和x+b都是完全平方數。如在某個范圍內找不到這樣的x,則說明對這一組a、b不存在奇特數,需再輸入一組a、b。編程思路:首先根據題意,列出方程:這里已假定x+a是個完全平方數,接著需要驗證z也是個完全平方數,如驗證成功,則說明這樣的奇特數已經找到,并進一步給出z是哪個數的平方。可令①式中的y從1開始逐一增加,通過公式x=y2-a不斷地求出x,然后把它代入②式中求出z,再來判斷z是否為完全平方數。判斷完全平方數的運算可用一個函數來表示。判斷時采用下面的方法。根據公式:1+3+5+7+…+(2n-1)=n2

可令x=n2,從x中不斷地減去1,3,5,…,最后一定為0,此時函數返回值為1,說明x是一個完全平方數。如果連續(xù)減的最后結果不為0,則說明x不是一個完全平方數,返回值為0。程序如下:#include<stdio.h>#defineMAX10000intsquareful(longintx){inti;for(i=1;;i+=2){if((x-=i)<0) break;if(x==0)return1;}return0;}main(){inti,j;longinta,b,x,y,z;do{printf(″Inputa,b:\n″);scanf(″%ld%ld″,&a,&b);

for(y=1;;++y){x=y*y-a;if(x<0)continue;

z=x+b;if(squareful(z)==1)gotolabl;if(y>MAX){printf(″change(a,b)!\n″); break;}}}while(y>MAX);labl:printf(″x=%ld\n″,x);printf(″%ld+%ld=%ld**2\n″,x,a,y);for(j=1,i=1;;i+=2)if((z-=i)!=0)j++;elsebreak;printf(″%ld+%ld=%d**2\n″,x,b,j);return0;}運行輸出:Inputa,b:100150↙

change(a,b)!Inputa,b:100200↙

x=476476+100=24**2476+200=26**2在主調函數中調用了判斷完全平方數的函數squareful,若該函數值為1,說明已找到一個合乎要求的x,則通過goto語句轉到輸出這個x的語句。如果y>MAX,說明在MAX范圍內無這種奇特數,則通過break語句跳出for循環(huán),再在外層的do_while中輸入新一組(a,b)的值。在已確定z是一個完全平方數后,進一步還要確定z是哪一個數的平方,這仍然采用不斷從z中減去1,3,5…直到最后結果為0的方法,減一次后計數器j加1,結果為0時的j就是所求的結果,即j2=z。因j定義為int類型,所以輸出時使用控制符“%d”,而不是“%ld”。

【例5-6】任何一個非素數,都可以表示成多個素數之積的形式。#include<stdio.h>#include<math.h>voidf(intn){intk,r;printf(″%d=″,n);for(k=2;k<=sqrt(n);k++){r=n%k; while(r==0) {printf(″%d″,k); n/=k; if(n>1)printf(″*″); r=n%k;}}if(n!=1)printf(″%d\n″,n);}main(){intm;printf(″\n″);scanf(″%d″,&m);f(m);return0;}運行結果:

125↙ 125=5*5*5再運行:

348↙ 348=2*2*3*29再運行:

347↙

347=347 //347本身是一個素數

4.函數的嵌套調用

C語言中不能定義嵌套函數,但可以嵌套調用,嵌套調用的示意圖見圖5-3。執(zhí)行過程是沿著①、②、③、④、⑤、⑥、⑦、⑧、⑨的路線來進行的,程序始于main()函數,終于main()函數。

【例5-7】輸入一個整數,輸出其平方與立方。圖5-3嵌套調用的示意圖我們把求平方和立方的計算編成函數:/*calculatesquare*/longsq(longinti){longa;a=i*i;returna;}/*calculatecube*/longcub(longintj){longb;b=sq(j)*j;returnb;}#include<stdio.h>main(){longintn;printf(″Inputn=?\n″);scanf(″%ld″,&n);printf(″squareof%ldis%ld\n″,n,sq(n));printf(″Cubeof%ldis%ld\n″,n,cub(n));return0;}運行輸出:Inputn=?3↙

squareof3is9Cubeof3is27程序中,主函數調用cub函數,而cub函數又調用sq函數,構成嵌套調用。

【例5-8】用弦截法求方程x3-5x2+16x-80=0的根。

編程思路:設f(x)=x3-5x2+16x-80,其曲線如圖5-4所示,讀入兩個數x1和x2,其對應的函數值為f(x1)和f(x2),通過這兩點作弦交X軸于x,則x距解x0的距離比x1和x2距x0的距離更近;而如果再通過f(x)和f(x2)作一條弦,則該弦交X軸的點會距x0更近……這樣可以一步步地逼近x0。圖5-4方程求解示意圖在讀入x1和x2時應保證方程的根x0在它們之間,如不在它們之間就再讀入兩個新的x1和x2,反復進行直到符合要求為止。根在x1和x2之間的條件是f(x1)·f(x2)<0,即f(x1)和f(x2)異號。為求弦和X軸的交點,可利用三角形的相似關系,如在圖5-4中有Δxx1f(x1)~Δxx2f(x2)又由于|f(x1)|=-f(x1),則比例式為由此公式求出x:進而可求出x處的函數值f(x)。x趨于x0的標志是|f(x)|趨于0,所以可以把|f(x)|<ε作為循環(huán)的終止條件。為求解這一問題,我們可以定義下列三個函數:①表示f(x)的函數:給定一個x作參數,返回f(x)的函數值。②求交點的函數:給定x1、x2的參數,返回弦與x軸的交點。③求根的函數:給定x1和x2作參數,返回方程符合條件的根。問題的流程圖如圖5-5所示。圖5-5問題的流程圖程序如下:#include<stdio.h>#include<math.h>#defineEPS1.e-3floatf(floatx){floaty;y=((x-5.0)*x+16.0)*x-80; /*把多項式寫成乘、加形式,以提高效率*/returny;}floatxpoint(floatx1,floatx2){floatx;x=(x1*f(x2)-x2*f(x1))/(f(x2)-f(x1));/*調用了函數f*/returnx;}floatroot(floatx1,floatx2){floatx,y,y1;y1=f(x1);

do{x=xpoint(x1,x2);y=f(x);if(y*y1>0) /*說明根在后半部*/{y1=y;x1=x;}elsex2=x;/*根在前半部*/}while(fabs(y)>=EPS);returnx;}main(){floatx1,x2,f1,f2,x;do{printf(″Inputx1,x2:\n″);

scanf(″%f%f″,&x1,&x2);

f1=f(x1);f2=f(x2);}while(f1*f2>=0);x=root(x1,x2);printf(″Theroot=%8.4f\n″,x);return0;}運行輸出:Inputx1,x2:28↙

Theroot=└┘└┘5.0000在程序中,主函數main調用了root函數,root調用了xpoint函數,xpoint調用了f函數,其調用關系如圖5-6所示。圖5-6調用關系圖上面用流程圖描述了執(zhí)行過程,如果用N-S圖來描述,則應注意N-S圖中表達do_while的條件表達式的方法。圖5-7為用N-S圖描述的執(zhí)行過程。圖5-7N-S圖描述5.3函數原型及函數聲明在上面的函數調用中,我們有意把被調函數都放在了主調函數的前面,為的是在調用時,被調函數的一切信息對主調函數來說都是已知的,因為編譯程序是按文本出現的先后次序對程序進行編譯的。如果被調函數在后,主調函數在前,則應如何處理呢?這時應該在主調函數之前或在主調函數中對被調函數進行聲明。函數聲明的一般形式是:

〈返回值類型〉〈函數名〉(〈參數類型聲明表〉);其中,〈參數類型聲明表〉的形式是:

〈參數類型〉[參數名]],〈參數類型〉[參數名]]…方括號中的內容可以不要,省略號表示可以重復。這就是說在函數聲明時,只列出參數類型就可以了,不必再寫出參數名,寫出是為了清楚,但編譯程序會將它忽略。例如,對求和函數sum就可作以下的聲明:main(){floatsum(float,float);

…sum();

}floatsum(floatx,floaty){…}如果被調函數在后而又未在主調函數中聲明,則編譯時會給出錯誤提示:

″Function′xx′shouldhaveaprototype″(“函數xx應當有個原型”)函數的聲明就是函數原型的聲明。函數原型提供函數如下的信息:

(1)函數返回值的類型。

(2)函數的參數個數。

(3)各個參數的類型。

(4)各參數之間的順序。編譯器就利用函數原型來檢驗函數調用,如調用和原型不一致就會給出錯誤提示,這樣可以避免不經檢驗而執(zhí)行該函數時可能導致的致命錯誤或邏輯錯誤。函數原型的聲明地點,可以在所有函數之外,也可以在某個函數之中。如在函數之外,則處在其聲明之下的所有函數都可以引用它;若在函數之中,則只有本函數可以引用它。

【例5-9】通過函數調用,求三個正整數的最小公倍數。

編程思路:如果三個數中最大數的倍數能被這三個數整除,則這個倍數就是它們的最小公倍數。先求出三個數的最大值,對這個最大值的倍數循環(huán)測試,看能否被這三個數整除。從最小的倍數開始測試,則遇到的第一個符合條件的倍數就是它們的最小公倍數。把求三個數最大值的計算編為一個獨立的函數,設該函數的定義在主調函數之后,則在主調函數前應對該函數作原型聲明。程序如下:#include<stdio.h>longmax3(long,long,long);main(){longa,b,c,i=1,j,m;printf(″Input3integers:\n″);scanf(″%ld%ld%ld″,&a,&b,&c);m=max3(a,b,c);while(1){j=m*i;if(j%a==0&&j%b==0&&j%c==0)break;i++;}printf(″Result=%ld\n″,j);return0;}longmax3(longx1,longx2,longx3){longmax;if(x1>x2)max=x1;elsemax=x2;if(max<x3)max=x3;returnmax;}運行輸出:Input3integers:123236369↙

Result=87084主函數中while(1)表示循環(huán)條件永遠為真,但能否繼續(xù)循環(huán)下去,還要看循環(huán)體中的if條件是否滿足,一旦條件滿足,則執(zhí)行break語句退出循環(huán),此時最大數的倍數j即為所求結果。

【例5-10】如果一個整數的所有因子(因子從1開始,不含自身)之和等于其自身,則這個整數就被稱為是完數。數學中有一個定理稱,如果2n+1-1是一個素數,則2n×(2n+1-1)就一定是完數。編程求10000之內的所有完數。

編程思路:對10000之內所有由2n+1-1生成的整數進行是否為素數的判斷,如其為素數,則用2n×(2n+1-1)生成的數就是相應的完數。對每一個完數還應求出它的所有因子。把判斷素數及求因子的操作分別設計為兩個函數。設函數定義在后,則在主函數中應對它們進行原型聲明。程序如下:#include<stdio.h>#include<math.h>main(){intprime(int);voidfactor(intn);intw,m,n=1;m=pow(2,n+1)-1;w=m*pow(2,n);while(w<10000){if(prime(m)){ printf(″%disaperfectnumber\n″,w); printf(″%d=pow(2,%d)*(pow(2,%d+1)-1)n=%d\n″,w,n,n,n); factor(w);}n++;m=pow(2,n+1)-1;w=m*pow(2,n);}printf(″\n\n″);return0;}intprime(intn){inti,q,flag=1;q=sqrt(n);for(i=2;i<=q;i++) if(n%i==0) flag=0;if(flag==1) return1;else return0;}voidfactor(intn){inti,m;m=n/2;printf(″%d=1″,n);for(i=2;i<=m;i++)if(n%i==0&&n!=0){ putchar(′+′); printf(″%d″,i);}printf(″\n″);}運行輸出:6isaperfectnumber6=pow(2,1)*(pow(2,1+1)-1)n=16=1+2+328isaperfectnumber28=pow(2,2)*(pow(2,2+1)-1)n=228=1+2+4+7+14496isaperfectnumber496=pow(2,4)*(pow(2,4+1)-1)n=4496=1+2+4+8+16+31+62+124+2488128isaperfectnumber8128=pow(2,6)*(pow(2,6+1)-1)n=68128=1+2+4+8+16+32+64+127+254+508+1016+2032+4064函數原型的一個重要特點是強制類型轉換,即把實參的類型強制轉換成形參的類型,就好像是實參把其值直接賦給形參類型的變量一樣。比如數學庫函數sqrt的函數原型中指定參數為double類型,但如用整型數據調用該函數時仍能正確運行。語句

printf(″%.3f\n″,sqrt(4));能正確計算sqrt(4)并打印出值2.000。函數原型通知編譯器在把整數4傳遞給sqrt之前先轉換成4.0。通常情況下,在函數調用前,與原型中參數類型不完全一致的實參會按照“提升規(guī)則”轉換為合適的類型。提升規(guī)則把存儲空間較小的類型轉換成存儲空間較大的類型,實際上是建立該值的臨時值來使用,原始值并沒有被修改。提升規(guī)則說明了在不丟失精度的情況下把一種類型轉換為其他類型。反之,相反的調用會產生不正確的結果。例如,求整數平方的函數原型為:

intsquare(int);用浮點型實參調用就會返回不正確的結果,如square(4.5),則返回16而非20.25。因此應該避免與提升規(guī)則不一致的逆向調用。5.4數據存儲類當用戶編程上機的時候,編譯器會為用戶提供一定的內存空間讓用戶使用。這個內存空間被劃分為不同的區(qū)域以存放不同的數據。大體上用戶區(qū)劃分為三部分(如圖5-8所示):程序區(qū),用來存放用戶的程序;動態(tài)區(qū),用來存放暫時的數據;靜態(tài)區(qū),用來存放相對永久的數據。

C語言中的變量都有兩個特征:一個是它們的作用范圍有大有小;一個是它們的存在期限有長有短。這兩個特征統(tǒng)一于它們的存儲類,因此我們從存儲出發(fā)來研究變量的這兩方面的特征。程序區(qū)動態(tài)區(qū)靜態(tài)區(qū)圖5-8用戶區(qū)的劃分用戶區(qū)5.4.1自動(auto)變量自動變量的定義位置在復合語句內部。復合語句可以嵌套,函數體就是個最大的復合語句。自動變量在定義時可以加前綴auto關鍵字或什么都不加。自動變量的作用范圍是從定義點起到復合語句的結束。例如下面的程序:#include<stdio.h>main(){autointi=1,j=2;printf(″i=%d,j=%d\n″,i,j);{ inti=3,a=4; floatx=1.1,y=2.2; printf(″i=%d,a=%d\n″,++i,a);

printf(″x=%f,y=%f\n″,x,y);

j++;}printf(″i=%d,j=%d\n″,i,j);return0;}程序中,i、j、x、y、a都是自動變量,只是x、y和a的作用范圍是在內嵌的復合語句中,而j的作用范圍是整個函數體,其作用力可貫穿內嵌的復合語句。注意變量i,在兩個地方都有定義,但它們的作用范圍不同:內嵌復合語句中的i只在此復合語句中起作用;而外部的i在整個函數中起作用,但因被內嵌的復合語句中的i所屏蔽,所以其作用達不到復合語句內部。因此這兩個i是不同的變量,雖然它們的名字是一樣的。所以運行上面的程序,輸出結果是:i=1,j=2i=4,a=4x=1.100000,y=2.200000i=1,j=3自動變量放在動態(tài)區(qū)中,函數調用結束后就被撤銷。5.4.2寄存器(register)變量用戶頻繁使用的數據不一定全要放在內存中,還可以放在運算器的寄存器中。如果數據放在內存中,運算器就要到內存中取數據,運算結束后還要送回內存,這樣就需要反復地訪問內存,影響運算速度。若把數據直接放在運算器的寄存器中,則可免除頻繁訪問內存之勞,提高執(zhí)行效率。定義寄存器變量時前面要加關鍵字register,如:

registerintri,rj;注意:①寄存器變量的個數是有限的,并且各個系統(tǒng)都不相同。如寄存器變量的個數超過系統(tǒng)提供的數量,則多出來的變量會被作為自動變量處理。②對于有些系統(tǒng),如TurboC,會把寄存器變量均作為自動變量處理,所以定義register變量意義不大。5.4.3靜態(tài)(static)變量如果定義變量時在前面加上static修飾,則這樣的變量就是靜態(tài)變量,它被放在內存的靜態(tài)區(qū)。靜態(tài)變量的使用特征是:在定義時賦初值一次,如不顯式賦初值,則系統(tǒng)會對其自動賦初值(數值型變量初值為0,字符型變量初值為空字符′\0′)。包含static變量的函數被調用后,static變量的值并不消失,當再次調用該函數時,上次調用的結果就作為本次的初值使用。在main函數中定義static變量的意義不大,因為程序每次運行都是重新分配空間的。

【例5-11】觀察static變量和自動變量的區(qū)別。#include<stdio.h>main(){inti;voidaust();for(i=0;i<5;i++)aust();return0;}voidaust(){intau=0;staticintst=0;

printf(″au=%d,st=%d\n″,au,st);++au;++st;}運行輸出:au=0,st=0au=0,st=1au=0,st=2au=0,st=3au=0,st=4在函數aust中,靜態(tài)變量st的初值為0,函數每調用一次,其值增1,并把結果保存到下次調用時。而自動變量au雖然在函數調用時其值也增1,但當函數調用結束后它的值也就消失了。

【例5-12】求下列函數的執(zhí)行結果。#include<stdio.h>main(){inti,j;intf(int);i=f(3);j=f(5);printf(″i=%d,j=%d\n″,i,j);return0;}intf(intn){staticints=1;while(n)s*=n--;returns;}運行輸出:

i=6,j=720函數f()的功能是求參數n的階乘,即f(n)=n!,main函數對它調用了兩次。經過f(3)的調用,使static變量s的值變?yōu)?;當第二次調用f(5)時,s的值并不再是從初值1開始,而是從上次調用的結果開始,所以j的值應該是3!*5!=720。

【5-13】輸出下面程序的執(zhí)行結果。#include<stdio.h>main(){inti,m=3;intf(int,int);for(i=1;i<=3;i++)printf(″%d.″,f(i,m));return0;}intf(intx,inty){staticinti=1,m=1;i+=m+2;m=i*x+y;returnm;}運行輸出:

7.29.135.注意:這里主調函數和被調函數中出現了同名變量i和m,但它們是不同的變量。被調函數f()中的變量i和m都是局限于此函數中的靜態(tài)變量,每次調用后的結果都不一樣;而主函數main()中的變量i和m在作為參數傳遞給函數f()時就變成了x和y的值。5.4.4外部變量在所有函數之外定義的變量稱為外部變量,它們不為某個函數所專有,程序中所有函數都可以引用它們,因此外部變量也就是全局變量。外部變量放在靜態(tài)區(qū)中。外部變量的使用方法根據它所處位置的不同而不同:

(1)在外部變量自然作用范圍之內的函數可以直接引用它們。自然作用范圍指從變量的定義點到文件結束。

(2)在自然作用范圍之外,即對全局變量的引用在前,而它的定義在后,則應當對它作extern說明。

【例5-14】#include<stdio.h>intmax(intx,inty){returnx>y?x:y;}main(){externinta,b;intmin(int,int);printf(″max=%d\n″,max(a,b));printf(″min=%d\n″,min(a,b));return0;}inta=8,b=5;intmin(inti,intj){returni<j?i:j;}運行輸出:max=8min=5在main函數中引用a、b時,a、b尚未定義,因此要做extern說明。為了避免忽略對全局變量的extern說明,一般總把全局變量放在程序的最前面,使所有函數都處在它的自然作用范圍之內,所有函數都可以在不做extern說明的情況下引用它們。使用全局變量的好處是可以實現數據在各函數之間的流通,能增加函數間數據聯系的渠道,便于寫出高效的程序。缺點是任何函數都可以對全局變量進行修改,因此很難判斷它的當前值是什么,容易出現錯誤,因此應該慎重使用。

【例5-15】求調和級數的和,結果用分數表示。編程思路:利用下式求兩個分數之和:

把求兩個分數之和的運算讓函數addrat來完成。在求出一個分數后,還要把它們化簡成真分數,這就要找出分子、分母的最大公因子,并用它對分數進行化簡,這個計算由函數lowterm完成。在整個操作中都是對分子、分母進行的,若在各個函數中都定義兩個分子、分母變量,則互相傳遞時會很麻煩,因此可以考慮定義兩個全局變量num和den代表分子、分母,以它們作為數據紐帶把各個函數聯系起來。#include<stdio.h>intnum,den;main(){intn,nterm;voidaddrat(int,int);voidlowterm();printf(″Inputthenumberofterms\n″);scanf(″%d″,&n);if(n<=0)printf(″Baddata!\n″);elseif(n==1)printf(″1/1\n″);else{num=1;den=1;for(nterm=2;nterm<=n;nterm++) { addrat(1,nterm); lowterm(); printf(″%d/%d\n″,num,den); }}return0;}voidaddrat(inta,intb){num=num*b+a*den;den=den*b;}voidlowterm(){intnum1,den1,rem;num1=num;den1=den;while(den1!=0){rem=num1%den1;num1=den1;den1=rem;}if(num1>1){num=num/num1;den=den/num1;}}運行輸出:Inputthenumberofterms-1↙

Baddata!再運行一次:Inputthenumberofterms1↙

1/1再運行一次:Inputthenumberofterms5↙

3/211/625/12137/60主函數只對全局變量num和den賦了初值1,并沒有其他明顯的操作,但最后的輸出卻是經過運算后的值,這主要是借助全局變量num和den在函數addrat和lowterm中進行的,因此主函數顯得相當簡潔。5.5多文件程序中函數和變量的處理

C語言中的函數定義不能嵌套,所以函數相互之間都是互為外部的,有時說內部函數和外部函數,這主要是針對某個文件而言的。這里需要對C程序的組成作個說明,如圖5-9所示。圖5-9C程序的組成圖5-9說明,一個大一點的C程序可以由多個文件組成,一個文件又可以包含多個函數,包含main函數的文件是主文件,是程序的入口和出口。文件是一個獨立的編譯單位,而函數不是獨立的編譯單位。在一個文件中定義的外部變量和函數能否被其他文件所引用,是我們現在要討論的問題。這里有兩種情況:

(1)若一個文件中定義的外部變量和函數不允許其他文件引用,這時應在函數名和變量名的前面加上關鍵字static。如:staticinta;staticfloatmax(floatx,floaty)這里static聲明就把a和max局限在它所定義的文件中,它們只能在這個文件中被使用,不允許其他文件使用,相對于這個文件來說,它們是內部的。

(2)若一個文件中定義的函數和外部變量可以被其他文件引用,這時在函數名和外部變量名前不加static說明,但是凡引用它們的文件必須在各自的文件內部對這些函數和變量作extern說明。比如,在文件f1.c中定義了一個外部變量b和一個函數sum,在文件f2.c中想引用它們,則在f2.c中必須作如下說明:externintb;externintsum(int,int);

【例5-16】輸入二元一次方程組中自變量的系數和常數項,解此方程組:編程思路:根據數學知識,可利用系數行列式求解。令則我們把g、x、y的求解用一個函數solve解決,并單獨放在一個文件fs.c中,主函數放在文件fm.c中,它調用solve函數并向其提供數據。

文件fm.cexternvoidsolve();inta,b,c,d,e,f;floatg,x,y;#include<stdio.h>main(){printf(″Inputdata:a,b,c,d,e,f:\n″);scanf(″%d%d%d%d%d%d″,&a,&b,&c,&d,&e,&f);solve();if(g==0) printf(″Therearemanysolution!\n″);

else printf(″x=%f,y=%f\n″,x,y);

return0;}

文件fs.cexterninta,b,c,d,e,f;externfloatg,x,y;voidsolve(){g=a*e-b*d;;if(g!=0){x=(c*e-b*f)/g;y=(a*f-c*d)/g;}}運行輸出:Inputdata:a,b,c,d,e,f:123456↙

x=-1.000000y=2.000000在文件fm.c中要對在fs.c文件中定義的函數solve作extern說明,同樣在文件fs.c中要對在fm.c文件中定義的外部變量a、b、c、d、e、f、g、x、y作extern說明。如何運行由多個文件組成的程序呢?有兩種方法:組成擴展名為.prj的項目文件;利用文件包含。

(1)建立項目文件的操作步驟如下(以TurboC編譯器為例):①打開“Project/Openproject”對話框。②輸入要建立的項目文件名,比如ms.prj。③利用“Project/Additem”命令打開文件清單。④在*.c文件中選擇fm.c和fs.c文件并加入到ms.prj文件中。⑤按ESC或Done按鈕,則建立項目文件完成。⑥對項目文件像對其他.c文件一樣進行編譯、連接和運行。⑦在執(zhí)行結束后,應執(zhí)行“Project/CloseProject”操作,否則會影響后面其他文件的編譯。

(2)文件包含方法是指,在一個文件中用編譯預處理命令#include將本文件中所需的其他文件包含進來為我所用。比如在fm.c文件中加入 #include″fs.c″這條預處理命令后,就可以直接對fs.c文件進行編譯、連接、運行,這時fs.c文件已是fm.c的一部分,所以在fm.c中就不需要再對fs.c文件中的solve函數作extern說明了。同樣在fs.c中也不需要對fm.c文件中定義的外部變量作extern說明了。

(3)VC++環(huán)境下建立項目文件的步驟。在VC中,任何C文件都被組織到項目(或工程,即project)中,而項目又是在工作區(qū)(workspace)中運行的。一個工作區(qū)中可以有多個項目,一個項目又可以有多個C文件,其結構形式為:工作區(qū)和項目可以由用戶建立,也可以自動生成。由用戶建立時,其順序是先建立工作區(qū),再建立項目,最后再建立文件。前面只介紹了建立文件的過程,沒有提到建立工作區(qū)和項目,實際上,它們已由編譯系統(tǒng)自動生成了。工作區(qū)名和項目名均默認地與文件名同名,但那是一個程序只有一個文件的情況。如果一個程序由多個文件組成,則必須顯式地建立項目文件,并把組成該程序的所有文件都組織到項目中。其步驟如下:

(1)打開“File/New”菜單,在New(新建)對話框中選擇Project(工程)標簽,在左邊的列表框中選擇“Win32ConsoleApplication”選項,即選擇控制臺工作方式。然后在右邊的“Projectname”一欄輸入項目名,它下面的“Location”欄用來指定項目所在的目錄,如圖5-10所示。圖5-10New對話框

(2)在輸入項目名(此處為fms)并指定其目錄后,按“OK”按鈕,并在后面出現的對話框中連續(xù)按回車鍵,則會出現如圖5-11所示的窗口。其左部窗口中顯示出“Workspace′fms′:1project[s]”,下一行是“fmsfiles”,這說明項目fms已被加到工作區(qū)fms中(這里工作區(qū)默認地采用了項目的名字)。項目中包含三個文件夾:SourceFiles(源程序文件)、HeaderFiles(頭文件)和ResourceFiles(資源文件)。項目是所有用于創(chuàng)建一個可運行程序的文件的集合。項目文件的擴展名是.dsp,不可以用type命令顯示其內容。圖5-11項目窗口

(3)執(zhí)行菜單“Project/AddToProject/Files...”,如圖5-12所示。這是為了要向項目文件中添加.c程序。在彈出的對話框“InsertFilesintoProject”中,選擇要加入到項目中的.c文件,如圖5-13所示。圖5-12添加.c程序圖5-13選擇.c程序

(4)單擊“OK”按鈕,此時如打開左邊項目fms的SourceFiles文件夾,可以發(fā)現已有兩個源程序文件fm.c和fs.c加入到了項目文件中。對項目文件不需要編譯,直接執(zhí)行“Build/Buildfms.exe”(或按F7),可生成可執(zhí)行文件.exe。該命令自動進行編譯和連接操作,并將編譯、連接的結果顯示在屏幕下方的調試信息窗口中,如圖5-14所示。圖5-14顯示調試信息

(5)執(zhí)行菜單“Build/!Executefms.exe”(或按Ctrl+F5),運行該可執(zhí)行文件,結果如圖5-15所示。圖5-15運行結果5.6遞歸迄今為止,我們所用的函數調用都是以層次的方式用一個函數調用另一個函數,但某些問題可以用調用函數自身的方法來解決。遞歸函數就是直接或間接調用自身的函數。5.6.1遞歸函數用遞歸解決問題,就是把問題分為兩部分:一部分屬于基本問題,對它可以直接求出結果;另一部分雖然不是基本問題,但是與原問題類似并且比原問題簡單一些。對稍微簡單一些的問題繼續(xù)進行分解,直至達到邊界條件。解決完邊界處的問題(即基本問題)后,函數會沿著調用順序不斷給上一次的調用返回結果,直至原始問題。這看起來似乎很復雜,但事實上是個很自然的過程。遞歸函數用來解決遞歸問題。所謂遞歸問題,就是能逐漸簡化到邊界條件的問題。來看下面的例子。

【例5-17】求階乘的運算:

n!=1(n=0或n=1) n!=n(n-1)!(n≥2)當n大于等于2的時候,欲求n的階乘,只要先求出n-1的階乘,把所得結果乘以n即為n的階乘。至于n-1的階乘是多少,在這一步可先不考慮,設它為p。則

n!=n·p這是一步就可求出的問題,但這時p還不是一個確定的數,還要等待返回結果。接下來求p=(n-1)!,再把它進行分解。它等于n-1去乘n-2的階乘,即p=(n-1)·(n-2)!,設(n-2)!=q,則p=(n-1)·q。接下來的問題是再去求q。按照上述方法繼續(xù)類似的操作,問題一步步地化簡,最終一定能達到求1的階乘的地步,這時就稱達到了邊界條件,可以直接求解了。任何遞歸調用都有一個邊界條件,否則遞歸調用將會無限地進行下去。在上面的計算中,每一步都要等待下一步的計算結果,因此它當時的狀態(tài)就得先保存起來,等下步的結果返回后再進行基本運算。當到達邊界條件后,可求出確定的值,于是返回到上一步,而上一步當時的狀態(tài)已被保存,只等有返回值后就可進行直接運算,它運算的結果又繼續(xù)向上一步返回,這樣一步步向上返回,直至原始問題得解。從上面的分析可以看出,求解遞歸問題有兩個過程:第一個過程是遞歸調用,從原始問題出發(fā),層層向下,直至邊界條件;第二個過程是返回過程,從邊界條件出發(fā),層層向上返回,直至原始問題。通過遞歸調用圖,就可以清楚地看到這兩個過程。

【例5-18】求4!的遞歸調用,調用圖如圖5-16所示。

4!

4!=24 ↓ ↑返回4!=24(等待返回3!)4×3! 4×6 ↓ ↑返回3!=6(等待返回2!)3×2! 3×2 ↓ ↑返回2!=2 2×1!邊界條件2×1(遞歸調用過程) (返回過程)圖5-16求4!的遞歸調用圖下面是求n的階乘的程序:#include<stdio.h>longintfac(int);main(){inti;for(i=1;i<=10;i++)printf(″%2d!=%ld″,i,fac(i));return0;}longintfac(intn){if(n<=1)return1;elsereturnn*fac(n-1);}運行輸出:1!=12!=23!=64!=245!=1206!=7207!=50408!=403209!=36288010!=3628800

說明:①從輸出結果看,階乘增加得非常快,7以上的階乘就超過了int類型的取值范圍,這就是為什么要把返回值的類型定義為longint的原因,相應的輸出格式是′%ld′。如果要求更大數的階乘,可以選用float或double作為階乘函數返回值類型。②求階乘函數中有個核心語句:雙分支的if語句。其中的一個分支描述的是邊界條件,另一個分支是遞歸調用。在遞歸調用中又出現了該函數的函數名,但這里的參數比函數頭中的參數就簡單了一些,這就是遞歸函數的書寫模式。對于大多數遞歸函數,都可以這樣考慮和書寫。在遞歸函數體中還可以出現不只一個的遞歸調用,它們各自有不同的參數。

【例5-19】求Fibonacci數列:1,1,2,3,5,8,13,…觀察數列,可發(fā)現這樣的規(guī)律:從第3項開始,每一項都是其前面兩項之和。設fib(n)[JP]表示第n個Fibonacci數,則有:

fib(n)=1(n=1或n=2) fib(n)=fib(n-1)+fib(n-2)(n≥3)

Fibonacci數列是個很有趣的數列,隨著項數的增加,前后相鄰兩項的比值趨向于0.618,這個比值被稱為“黃金分割”。建筑師經常用黃金分割來指導設計門窗、房間和建筑物等,人們認為符合黃金分割的物體是一種美的物體。

Fibonacci函數的數學表達式已清楚地表明這是個遞歸問題,因此可以很容易地用C語言寫出它的遞歸函數。#include<stdio.h>longfib(int);main(){longbf;intn;printf(″Enteraninteger:\n″);scanf(″%d″,&n);bf=fib(n);printf(″Fibonacci(%d)=%ld″,n,bf);return0;}longfib(intm){if(m==1‖m==2)return1;elsereturnfib(m-1)+fib(m-2);}運行輸出:Enteraninteger:8↙

Fibonacci(8)=21

fib函數的遞歸調用如圖5-17所示(這里是求fib(5))。圖5-17求fib(5)的遞歸調用

對符合邊界條件的基本問題可直接求出其解。這里涉及到對操作數求值的次序問題。在fib(3)=fib(2)+fib(1)中,是先計算fib(2)還是先計算fib(1),ANSIC標準對此沒有作出明確的規(guī)定,因此程序可對調用順序不作任何假定。對本程序和大多數程序而言,計算結果都是正確的,但在某些程序中,操作數的運算順序可能會影響表達式的最終結果。在C語言的所有運算符中,ANSIC只規(guī)定了下面四種運算符對操作數的求值順序:&&、‖、逗號和?:。前三個運算符是雙目運算符,按從左到右的順序計算它的兩個操作數。最后一個是C語言中惟一的一個三目運算符,它最左邊的操作數優(yōu)先運算。至于其他運算符操作數的求值順序,不同的系統(tǒng)有不同的規(guī)定。因此如果編寫的程序依賴于操作數的求值順序,將可能導致錯誤,因為編譯器的執(zhí)行順序不一定會和編程者的想法一致。另外,遞歸函數的調用次數也是個值得注意的問題。以本題為例,每次遞歸調用fib函數都要再遞歸地調用兩次,因此當n很大時,對fib函數的調用次數將是驚人的,所以應該避免編寫像fib函數這樣的遞歸函數,即使要編寫,參數也不應很大。

【例5-20】把一個正整數的各位數字以字符形式輸出。#include<stdio.h>voiddigit(int);main(){intm;printf(″Inputaninteger:\n″);scanf(″%d″,&m);digit(m);return0;}voiddigit(intn){if(n==0)return;else{digit(n/10);putchar(n%10+′0′);}}

digit()是個遞歸函數,其參數為n,而在該函數中又以n/10為參數進行遞歸調用,則調用是向參數減少的方向變化的。當n變?yōu)?時,達到邊界條件,遞歸調用結束,開始回退。在回退的過程

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論