C語言常見問題_第1頁
C語言常見問題_第2頁
C語言常見問題_第3頁
C語言常見問題_第4頁
已閱讀5頁,還剩144頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)

文檔簡介

第1章C語言本章主要描述c語言ー些基本要素。當(dāng)你開始編寫c程序時,你可能對c語言的ー些基本問題感到困惑,如c語言所使用的約定、關(guān)鍵字和術(shù)語等。本章將回答這方面你經(jīng)常會遇到的ー些問題。例如,switch語句是最常用的ー種C語言構(gòu)件,本章將回答與它有關(guān)的三個常見問題。木章還涉及其它幾個問題,如循環(huán)、分支、運算符的優(yōu)先級和程序塊技術(shù)。在閱讀本章時,請注意有關(guān)switch語句和運算符優(yōu)先級的ー些問題,這些問題常常會使C語言的初學(xué)者感到迷惑。1.1什么是局部程序塊(localblock)?局部程序塊是指ー對大括號({})之間的?段C語言程序。ー個C函數(shù)包含ー對大括號,這對大括號之間的所有內(nèi)容都包含在ー個局部程序塊中。if語句和swich語句也可以包含ー對大括號,每對大括號之間的代碼也屬于ー個局部程序塊。此外,你完全可以創(chuàng)建你自己的局部程序塊,而不使用C函數(shù)或基本的C語句。你可以在局部程序塊中說明一些變量,這種變量被稱為局部變量,它們只能在局部程序塊的開始部分說明,并且只在說明它的局部程序塊中有效。如果局部變量與局部程序塊以外的變量重名,則前者優(yōu)先于后者。下面是一?個使用局部程序塊的例子:#include<stdio.h>voidmain(void);voidmain(){/*Beginlocalblockforfunctionmain()*/inttest_var=10;printf("Testvariablebeforetheifstatement:%d\n”,test_var);if(test_var>5)(/*Beginlocalblockfor"if"statement*/inttest_var=5;printf("Testvariablewithintheifstatement:%d\n",test_var);{/*Beginindependentlocalblock(nottiedtoanyfunctionorkeyword)*/inttest_var=0;printf("Testvariablewithintheindependentlocalblock:%d\n",test_var)/*Endindependentlocalblock*/printff'Testvariableaftertheifstatement:%d\n'\test_var);1/*Endlocalblockforfunctionmain()*/上例產(chǎn)生如下輸出結(jié)果:Testvariablebeforetheifstatement:10Testvariablewithintheifstatement:5Testvariablewithintheindependentlocalblock:0Testvariableaftertheifstatement:10注意,在這個例子中,每次test_var被定義時,它都要優(yōu)先于前面所定義的tesjvar變量。此外還要注意,當(dāng)if語句的局部程序塊結(jié)束時,程序重新進入最初定義的test_var變量的作用范圍,此時test_var的值為!0.請參見:1.2可以把變量保存在局部程序塊中嗎?I.2可以把變量保存在局部程序塊中嗎?用局部程序塊來保存變量是不常見的,你應(yīng)該盡量避免這樣做,但也有極少數(shù)的例外。例如,為了調(diào)試程序,你可能要說明一個全局變量的局部實例,以便在相應(yīng)的函數(shù)體內(nèi)部進行測試。為了使程序的某部分變得更易讀,你也可能要使用局部程序塊,例如,在接近變量被使用的地方說明?個變量有時就會使程序變得更易讀。然而,編寫得較好的程序通常不采用這種方式來說明變量,你應(yīng)該盡量避免使用局部程序塊來保存變量。請參見:1.I什么是局部程序塊?1.3什么時候用一條switch語句比用多條if語句更好?如果你有兩個以上基于同一個數(shù)字(numeric)型變量的條件表達式,那么最好使用一條switch語句。例如,與其使用ド述代碼:if(x=1)printf("xisequaltoone.\n");elseif(x==2)printf("xisequaltotwo.\nH);elseif(x==3)printf("xisequaltothree.\n");elseprintf("xisnotequaltoone,two,orthree.\n");不如使用下述代碼,它更易于閱讀和維護:switch(x)

breakprintf("xisequaltoone.\nH);breakbreak;printf("xisequaltotwo.\nM);breakprintf('xisequaltothree.\nH);break;default:printf("xisnotequaltoone,two,orthree.\nH);break;)注意,使用switch語句的前提是條件表達式必須基于同一個數(shù)字型變量。例如,盡管下述if語句包含兩個以上的條件,但該例不能使用switch語句,因為該例基于字符串比較,而不是數(shù)字比較:char*name="Lupto";if(!stricmp(name?”Isaac"))printf("Yournamemeans'Laughter'.\n");elseif(!stricmp(name,"Amy"))printf("Yournamemeans'Beloved'.\n");elseif(!stricmp(name,"Lloyd"))printf("Yournamemeans'Mysterious'.\n");elseprintf("Ihaven'taclueastowhatyournamemeans.\n");請參見:1.4switch語句必須包含default分支嗎75switch語句的最后一個分支可以不要break語句嗎?1.4switch語句必須包含default分支嗎?不,但是為了進行錯誤檢査或邏輯檢査,還是應(yīng)該在switch語句中加入default分支。例如,下述switch語句完全合法:switch(char_code)(casetyt:case'y':printf("YouansweredYES!\n")breakcase'N':case'n':printf("YouansweredNO!\n");break)但是,如果一個未知字符被傳遞給這條switch語句,會出現(xiàn)什么情況呢?這時,程序?qū)]有任何輸出。因此,最好還是加入ー個default分支,以處理這種情況:default:printf("Unknownresponse:%d\n",char_code);此外,default分支能給邏輯檢查帶來很多方便。例如,如果用switch語句來處理數(shù)冃固定的條件,而且認為這些條件之外的值都屬于邏輯錯誤,那么可以加入ー個default分支來辨識邏輯錯誤。請看下列:voidmove_cursor(intdirection){switch(direction)(caseUP:cursor_up()breakcaseDOWN:cursor_down()breakcaseLEFT:cursor_left()breakcaseRIGHT:cursor_right()breakdefault: printf("Logicerroronlinenumber%ld!!!\n",_LINE_)break))請參見:3什么時候用一條switch語句比用多條if語句更好?1.5Switch語句的最后?個分支可以不要break語句嗎?1.5switch語句的最后一個分支可以不要break語句嗎?盡管switch語句的最后一個分支不一定需要break語句,但最好還是在switch語句的每個分支后面加上break語句,包括最后ー個分支。這樣做的主要原因是:你的程序很可能要讓另?個人來維護,他可能要增加一些新的分支,但沒有注意到最后ー個分支沒有break語句,結(jié)果使原來的最后ー個分支受到其后新増分支的干擾而失效。在毎個分支后面加上break語句將防止發(fā)生這種錯誤并增強程序的安全性。此外,目前大多數(shù)優(yōu)化編譯程序都會忽略最后一條break語句,所以加入這條語句不會影響程序的性能。請參見:1.3什么時候用一條switch語句比用多條if語句更好?4switch語句必須包含default分支嗎?1.6除了在for語句中之外,在哪些情況ド還要使用逗號運算符?逗號運算符通常用來分隔變量說明、函數(shù)參數(shù)、表達式以及for語句中的元素。下例給出了使用逗號的多種方式:#includc<stdio.h>

#include<stdlib.h>voidmain(void);voidmain()(/*Here,thecommaoperatorisusedtoseparatethreevariabledeclarations.*/inti,j,k;/*Noticehowyoucanusethecommaoperatortoperformmultipleinitializationsonthesameline.*/i=0,j=l,k=2;printf("i=%d,j=%d,k=%d\n”,i,j,k);/*Here,thecommaoperatorisusedtoexecutethreeexpressionsinoneline:assignktoi,incrementj,andincrementk.Thevaluethatireceivesisalwaystherightmostexpression.*/i=(j++,k++);printf("i=%d,j=%d,k=%d\n",i,j,k);/*Here,thewhilestatementusesthecommaoperatortoassignthevalueofiaswellastestit.*/while(i=(rand()%100),i!=50)printf("iis%d,tryingagain...\n",i)printf(H\nGuesswhat?iis50!\nH))請注意ド述語句:i:(j++?k++)這條語句?次完成了三個動作,依次為:(1)把k值賦給io這是因為左值(Ivaule)總是等于最右邊的參數(shù),本例的左值等于k。注意,本例的左值不等于k++,因為k++是ー個后綴自增表達式,在把k值賦給j之后kオ會門增。如果所用的表達式是++k,貝リ++k的值會被賦給i,因為++k是?個前綴自增表達式,k的自增發(fā)生在賦值操作之前。(2)j自增.(3)k自増。此外,還要注意看上去有點奇怪的while語句:while(i=(rand()%100),i!=50)printf("iis%d,tryingagain...\nH);這里,逗號運算符將兩個表達式隔開,while語句的每次循環(huán)都將計算這兩個表達式的值。逗號左邊是第個表達式,它把〇至99之間的一個隨機數(shù)賦給i!第二個表達式在while語句中更常見,它是ー個條件表達式,用來判斷i是否不等于50。while語句每一次循環(huán)都要賦予iー個新的隨機數(shù),并且檢査其值是否不等于50。最后,i將被隨機地賦值為50,而while語句也將結(jié)束循環(huán)。請參見:1.12運算符的優(yōu)先級總能保證是“自左至右’’或“自右至左’’的順序嗎?1.13++var和var++有什么區(qū)別?1.7怎樣才能知道循環(huán)是否提前結(jié)束了?循環(huán)通常依賴于?個或多個變量,你可以在循環(huán)外檢查這些變量,以確保循環(huán)被正確執(zhí)行。請看下例:intxchar*cp[REQUESTED_BLOCKS]/*Attempt(invain,Imustadd...)toallocate51210KBblocksinmemory.*/for(x=0;x<REQUESTED_BLOCKS;x++)(cpi[x]=(char*)maHoc(10000,1)if(cp[x]==(char*)NULL)break)/*IfxislessthanREQUESTED-BLOCKS,theloophasendedprematurely.*/if(x<REQUESTED_BLOCKS)printf("Bummer!Myloopendedprematurely!\nH);注意,如果上述循環(huán)執(zhí)行成功,它一定會循環(huán)512次。緊接著循環(huán)的if語句用來測試循環(huán)次數(shù),從而判斷循環(huán)是否提前結(jié)束。如果變量x的值小于512,就說明循環(huán)出錯了。1.8goto,longjmp()和setjmpO之間有什么區(qū)別?goto語句實現(xiàn)程序執(zhí)行中的近程跳轉(zhuǎn)(localjump),longjmp()和setjmpO函數(shù)實現(xiàn)程序執(zhí)行中的遠程跳轉(zhuǎn)(nonlocaljump,也叫faijump)。通常你應(yīng)該避免任何形式的執(zhí)行中跳轉(zhuǎn),因為在程序中使用goto語句或longjmp。函數(shù)不是ー種好的編程習(xí)慣。goto語句會跳過程序中的一段代碼并轉(zhuǎn)到ー個預(yù)先指定的位置。為了使用goto語句,你要預(yù)先指定?個有標(biāo)號的位置作為跳轉(zhuǎn)位置,這個位置必須與goto語句在同一個函數(shù)內(nèi)。在不同的函數(shù)之間是無法實現(xiàn)goto跳轉(zhuǎn)的。下面是ー個使用goto語句的例子:voidbad_programmers_function(void)(intxprintf("ExcusemewhileIcountto5000...\n");while(1)[printf("%d\n",x)gotoall_doneif(x=5000)gotoall_doneelsex=x+l;)all_done:prinft("Whew!Thatwasn'tsobad.wasit?\n");如果不使用got。語句,是例可以編寫得更好、下面就是ー個改進了實現(xiàn)的例子:voidbetter_function(void){intxprintf("ExcusemewhileIcountto5000...\n");for(x=l;x<=5000,x++)printf(M%d\n”,x)printf("Whew!Thatwasn'tsobad,wasit'An");)前面已經(jīng)提到,longjmpOftsetjmpO函數(shù)實現(xiàn)程序執(zhí)行中的遠程跳轉(zhuǎn)。當(dāng)你在程序中調(diào)用s的mp()時,程序當(dāng)前狀態(tài)將被保存到ー個jmp_buf類型的結(jié)構(gòu)中。此后,你可以通過調(diào)用longjmpO函數(shù)恢復(fù)到調(diào)用setjmp()時的程序狀態(tài)。與goto語句不同,longjmpO和s的mp()函數(shù)實現(xiàn)的跳轉(zhuǎn)不一定在同一個函數(shù)內(nèi)。然而,使用這兩個函數(shù)有一個很大的缺陷,當(dāng)程序恢復(fù)到它原來所保存的狀態(tài)時,它將失去對所有在longjmpO和setjmp()之間動態(tài)分配的內(nèi)存的控制,也就是說這將浪費所有在longjmpO和setjmpO之間用malloc()和calloc。分配所得的內(nèi)存,從而使程序的效率大大降低。因此,你應(yīng)該盡量避免使用longjmpO和setjmpO函數(shù),它們和goto語句ー樣,都是不良編程習(xí)慣的表現(xiàn)。ド面是使用longjmpO函數(shù)和setjmpO函數(shù)的一個例子:#include<stdio.h>#include<setjmp.h>jmp_bufsavcd_state;voidmain(void);voidcall_longjmp(void);voidmain(void)(intret_code;printf("Thecurrentstateoftheprogramisbeingsaved...\n");ret_code=setjmp(saved_state)if(ret_code==1){printff'Thelongjmpfunctionhasbeencalled.\n")printff'Theprogram'spreviousstatehasbeenrestored.\n");exit(O))prints"!amabouttocalllongjmpand\nM);printf('retumtothepreviousprogramstate...\n")call_longjmp()voidcall_longjmp(void){longjmp(saved_state,1)(1.9什么是左值(Ivaule)?左值是指可以被賦值的表達式。左值位于賦值語句的左側(cè),與其相對的右值(rvaule,見1.11)則位于賦值語句的右側(cè)。每條賦值語句都必須有一個左值和一個右值。左值必須是內(nèi)存中一個可存儲的變最,而不能是ー個常量。下面給出了一些左值的例子:intx;int*p_int;x=l;p_int=5;變量x是ー個整數(shù),它對應(yīng)于內(nèi)存中的一個可存儲位置,因此,在語句“x=ド中,x就是ー個左值。注意,在第二個賦值語句“*p_int=5”中,通過“*”修飾符訪問p_int所指向的內(nèi)存區(qū)域:因此,p_int是ー個左值。相反,下面的兒個例子就不是左值:#defineCONST.VAL10intx/*example1*/l=x;/*example2*/CONST.VAL=5;在上述兩條語句中,語句的左側(cè)都是ー個常量,其值不能改變,因為常量不表示內(nèi)存中可存儲的位置。因此,這兩條賦值語句中沒有左值,編譯程序會指出它們是錯誤的。請參見:1.10數(shù)組(array)可以是左值嗎?.1.1I什么是右值(rvaule)?I.10數(shù)組(array)可以是左值嗎?在1.9中,左值被定義為可被賦值的表達式。那么,數(shù)組是可被賦值的表達式嗎?不是,因為數(shù)組是由若干獨立的數(shù)組元素組成的,這些元素不能作為ー個整體被賦值。下述語句是、法

的:intx[5],y[5];x=y:不過,你可以通過for循環(huán)來遍歷數(shù)組中的每個元素,并分別對它們賦值,例如:inti;intx[5];inty[5];for(i=0;i<5,i++)x[i]=y[i];此外,你可能想?次拷貝整個數(shù)組,這可以通過象memcpyO這樣的函數(shù)來實現(xiàn),例如:memcpy(x,y,sizeof(y)):與數(shù)組不同,結(jié)構(gòu)(structure)可以作為左值。你可以把ー個結(jié)構(gòu)變量賦給另?個同類型的結(jié)構(gòu)變量,例如:typedefstructt_name(charlast_name[25]:charfirst_name[15];charmiddle-init[2];)NAMENAMEmy_name,your_name;your_name=my_name;在上例中,結(jié)構(gòu)變量my_name的全部內(nèi)容被拷貝到結(jié)構(gòu)變量your_name中,其作用和ド述語句是相同的:memcpy(your_name,my_name,sizeof(your_name);請參見:1.9什么是左值(Ivaule)?1.11什么是右值(rvaule)?1.11什么是右值(rvaule)?在1.9中,左值被定義為可被賦值的表達式,你也可以認為左值是出現(xiàn)在賦值語句左邊的表達式。這樣,右值就可以被定義為能賦值的表達式,它出現(xiàn)在賦值語句的右邊。與左值不同,右值可以是常量或表達式:例如:intX>y;x=1;/*1iSanrvalue,xisanlvalue*/在1.9中已經(jīng)介紹過,?條賦值語句必須有個左值和,個右值,因此,下述語句無法通過編譯,因為它缺少ー個右值:intx;x=void_function_call();/*the{unctionvoid-function-call()returnsnothing*/如果上例中的函數(shù)返回一個整數(shù),那么它可以被看作一個右值,因為它的返冋值可以存儲到左值X中。請參見:1.9什么是左值。vaule)?1.10數(shù)組可以是左值嗎?I.12運算符的優(yōu)先級總能保證是“自左至右”或“自右至左”的順序嗎?對這個問題的簡電回答是:這兩種順序都無法保證。C語言并不總是自左至右或自右至左求值,一般說來,它首先求函數(shù)值,其次求復(fù)雜表達式的值,最后求簡單表達式的值。此外,為了進ー步優(yōu)化代碼,目前流行的大多數(shù)C編譯程序常常會改變表達式的求值順序?因此,你應(yīng)該用括號明確地指定運算符的優(yōu)先級。例如,請看下述表達式:a=b+c/d/function—call()*5I:述表達式的求值順序非常模糊,你很可能得不到所要的結(jié)果,因此,你最好明確地指定運算符的優(yōu)先級:a=b+(((c/d)/function—call())*5)這樣,就能確保表達式被正確求值,而且編譯程序不會為了優(yōu)化代碼而重新安排運算符的優(yōu)先級了。1.13++var和var++有什么區(qū)別?“++”運算符被稱為白增運算符。如果“++”運算符出現(xiàn)在變量的前面(++var),那么在表達式使用變量之前,變量的值將增加1。如果“++”運算符出現(xiàn)在變量之后(var++),那么先對表達式求值,然后變量的值オ增加1。對自減運算符(-)來說,情況完全相同。如果運算符出現(xiàn)在變量的前面,則相應(yīng)的運算被稱為前綴運算:反之,則稱為后綴運算。例如,請看ー個使用后綴自增運算符的例子:intx,y;x=l;y=(x++*5);上例使用了后綴自增運算符,在求得表達式的值之后,x的值オ增加1,因此,y的值為1乘以5,等于5。在求得表達式的值之后,x自増為2。現(xiàn)在看ー個使用前綴自增運算符的例子:intX,y;K=l;y=(x+l);/*y=(x+l);/*(x+l)isanrvalue;yisanlvalue*/這個例子和前一個相同,只不過使用了前綴自增運算符,而不是后綴自增運算符,因此,X的值先增加1,變?yōu)?,然后オ求得表達式的值。這樣,y的值為2乘以5,等于10.14取模運算符(modulusoperatoヴ%”的作用是什么?取模運算符“%”的作用是求兩個數(shù)相除的余數(shù).例如,請看下面這段代碼:x=15/7;如果x是ー個整數(shù),x的值將為2.然而,如果用取模運算符代替除法運算符"/",得到的結(jié)果就不同了:X=15%7;這個表達式的結(jié)果為15除以7的余數(shù),等于1.這就是說,15除以7得2余1.取模運算符通常用來判斷?個數(shù)是否被另一個數(shù)整除.例如,如果你要打印字母表中序號為3的倍數(shù)的字母,你可以使用下面這段代碼:intx;for(x=l;x<=26;x++)if((x%3)==0)printf("%cH;x+64);上例將輸出字符串"cfilorux",即字母表中序號為3的倍數(shù)的所有字母。第2章變量和數(shù)據(jù)存儲C語言的強大功能之一是可以靈活地定義數(shù)據(jù)的存儲方式.C語言從兩個方面控制變量的性質(zhì):作用域(scope)和生存期(lifetime).作用域是指可以存取變量的代碼范圍,生存期是指可以存取變量的時間范圍.作用域有三種:extern(外部的)這是在函數(shù)外部定義的變量的缺省存儲方式.extern變量的作用域是整個程序.static(靜態(tài)的)在函數(shù)外部說明為static的變量的作用域為從定義點到該文件尾部:在函數(shù)內(nèi)部說明為static的變量的作用域為從定義點到該局部程序塊尾部.auto(自動的)這是在函數(shù)內(nèi)部說明的變量的缺省存儲方式.auto變量的作用域為從定義點到該局部程序塊尾部.變量的生存期也有三種,但它們不象作用域那樣有預(yù)定義的關(guān)鍵字名稱.第一種是extern和static變量的生存期,它從main()函數(shù)被調(diào)用之前開始,到程序退出時為止.第二種是函數(shù)參數(shù)和auto變策的生存期,它從函數(shù)調(diào)用時開始,到函數(shù)返回時為止.第三種是動態(tài)分配的數(shù)據(jù)的生存期,它從程序調(diào)用malloc()或calloc()為數(shù)據(jù)分配存儲空間時開始,到程序調(diào)用68()或程序退出時為止.1變量存儲在內(nèi)存(memory)中的什么地方?變量可以存儲在內(nèi)存中的不同地方,這依賴了它們的生存期。在函數(shù)外部定義的變量(全局變量或靜態(tài)外部變量)和在函數(shù)內(nèi)部定義的static變量,其生存期就是程序運行的全過程,這些變量被存儲在數(shù)據(jù)段(datasegment)中。數(shù)據(jù)段是在內(nèi)存中為這些變量留出的一段大小固定的空間,它分為兩部分,一部分用來存放初始化變量,另?部分用來存放未初始化變量.在函數(shù)內(nèi)部定義的auto變量(沒有用關(guān)鍵字static定義的變量)的生存期從程序開始執(zhí)行其所在的程序塊代碼時開始,到程序離開該程序塊時為止。作為函數(shù)參數(shù)的變量只在調(diào)用該函數(shù)期間存在.這些變量被存儲在棧(stack)中.棧是內(nèi)存中的一段空間,開始很小,以后逐漸自動增大,直到達到某個預(yù)定義的界限。在象DOS這樣的沒有虛擬內(nèi)存(virtualmemory)的系統(tǒng)中,這個界限由系統(tǒng)決定,并且通常非常大,因此程序員不必擔(dān)心用盡??臻g。關(guān)于虛擬內(nèi)存的討論,請參見2.3.第三種(也是最后ー種)內(nèi)存空間實際上并不存儲變量,但是可以用來存儲變量所指向的數(shù)據(jù)。如果把調(diào)用malloc。函數(shù)的結(jié)果賦給ー個指針變量,那么這個指針變量將包含ー塊動態(tài)分配的內(nèi)存的地址,這塊內(nèi)存位于?段名為“堆(heap)”的內(nèi)存空間中。堆開始時也很小,但當(dāng)程序員調(diào)用malk)c()或calloc()等內(nèi)存分配函數(shù)時它就會增大.堆可以和數(shù)據(jù)段或棧共用ー個內(nèi)存段(memorysegment),也"J以有它自己的內(nèi)存段,這完全取決于編譯選項和操作系統(tǒng)。與棧相似,堆也有一個增長界限,并且決定這個界限的規(guī)則與棧相同.請參見:1什么是局部程序塊(lOcalblock)?2變量必須初始化嗎?2.3什么是頁抖動(pagethrashing)?7.20什么是棧(stack)?7.21什么是堆(heap)7.2.2變量必須初始化嗎?不.使用變量之前應(yīng)該給變量一個值,ー個好的編譯程序?qū)椭惆l(fā)現(xiàn)那些還沒有被給定一個值就被使用的變量。不過,變量不?定需要初始化.在函數(shù)外部定義的變量或者在函數(shù)內(nèi)部用static關(guān)鍵字定義的變量(被定義在數(shù)據(jù)段中的那些變量,見2.1)在沒有明確地被程序初始化之前都已被系統(tǒng)初始化為〇了.在函數(shù)內(nèi)部或程序塊內(nèi)部定義的不帶static關(guān)鍵ア的變量都是自動變量,如果你沒有明確地初始化這些變量,它們就會具有未定義值。如果你沒有初始化一個自動變量,在使用它之前你就必須保證先給它賦值.調(diào)用malloc。函數(shù)從堆中分配到的空間也包含未定義的數(shù)據(jù),因此在使用它之前必須先進行初始化,但調(diào)用calloc()函數(shù)分配到的空間在分配時就已經(jīng)被初始化為。了。請參見:1.1什么是局部程序塊(lOcalblock)?7.20什么是棧(stack)?7.21什么是堆(heap)?2.3什么是頁抖動(pagethrashing)?有些操作系統(tǒng)(如UNIX和増強模式ド的Windows)使用虛擬內(nèi)存,這是種使機器的作業(yè)地址空間大于實際內(nèi)存的技術(shù),它是通過用磁盤空間模擬RAM(random—accessmemory)來實現(xiàn)的。在80386和更高級的IntelCPU芯片中,在現(xiàn)有的大多數(shù)其它微處理器(如Motorola68030,spare和PowerPC)中,都有一個被稱為內(nèi)存管理單元(MemoryManagementUnit,縮寫為MMU)的器件。MMU把內(nèi)存看作是由?系歹『’頁(page)'’組成的來處理。ー頁內(nèi)存是指ー個具有?定大小的連續(xù)的內(nèi)存塊,通常為4096或8192字節(jié)。操作系統(tǒng)為每個正在運行的程序建立并維護ー張被稱為進程內(nèi)存映射(ProcessMemoryMap,縮與為PMM)的表,表中記錄了程序可以存取的所有內(nèi)存頁以及它們的實際位置。每當(dāng)程序存取ー塊內(nèi)存時,它會把相應(yīng)的地址(虛擬地址,virtualaddress)傳送給MMU,MMU會在PMM中查找這塊內(nèi)存的實際位置(物理地址,physicaladdress),物理地址可以是由操作系統(tǒng)指定的在內(nèi)存中或磁盤I:的任何位置。如果程序要存取的位置在磁盤1?就必須把包含該地址的頁從磁盤上讀到內(nèi)存中,并且必須更新PMM以反映這個變化(這被稱為pagefault,即頁錯)。希望你繼續(xù)讀ド去,因為ド面就要介紹其中的難點了。存取磁盤比存取RAM要慢得多,所以操作系統(tǒng)會試圖在RAM中保持盡量多的虛擬內(nèi)存。如果你在運行?個非常大的程序(或者同時運行幾個小程序),那么可能沒有足夠的RAM來承擔(dān)程序要使用的全部內(nèi)存,因此必須把ー些頁從RAM中移到磁盤1:(這被為pagingout,即頁出)。操作系統(tǒng)會試圖去判斷哪些頁可能暫時不會被使用(通?;谶^去使用內(nèi)存的情況),如果它判斷錯了,或者程序正在很多地方存取很多內(nèi)存,那么為了讀入已調(diào)出的頁,就會產(chǎn)生大量頁錯動作。因為RAM已被全部使用,所以為了調(diào)入要存取的ー頁,必須調(diào)出另ー頁,而這將導(dǎo)致更多的頁錯動作,因為此時不同的ー頁已被移到磁盤上。在短時間內(nèi)出現(xiàn)大量頁錯動作的情形被稱為頁抖動,它將大大降低系統(tǒng)的執(zhí)行效率。頻繁存取內(nèi)存中大量散布的位置的程序更容易在系統(tǒng)中造成頁抖動。如果同時運行許多小程序,而實際上已經(jīng)不再使用這些程序,也很容易造成頁抖動。為了減少頁抖動,你應(yīng)該減少同時運行的程序的數(shù)冃。對了大的程序,你應(yīng)該改變它的工作方式,以盡量使操作系統(tǒng)能準確地判斷出哪些頁不再需要。為此,你可以使用髙速緩沖存儲技術(shù),或者改變用于大型數(shù)據(jù)結(jié)構(gòu)的查找算法,或者使用效率更髙的malloc。函數(shù)。當(dāng)然,你也可以考慮增加系統(tǒng)的RAM,以減少頁出動作。請參見:7.17怎樣說明?個大于640KB的數(shù)組?7.21什么是堆(heap)?18.14怎樣才能使DOS程序獲得超過64KB的可用內(nèi)存?21.31Windows是怎樣組織內(nèi)存的?2.4什么是const指針?如果希望ー個變量在被初始化后其值不會被修改,程序員就會通過cons,修飾符和編譯程序達成默契。編譯程序會努力去保證這種默契——它將禁止程序中出現(xiàn)對說明為const的變量:進行修改的代碼。const指針的準確提法應(yīng)該是指向const數(shù)據(jù)的指針,即它所指向的數(shù)據(jù)不能被修改。只要在指針說明的開頭加入const修飾符,就可說明一個cosnt指針。盡管const指針?biāo)赶虻臄?shù)據(jù)不能被修改,但cosnt指針本身是可以修改的。下而給出了const指針的ー些合法和非法的用法例子:constchar*str="hello";charc=*str;/*legal*/str++; /*legal*/*str=*a*; /*illegal*/str[l]=b;/"illegal*/前兩條語句是合法的,因為它們沒有修改str所指向的數(shù)據(jù):后兩條語句是非法的,因為它們要修改str所指向的數(shù)據(jù)。在說明函數(shù)參數(shù)時,常常要使用const指針。例如,一個計算字符串長度的函數(shù)不必改變字符串內(nèi)容,它可以寫成這樣:my_strlen(constchar*str){intcount=0;while(*str++)(count++;)returncount;)注意,如果有必要,ー個非const指針可以被隱式地轉(zhuǎn)換為consl指針,但ー個const指針不能被轉(zhuǎn)換成非const指針。這就是說,在調(diào)用my_strlen()時,它的參數(shù)既可以是ー個const指針,也可以是?個非const指針。請參見:2.7ー個變量可以同時被說明為const和volatile嗎?2.8什么時候應(yīng)該使用const修飾符?2.14什么時候不應(yīng)該使用類型強制轉(zhuǎn)換(typecasり?2.18用const說明常量有什么好處?2.5什么時候應(yīng)該使用register修飾符沱真的有用嗎?代gister修飾符暗示編譯程序相應(yīng)的變量將被頻繁使用,如果可能的話,應(yīng)將其保存在CPU的寄存器中,以加快其存取速度。但是,使用register修飾符有"幾點限制。首先,register變量必須是能被CPU寄存器所接受的類型。這通常意味著register變量必須是?個單個的值,并且其長度應(yīng)小于或等于整型的長度。但是,有些機器的寄存器也能存放浮點數(shù)。其次,因為register變量可能不存放在內(nèi)存中,所以不能用取址運算符“&”來獲取register變量的地址。如果你試圖這樣做,編譯程序就會報告這是ー個錯誤。register修飾符的用處有多大還受其它?些規(guī)則的影響。因為寄存器的數(shù)量是有限的,而且某些寄存器只能接受特定類型的數(shù)據(jù)(如指針和浮點數(shù)),因此,.真正能起作用的register修飾符的數(shù)目和類型都依賴于運行程序的機器,而任何多余的register修飾符都將被編譯程序所忽略。在某些情況ド,把變量保存在寄存器中反而會降低運行速度,因為被占用的寄存器不能再用于其它目的,或一者變量被使用的次數(shù)不夠多,不足以抵消裝入和存儲變量所帶來的額外開銷。那么,什么時候應(yīng)該使用register修飾符呢?回答是,對現(xiàn)有的大多數(shù)編譯程序來說,永遠不要使用register修飾符。早.期的C編譯程序不會把變量保存在寄存器中,除非你命令它這樣做,這時register修飾符是C語言的一種很有價值的補充。然而,隨著編譯程序設(shè)計技術(shù)的進步,在決定哪些變量應(yīng)該被存到寄存器中時,現(xiàn)在的C編譯程序能比程序員作出更好的決定。實際上,許多C編譯程序會忽略register修飾符,因為盡管它完全合法,但它僅僅是暗示而不是命令。在極罕見的情況下,程序運行速度很慢,而你也知道這是因為有一個變故被存儲在內(nèi)存中,也許你地后會試圖在該變量前面加上register修飾符,但是,如果這并沒有加快程序的運行速度,你也不要感到奇怪。請參見:6什么時候應(yīng)該使用volatile修飾符?2.6什么時候應(yīng)該使用volatile修飾符?volatile修飾符告訴編譯程序不要對該變量所參與的操作進行某些優(yōu)化。在兩種特殊的情況下需要使用volatile修飾符:第ー種情況涉及到內(nèi)存映射硬件(memory-mappedhardware,如圖形適配器,這類設(shè)備對計算機來說就好象是內(nèi)存的?部分?樣),第二種情況涉及到共享內(nèi)存(sharedmemory,即被兩個以上同時運行的程序所使用的內(nèi)存)。大多數(shù)計算機擁有一系列寄存器,其存取速度比計算機主存更快。好的編譯程序能進行ー種被稱為“冗余裝入和存儲的刪去”(redundantloadandstoreremoval)的優(yōu)化,即編譯程序會,在程序中尋找并刪去這樣兩類代碼:?類是可以刪去的從內(nèi)存裝入數(shù)據(jù)的指令,因為相應(yīng)的數(shù)據(jù)已經(jīng)被存放在寄存器中;另ー種是可以刪去的將數(shù)據(jù)存入內(nèi)存的指令,因為相應(yīng)的數(shù)據(jù)在再次被改變之前可以一直保留在寄存器中。如果ー個指針變量指向普通內(nèi)存以外的位置,如指向ー個外圍設(shè)備的內(nèi)存映射端口,那么冗余裝入和存儲的優(yōu)化對它來說可能是有害的。例如,為了調(diào)整某個操作的時間,可能會用到下述函數(shù):time_ttime_addition(volatileconststructtimer*t,inta),fintnintxtime_tthenx=O;then=t->valuefor(n=O;n<1000;n++)(x=x+a;)returnt->value-then;)在上述函數(shù)中,變量"〉value實際上是ー個硬件計數(shù)器,其值隨時間增加。該函數(shù)執(zhí)行1000次把a值加到x上的操作,然后返回t->value在這1000次加法的執(zhí)行期間所增加的值。如果不使用volatile修飾符,一個聰明的編譯程序可能就會認為t->value在該函數(shù)執(zhí)行期間不會改變,因為該函數(shù)內(nèi)沒有明確地改變t->value的語句。這樣,編譯程序就會認為沒有必要再次從內(nèi)存中讀入t->value并將其減去then,因為答案永遠是〇。因此,編譯程序可能會對該函數(shù)進行“優(yōu)化’’,結(jié)果使得該函數(shù)的返回值永遠是〇。如果ー個指針變量指向共享內(nèi)存中的數(shù)據(jù),那么冗余裝入和存儲的優(yōu)化對它來說可能也是有害的,共享內(nèi)存通常用來實現(xiàn)兩個程序之間的互相通訊,即讓?個程序把數(shù)據(jù)存到共享的那塊內(nèi)存中,而讓另ー個程序從這塊內(nèi)存中讀數(shù)據(jù)。如果從共享內(nèi)存裝入數(shù)據(jù)或把數(shù)據(jù)存入共享內(nèi)存的代碼被編譯程序優(yōu)化掉了,程序之間的通訊就會受到影響。請參見:2.7 ?個變量可以同時被說明為const和volatile嗎?2.14什么時候不應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?2.7ー個變量可以同時被說明為consl和volalile嗎?可以。const修飾符的含義是變量的值不能被使用了const修飾符的那段代碼修改,但這并不意味著它不能被這段代碼以外的其它手段修改。例如,在2.6的例ア中,通過,個volatileconst指針t來存取timer結(jié)構(gòu)。函數(shù)time_addition()本身并不修改t->value的值,因此t->value被說明為const〇不過,計算機的硬件會修改這個值,因此t->vakie又被說明為volatile。如果同時用const和volatile來說明一個變量,那么這兩個修飾符隨便哪個在先都行,請參見:2.6什么時候應(yīng)該使用volatile修飾符?2.8什么時候應(yīng)該使用const修飾符?2.14什么時候不應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?8什么時候應(yīng)該使用const修飾符?使用const修飾符有幾個原因,第一個原因是這樣能使編譯程序找出程序中不小心改變變量值的錯誤。請看下例:while(*str=O)/*programmermeanttowrite*str!=0*/(/*somecodehere*/strq++;J其中的,』,,符號是輸入錯誤。如果在說明str時沒有使用const修飾符,那么相應(yīng)的程序能通過編譯但不能被正確執(zhí)行。第二個原因是效率。如果編譯程序知道某個變量不會被修改,那么它可能會對生成的代碼進行某些優(yōu)化。如果?個函數(shù)參數(shù)是?個指針,并且你不希望它所指向的數(shù)據(jù)被該函數(shù)或該函數(shù)所調(diào)用的函數(shù)修改,那么你應(yīng)該把該參數(shù)說明為const指針。如果ー個函數(shù)參數(shù)通過值(而不是通過指針)被傳遞給函數(shù),并且你不希望其值被該函數(shù)所調(diào)用的函數(shù)修改,那么你應(yīng)該把該參數(shù)說明為const。然而,在實際編程中,只有在編譯程序通過指針存取這些數(shù)據(jù)的效率比拷貝這些數(shù)據(jù)更高時,オ把這些參數(shù)說明為const。請參見:7一個變量可以同時被說明為const和volatile嗎?14什么時候不應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?18用const說明常量有什么好處?2.9浮點數(shù)比較(floating-pointcomparisons)的可靠性如何?浮點數(shù)是計算機編程中的“魔法(blackart)”,原因之一是沒有一種理想的方式可以表示一個任意的數(shù)字。電子電氣工程協(xié)會(IEEE)已經(jīng)制定出浮點數(shù)的表示標(biāo)準,但你不能保證所使用的每臺機器都遵循這ー標(biāo)準。即使你使用的機器遵循這ー標(biāo)準,還存在更深的問題。從數(shù)學(xué)意義上講,兩個不同的數(shù)字之間有無窮個實數(shù)。計算機只能區(qū)分至少有一位(bi。不同的兩個數(shù)字。如果要表示那些無窮無盡的各不相同的數(shù)字,就要使用無窮數(shù)目的位。計算機只能用較少的位(通常是32位或64位)來表示一-個很大的范圍內(nèi)的數(shù)字,因此它只能近似地表示大多數(shù)數(shù)字。由于浮點數(shù)是如此難對付,因此比較ー個浮點數(shù)和某個值是否相等或不等通常是不好的編程習(xí)慣。但是,判斷一個浮點數(shù)是否大于或小于某個值就安全多了。例如,如果你想以較小的步長依次使用ー個范圍內(nèi)的數(shù)字,你可能會編寫這樣ー個程序:#include<stdio.h>constfloatfirst=0.0;constfloatlast=70.0constfloatsmall=0.007main()(floatf;for(f=first;f!=last&&f<last+l.O;f+=small)printf("fisnow%g\n",f);)然而,舍入誤差(roundingerror)和變量small的表示誤差可能導(dǎo)致f永遠不等于last(f可能會從稍小于last的ー個數(shù)增加到ー個稍大于last的數(shù)),這樣,循環(huán)會跳過last。加入不等式"fvlast+1.0”就是為了防止在這種情況發(fā)生后程序繼續(xù)運行很長時間。如果運行該程序并且被打印出來的f值是71或更大的數(shù)值,就說明已經(jīng)發(fā)生了這種情況。ー種較安全的方法是用不等式"fvlast”作為條件來終止循環(huán),例如:floatf;fbr(f=first;fclast;f+=small)你甚至可以預(yù)先算出循環(huán)次數(shù),然后通過這個整數(shù)進行循環(huán)計數(shù):floatf;intcount=(last-first)/small;fbr(f=first;count—>0;f+=small)請參見:2.11對不同類型的變量進行算術(shù)運算會有問題嗎?2.10怎樣判斷ー個數(shù)字型變量可以容納的最大值?要判斷某種特定類型可以容納的最大值或最小值,?種簡便的方法是使用ANSI標(biāo)準頭文件limits.h中的預(yù)定義值。該文件包含一些很有用的常量,它們定義了各種類型所能容納的值,下表列出了這些常量:CHAR—BITchar的位數(shù)(biりCHAR—MAX char的十進制整數(shù)最大值CHAR—MINchar的十進制整數(shù)最小值MB—LEN—MAX多字節(jié)字符的最大字節(jié)(byte)數(shù)1NT—MAXint的十進制最大值1NT—MIN int的十進制最小值LONG—MAX long的十進制最大值LONG—MIN long的十進制最小值SCHAR—MAX signedchar的十進制整數(shù)最大值SCHAR—MIN signedchar的卜進制整數(shù)最小值SHRT—MIN short的十進制最小值SHRT—MAX short的十進制最大值UCHAR—MAX unsignedchar的十進制整數(shù)最大值UINT—MAX unsignedint的十進制最大值ULONG—MAX unsignedlongint的十進制最大值USHRT—MAX unsignedshortint的十進制最大值對于整數(shù)類型,在使用2的補碼運算的機器(你將使用的機器幾乎都屬此類)上,ー個有符號類型可以容納的數(shù)字范圍為ー2’小一到(+2,屈」),?個無符號類型可以容納的數(shù)字范圍為0到(+2?題ー1)。例如,?個16位有符號整數(shù)可以容納的數(shù)字范圍為-2ム(即ー32768)到(+2應(yīng)1)(即+32767)。請參見:10.I用什么方法存儲標(biāo)志(flag)效率最高?10.2什么是“位屏幕(bitmasking)”?10.616位和32位的數(shù)是怎樣存儲的?11對不同類型的變量進行算術(shù)運算會有問題嗎?C有三類固有的數(shù)據(jù)類型:指針類型、整數(shù)類型和浮點類型:指針類型的運算限制最嚴,只限于以下兩種運算:?兩個指針相減,僅在兩個指針指向同一數(shù)組中的元素時有效。運算結(jié)果與對應(yīng)于兩個指針的數(shù)組下標(biāo)相減的結(jié)果相同。+指針和整數(shù)類型相加I。運算結(jié)果為ー個指針,該指針與原指針之間相距n個元素,n就是與原指針相加的整數(shù).浮點類型包括float,double和longdouble這三種同有類型。整數(shù)類型包括char,unsignedchar,short,unsignedshort,int,unsignedint,long和unsignedlong。對這些類型都可進行以下4種算術(shù)運算:+加-減?乘/除對整數(shù)類型不僅可以進行上述4種運算,還可進行以下幾種運算:%取模或求余?右移?左移&按位與!按位或ヘ按位異或!邏輯非取反盡管C允許你使用“混合模式’’的表達式(包含不同類型的算術(shù)表達式),但是,在進行運算之前,它會把不同的類型轉(zhuǎn)換成同一類型(前面提到的指針運算除外)。這種自動轉(zhuǎn)換類型的過程被稱為“運算符升級(operatorpromotion)".請參見:2.12什么是運算符升級(operatorpromotion)?2.12什么是運算符升級(operatorpromotion)?當(dāng)兩個不同類型的運算分量(operand)進行運算時,它們會被轉(zhuǎn)換為能容納它們的最小的類型,并且運算結(jié)果也是這種類型。下表列出了其中的規(guī)則,在應(yīng)用這些規(guī)則時,你應(yīng)該從表的頂端開始往下尋找,直到找到第一條適用的規(guī)則。運算分量1運算分量2轉(zhuǎn)換結(jié)果longdouble 其它任何類型 !ongdoubledouble 任何更小的類型 doublefloat 任何更小的類 floatunsignedlong任何整數(shù)類 unsignedlonglong unsigned>LONG_MAXunsignedlonglong 任何更小的類型longunsigned 任何有符號類型 unsigned下面的程序中就有幾個運算符升級的例子。變量n被賦值為3/4,因為3和4都是整數(shù),所以先進行整數(shù)除法運算,結(jié)果為整數(shù)〇。變量f2被賦值為3/4.0,因為4.0是ー個float類型,所以整數(shù)3也被轉(zhuǎn)換為float類型,結(jié)果為float類型0.75.#include<stdio.h>main()floatfl=3/4;floatf2=3/4.0printf(M3/4==%gor%gdependingonthetypeused.\n",fl,也);)請參見:2.11對不同類型的變量進行算術(shù)運算會有問題嗎?2.13什么時候應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?2.13什么時候應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?在兩種情況ド需要使用類型強制轉(zhuǎn)換。第一種情況是改變運算分量的類型,從而使運算能正確地進行。下面的程序與2.12中的例子相似,但有不同之處。變量n被賦值為整數(shù)i除以整數(shù)j的結(jié)果,因為是整數(shù)相除,所以結(jié)果為〇。變量f2也被賦值為i除以j的結(jié)果,但本例通過(float)類型強制轉(zhuǎn)換把i轉(zhuǎn)換成一個"oat類型,因此執(zhí)行的是浮點數(shù)除法運算(見2.11),結(jié)果為0.75〇#include<stdio.h>main()(inti=3;intj=4floatfl=i/j;floatf2=(float)i/j;printf(u3/4=%gor%gdependingonthetypeused.\n",fl,f2);)第二種情況是在指針類型和void?類型之間進行強制轉(zhuǎn)換,從而與期望或返回void指針的函數(shù)進行正確的交接。例如,下述語句就把函數(shù)malloc()的返回值強制轉(zhuǎn)換為ー個指向foo結(jié)構(gòu)的指針:structfoo*p=(structfoo*)malloc(sizeof(structfoo));請參見:2.6什么時候應(yīng)該使用volatile修飾符?2.8什么時候應(yīng)該使用const修飾符?2.11對不同類型的變量進行算術(shù)運算會有問題嗎?2.12什么是運算符升級(operatorpromotion)?2.14什么時候不應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?7.5什么是void指針?7.6什么時候使用void指針?7.21什么是堆(heap)?7.27可以對void指針進行算術(shù)運算嗎?2.14什么時候不應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?不應(yīng)該對用const或volatile說明了的對象進行類型強制轉(zhuǎn)換,否則程序就不能正確運行。不應(yīng)該用類型強制轉(zhuǎn)換把指向ー?種結(jié)構(gòu)類型或數(shù)據(jù)類型的指針轉(zhuǎn)換成指向另ー種結(jié)構(gòu)類型或數(shù)據(jù)類型的指針。在極少數(shù)需要進行這種類型強制轉(zhuǎn)換的情況下,用共用體(union)來存放有關(guān)數(shù)據(jù)能更清楚地表達程序員的意圖。請參見:2.6什么時候應(yīng)該使用volatile修飾符?2.8什么時候應(yīng)該使用const修飾符?2.15可以在頭文件中說明或定義變量嗎?被多個文件存取的全局變量可以并且應(yīng)該在?個頭文件中說明,并且必須在ー個源文件中定義。變量不應(yīng)該在頭文件中定義,因為ー個頭文件可能被多個源文件包含,而這將導(dǎo)致變量被多次定義。如果變量的初始化只發(fā)生一次,ANSIC標(biāo)準允許變量有多次外部定義;但是,這樣做沒有任何好處,因此最好避免這樣做,以使程序有更強的可移植性。注意:變量的說明和定義是兩個不同的概念,在2.16中將講解兩者之間的區(qū)別。僅供ー個文件使用的“全局''變量應(yīng)該被說明為static,而且不應(yīng)該出現(xiàn)在頭文件中。請參見:2.16說明一個變量和定義ー個變量有什么區(qū)別?2.17可以在頭文件中說明static變量嗎?2.16說明一個變量和定義ー個變量有什么區(qū)別?說明個變量意味著向編譯程序描述變量的類型,但并不為變量分配存儲空間。定義?個變量意味著在說明變量的同時還要為變量分配存儲空間。在定義?個變量的同時還可以對變量進行初始化。下例說明了一個變量和一個結(jié)構(gòu),定義了兩個變量,其中一個定義帶初始化:externintdecll;/*thisisadeclaration*/structdecl2{intmember;};/*thisjustdeclaresthetype—novariablementioned*/intdefl=8; /*thisisadefinition*/intdef2;/*thisisadefinition*/換句話說,說明一個變最相當(dāng)于告訴編譯程序”在程序的某個位置將用到ー個變量,這里給出了它的名稱和類型”,定義ー個變量則相當(dāng)于告訴編譯程序“具有這個名稱和這種類型的變量就在這里"。ー個變量可以被說明許多次,但只能被定義一次。因此,不應(yīng)該在頭文件中定義變量,因為ー個頭文件可能會被ー個程序的許多源文件所包含。請參見;2.17可以在頭文件中說明static變量嗎?2.17可以在頭文件中說明static變量嗎?如果說明了一個static變量,就必須在同一個文件中定義該變量(因為存儲類型修飾符static和extern是互斥的)。你可以在頭文件中定義?個static變量,但這會使包含該頭文件的源文件都得到該變量的ー份私有拷貝,而這通常不是你想得到的結(jié)果。請參見:2.16說明一個變量和定義ー個變量有什么區(qū)別?2.18用const說明常量有什么好處?使用關(guān)鍵字const有兩個好處:第一,如果編譯程序知道ー個變量的值不會改變,編譯程.序就能對程序進行優(yōu)化;第二,編譯程序會試圖保證該變量的值不會因為程序員的疏忽而被改變。當(dāng)然,用#define來定義常量也有同樣的好處。用const而不用#define來定義常量的原因是const變量可以是任何類型(如結(jié)構(gòu),而用#define定義的常量不能表示結(jié)構(gòu))。此外,const變量是真正的變量,它有可供使用的地址,并且該地址是唯一的(有些編譯程序在每次使用用#define定義的字符串時都會生成一份新的拷貝,見9.9)。請參見:2.7ー個變量可以同時被說明為const和volatile嗎?2.8什么時候應(yīng)該使用con3t修飾符?2.14什么時候不應(yīng)該使用類型強制轉(zhuǎn)換(typecast)?9.9字符串和數(shù)組有什么不同?第3章排序與查找在計算機科學(xué)中,排序(soning)是研究得最多的問題之ー,許多書籍都深入討論了這個問題。本章僅僅是一個介紹,重點放在C語言的實際應(yīng)用上。排序程序員可以使用的基本排序算法有5種:?插入排序(insertionsort.)?交換排序(exchangesOrt)?選擇排序(selectionsort)?歸并排序(mergesort)?分布排序(distributionsort)為了形象地解釋每種排序算法是怎樣工作的,讓我們來看?看怎樣用這些方法對桌上一付亂序的牌進行排序。牌既要按花色排序(依次為梅花、方塊、紅桃和黑心),還要按點數(shù)排序(從2到A).插入排序的過程為:從一堆牌的上面開始拿牌,每次拿一張牌,按排序原則把牌放到手中正確的位置。桌上的牌拿完后,手中的牌也就排好序了。交換排序的過程為:(1)先拿兩張牌放到手中。如果左邊的牌要排在右邊的牌的后面,就交換這兩張牌的位置。(2)然后拿ドー張牌,并比較最右邊兩張牌,如果有必要就交換這兩張牌的位置。(3)重復(fù)第Q)步,直到把所有的牌都拿到手中。(4)如果不再需要交換手中任何兩張牌的位置,就說明牌已經(jīng)排好序了;否則,把手中的牌放到桌上,重復(fù)(1)至(4)步,直到手中的牌排好序。選擇排序的過程為;在桌上的牌中找出最小的一張牌,拿在手中;重復(fù)這種操作,直到把所有牌都拿在手中。歸并排序的過程為:把桌上的牌分為52堆,每堆為張牌。因為每堆牌都是有序的(記住,此時毎堆中只有一張牌),所以如果把相鄰的兩堆牌合并為一堆,并對每堆牌進行排序,就可以得到26堆已排好序的牌,此時每一堆中有兩張牌。重復(fù)這種合并操作,就可以依次得到13堆牌(每一堆中有4張牌),7堆牌(有6堆是8張牌,還有一堆是4張牌),最后將得到52張的一堆牌。分布排序(也被稱作radixsort,即基數(shù)排序)的過程為;先將牌按點數(shù)分成13堆,然后將這!3堆牌按點數(shù)順序疊在ー起;再將牌按花色分成4堆,然后將這4堆牌按花色順序疊在ー起,牌就排好序了。在選用排序算法時,你還需要了解以ド幾個術(shù)語:(1)自然的(natural)如果某種排序算法對有序的數(shù)據(jù)排序速度較快(工作量變小),對無序的數(shù)據(jù)排序速度卻較慢(工作變量大),我們就稱這種排序算法是自然的。如果數(shù)據(jù)已接近有序,就需要考慮選用自然的排序算法。(2)穩(wěn)定的(stable)如果某種排序算法能保持它認為相等的數(shù)據(jù)的前后順序,我們就稱這種排序算法是穩(wěn)定的。例如,現(xiàn)有以下名單:MaryJonesMarySmithTomJonesSusieQueue如果用穩(wěn)定的排序算法按姓對上述名單進行排序,那么在排好序后"MaryJones"和"TomJones”將保持原來的Jr順序,因為它們的姓是相同的。穩(wěn)定的排序算法可按主、次關(guān)鍵字對數(shù)據(jù)進行排序,例如按姓和名排序(換句話說,主要按姓排序,但對姓相同的數(shù)據(jù)還要按名排序)。在具體實現(xiàn)時,就是先按次關(guān)鍵字排序,再按主關(guān)鍵字排序。(3)內(nèi)部排序(internalsort)和外部排序(externalsort)待排數(shù)據(jù)全部在內(nèi)存中的排序方法被稱為內(nèi)部排序,待排數(shù)據(jù)在磁盤、磁帶和其它外存中的排序方法被稱為外部排序。査找和排序算法?樣,查找(seairhing)算法也是計算機科學(xué)中研究得最多的問題之一。查找算法和排序算法是有聯(lián)系的,因為許多査找算法依賴于要査找的數(shù)據(jù)集的有序程度?;镜臇苏易鸱ㄓ幸韵?種:?順序査找(sequentialsearching)??比較查找(comparisonsearching)?基數(shù)査找(radixsearching)?哈希查找(hashing)下而仍然以ー付亂序的牌為例來描述這些算法的工作過程。順序査找的過程為:從第一張開始査看每ー張牌,直到找到要找的牌。比較查找(也被稱作binarysearching,即折半查找)要求牌已經(jīng)排好序,其過程為:任意抽?張牌,如果這張牌正是要找的牌,則查找過程結(jié)束。如果抽出的這張牌比要找的牌大,則在它前面的牌中重復(fù)査找操作:反之,則在它后面的牌中重復(fù)査找操作,直到找到要找的牌。基數(shù)査找的過程為:先將牌按點數(shù)分成13堆,或者按花色分成4堆。然后找出與要找的牌的點數(shù)或花色相同的那?堆牌,再在這堆牌中用任意?種查找算法找到要找的牌。哈希查找的過程為:(1)在東面上留出可以放若干堆牌的空間,并構(gòu)造ー個函數(shù),使其能根據(jù)點數(shù)和花色將牌映射到特定的堆中(這個函數(shù)被稱為hashfunction,即哈希函數(shù))。(2)根據(jù)哈希函數(shù)將牌分成若干堆。(3)根據(jù)哈希函數(shù)找到要找的牌所在的堆,然后在這ー堆牌中找到要找的牌。例如,可以構(gòu)造這樣ー個哈希函數(shù):pile=rank+suit其中,rank是表示牌的點數(shù)的一個數(shù)值;suit是表示牌的花色的一個數(shù)值:pile表示堆值,它將決定一張牌歸入到哪ー堆中。如果用1,2 13分別表示A,2, K,用0,1,2和3分別表示梅花、方塊、紅桃和黑桃,則pile的值將為1,2 16.這樣就可以把ー付牌分成16堆。哈希查找雖然看I:去有些離譜,但它確實是一種非常實用的查找算法。各種各樣的程序,從壓縮程序(如Stacker)到磁盤高速緩存程序(如SmartDrive),幾乎都通過這種方法來提髙查找速度,排序或査找的性能有關(guān)排序和查找的?個主:要問題就是速度。這個問題經(jīng)常被人們忽視,因為與程序的其余部分相比,排序或查找所花費的時間幾乎可以被忽略。然而,對大多數(shù)排序或查找應(yīng)用來說,你不必一開始就花很多精力去編制一段算法程序,而應(yīng)該先在現(xiàn)成的算法中選用一種最簡單的(見3.1和3.4)(當(dāng)你發(fā)現(xiàn)所用的算法使程序運行很慢時,再換用ー種更好的算法(請參見卜文中的介紹)。下面介紹ー種判斷排序或查找算法的速度的方法。首先,引入一個算法的復(fù)雜度的概念,它指的是在各種情況(最好的、最差的和平均的)下排序或査找需要完成的操作次數(shù),通過它可以比較不同算法的性能。算法的復(fù)雜度與排序或査找所針對的數(shù)據(jù)集的數(shù)據(jù)量有關(guān),因此,引入?個基于數(shù)據(jù)集數(shù)據(jù)量的表達式來表示算法的復(fù)雜度。最快的算法的復(fù)雜度0(1),它表示算法的操作次數(shù)與數(shù)據(jù)量無關(guān)。復(fù)雜度O(N)(N表示數(shù)據(jù)集的數(shù)據(jù)量)表示算法的操作次數(shù)與數(shù)據(jù)量直接相關(guān)。復(fù)雜度O(logN)介于上述兩者之間,它表示算法的操作次數(shù)與數(shù)據(jù)量的對數(shù)有關(guān)。復(fù)雜度為O(NlogN)(N乘以logN)的算法比復(fù)雜度為O(N)的算法要慢,而復(fù)雜度為(XNラ的算法更慢。注意:如果兩種算法的復(fù)雜度都是O(logN),那么logN的基數(shù)較大的算法的速度要快些,在本章的例子中,logN的基數(shù)均為10。表3.I本章所有算法的復(fù)雜度算法最好情況平均情況最壞情況快速排序O(NlogN)O(NlogN)O(N2)歸并排序0(N)O(NlogN)O(NlogN)基數(shù)排序0(N)O(N)0(N)線性查找0(N)折半查找O(NlogN)哈希查找O(N/M)*健樹查找0(1)***M是哈希表項的數(shù)目**實際上相當(dāng)于有232個哈希表項的哈希查找表3.1列出了本章所有算法的復(fù)雜度。對于排序算法,去中給出了最好的、平均的和最差的情況下的復(fù)雜度,平均情況是指數(shù)據(jù)隨機排列的情況;排序算法的復(fù)雜度視數(shù)據(jù)的初始排列情況而定,它一般介于最好的和最差的兩種情況之間。對于査找算法,表中只給出了平均情況ド的復(fù)雜度,在最好的情況(即要找的數(shù)據(jù)恰好在第一次査找的位置)ド,査找算法的復(fù)雜度顯然是0(1);在最壞的情況(即要找的數(shù)據(jù)不在數(shù)據(jù)集中)下,查找算法的復(fù)雜度通常與平均情況下的復(fù)雜度相同。需要注意的是,算法的復(fù)雜度只表示當(dāng)N值變大時算法的速度變慢的程度,它并不表示算法應(yīng)用于給定大小的數(shù)據(jù)集時的實際速度。算法的實際速度與多種因素有關(guān),包括數(shù)據(jù)集的數(shù)據(jù)類型以及所用的編程語言、編譯程序和計算機等。換句話說,與復(fù)雜度高的算法相比,復(fù)雜度低的算法并不具備絕對的優(yōu)越性。實際上,算法的復(fù)雜度的真正意義在于,當(dāng)N值大于某ー數(shù)值后,復(fù)雜度低的算法就會明顯比復(fù)雜度髙的算法快。為了說明算法的復(fù)雜度和算法的實際執(zhí)行時間之間的關(guān)系,表3.2列出了本章所有例子程序的執(zhí)行時間。本章所有例子程序均在一臺以Linux為操作系統(tǒng)的9()MHz奔騰計算機上由GNUC編譯程序編譯,在其它操作系統(tǒng)中,這些例子程序的執(zhí)行時間與表3.2所列的時間是成比例的。表3.2本章所有例子程序的執(zhí)行時間例子程序算法20004000600080001000013例3.1qsort()0.020.050.070.110,例3.2a快速排序0.020.070.130.180.20例3.2b歸并排序0.030.080.140.180.26例3.2c基數(shù)排序0.000.39例3.4bsearch()0.370.390.390.400.41例3.5折半查找0.320.340.340.360.3651例3.6線性査找9.6720.6828.7136.3145.例3.7鍵樹査找0.270.280.290.290.30例3.8哈希査找0.250.260.280.290.28注意:(1)表中所列的時間以秒為單位。(2)表中所列的時間經(jīng)過統(tǒng)ー處理,只包括排序或査找所花費的時間。(3)2000等數(shù)值表示數(shù)據(jù)集的數(shù)據(jù)量。(4)數(shù)據(jù)集中的數(shù)據(jù)是從文件/usr/man/manl/gcc.I(GNUC編譯程序中的一個文件)中隨機提取的詞。(5)在查找算法中,要查找的數(shù)據(jù)是從文件/usr/man/manl/g++.MGNUC++編譯程序中的一個文件)中隨機提取的詞。(6)函數(shù)qsort()和bseareh。分別是C標(biāo)準庫函數(shù)中用了快速排序算法和折半查找算法的函數(shù),其余例子程序是專門為本章編り的。在閱讀完以上內(nèi)容后,你應(yīng)該能初步體會到如何根據(jù)不同的情況來選用一種合適的排序或査找算法。在DonaldE.Knuth所著的《TheArtOfComputerProgramming,Volume3,SortingandSearching》ー書中,作者對排序和查找算法進行了全面的介紹,在該書中你將讀到更多關(guān)丁?復(fù)雜度和復(fù)雜度理論的內(nèi)容,并且能見到比本章中所提到的更多的算法。公用代碼本章中的許多例子程序是可以直接編譯運行的。在這些例子程序中,許多代科是相同的,這些相同的代碼將統(tǒng)一在本章的末尾列出。1哪ー種排序方法最方便?答案是C標(biāo)準庫函數(shù)qsortO,理由有以下三點:(1)該函數(shù)是現(xiàn)成的;(2)該函數(shù)是已通過調(diào)試的;(3)該函數(shù)通常是已充分優(yōu)化過的。qsort()函數(shù)通常使用快速排序算法,該算法是由C.A.R.Hoare于1962年提出的。以下是qsort()函數(shù)的原型:voidqsort(void*buf,size_thum,size_tsize,int(*comp)(constvoid*elel,constvoid*ele2));qsort()函數(shù)通過一個指針指向一個數(shù)組(buf),該數(shù)組的元素為用戶定義的數(shù)據(jù),數(shù)組的元素個數(shù)為num,每個元素的字節(jié)長度都為size?數(shù)組元素的排序是通過調(diào)用指針comp所指向的?個函數(shù)來實現(xiàn)的,該函數(shù)對數(shù)組中由elei和ele2所指向的兩個元素進行比較,并根據(jù)前者是小于、等了或大于后者而返回ー個小于、等于或大于〇的值。例3.1中給出了一個函數(shù)sortStrings(),該函數(shù)就是通過qsort()函數(shù)對一個以NULL指針結(jié)束的字符串?dāng)?shù)組進行排序的。將例3.I所示的代碼和本章結(jié)尾的有關(guān)代碼一起編譯成一個可執(zhí)行程序后,就能按字母順序?qū)?個以NULL指針結(jié)束的字符串?dāng)?shù)組進行排序了。l:#include<stdlib.h>2:3:/**ThisroutineisusedonlybysortStrings(),toprovid

溫馨提示

  • 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)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論