第三章C語言程序的控製結構
C語言的最大特點之一是結構化,結構化使程序結構清晰、易讀性強,可以提高程序設計的質量和效率。結構化程序由若幹基本結構組成,每一個基本結構可以包含一個或若幹個語句。在C語言中,有三種基本結構:順序結構、選擇結構、循環結構,本章將分別講述這三種基本結構。
3.1 算法及結構化程序設計
3.1.1 算法及其特征
當今計算機的迅猛發展已使軟件開發步入工程化階段。一個大型軟件的開發一般需要經曆“規劃”、“要求分析”、“設計”、“編碼”、“測試”和“運行維護”等幾個階段。它就象是一個大型的工程。其中,正確的“需求分析”對程序設計是至關重要的,而“編碼”就是對需求的具體實現。在“編碼”過程中,算法是非常重要的,良好的算法,往往會得到事半功倍的效果,算法思想決定了程序的質量和性能。
而目前許多軟件的開發不僅需要處理大量的關係複雜的數據,這些數據通常都具有一定的結構性。因此,軟件設計的實質就是設計合適的數據結構和基於這個數據結構的算法。於是著名的計算機專家N.沃恩教授提出了一個著名的公式:
程序=數據結構+算法
算法是程序設計中一個非常重要的概念,所謂算法就是處理問題的方法和步驟。更確切地說,算法是為解決特定的問題而要一步一步執行的有窮操作的描述。
一個完整的算法應具有如下特征:
?有窮性
任何一個算法都必須能在執行有限步之後結束。例如:
N!=1*2*3*…*(N-1)*N
其中N是下一個特定數,例如50,則這個描述可稱之為一個算法。又如:
sum=1+2+3+…+N+…
上式就不能稱之為算法,因為該描述在執行有限步後仍不能結束,它隻能稱之為一個計算方法。
?確定性
算法的每一步執行,其順序和內容都必須有確切的規定,不能有二義性。
?可執行性
可執行性是指算法的所有操作都是能做到的,即稱之為可操作性。
?0個或多個輸入;1個或多個輸出
算法既然是為解決特定問題而設計的,那麼它至少包含一個輸出步驟,來告知處理結果。無任何輸出信息的算法是無意義的。
為了更深入地理解算法,先看幾個有關算法的實例。
【例3-1】解一元二次方程的算法。
設有一元二次方程a*x*x+b*x+c=0,其中a、b和c是不等於0的實數。解此方程可按如下的方法和步驟:
(1)首先計算p=b*b-4*a*b*c。
(2)判斷p是否>=0若結果為“真”則執行步驟(3),若結果為“假”,則執行步驟(4)。
(3)計算二實根
x1=(-b+sqrt(p))/(2*a)
x2=(-b-sqrt(p))/(2*a)
結束計算。
(4)計算複根
x1=-b/(2*a)+isqrt(-p)/(2*a)
x2=-b(2*a)+ isqrt(-p)/(2*a)
結束計算。
以上四步就是解一元二次方程的一個算法。其中sqrt()是求平方根函數,其程序如下:
# include < stdio.h >
main()
{float a,b,c,x1,x2,p,q,q1,q2;
scanf(" % f% f% f",a,b,c);
p=b * b - 4 * a *c;
if(p > =0)
{
x1=(-b +sprt(p))/(2 * a); / *求實根 * /
x2-(-b -sprt(p))/(2 * a);
printf(" x1= % f \ n",x1);
printf(" x2=% f \ n",x2);
}
else
{
q1= -b/(2 * a); / * 求虛根 * /
q2=sqrt(-p)/(2 * a);
printf(" x1= % f + i% f \ n",q1,a2);
printf(" x2= % f -I% f \ n",q1,a2);
}
}
3.1.2 算法和類型與結構
計算機可處理的算法,一般歸納為“數值算法”和“非數值算法”兩類。“數值算法”常用於科學計算,而“非數值算法”則廣泛用於各類數據的數據處理,它常常要涉及大量數據和複雜的數據結構。不管是何種算法,都是由“結構”和“原操作”所構成。最基本的結構有順序、分支和循環三種,原操作包括輸入、輸出、表達式求值、變量賦值、比較兩個變量等等。下麵分別描述三種基本結構。
1.順序結構
順序結構是由一組順序執行的程序塊所組成的,每一個程序塊可以是一個非轉移語句、分支語句、循環語句或這些語句的排列、嵌套,它是任何一個算法都離不開的一個基本主體結構,它是由若幹順序執行的處理塊所組成。例如有三個處理塊所構成的順序結構如圖3-1所示。
圖3-1 順序結構示意圖
2.分支結構
分支結構是根據分支條件的取值來決定程序執行的走向,分支結構的示意圖如圖3-2所示:
圖3-2 分支結構示意圖
分支結構也是一種基本和常用的一種結構,它向我們提供了根據條件取值來選擇不同處理塊的方法。
3.循環結構
循環結構是一種對某一處理塊反複執行指定次數的結構,圖3-3是循環結構的示意圖,循環結構又分為“當循環”和“直到型循環”,“當循環”的特點是先判斷,後執行循環體;“直到型循環”是先執行,後判斷。圖3-3a和圖3-3b分別是“當循環”和“直到型循環”的示意圖。
a) b)
圖3-3 循環結構示意圖
循環中反複處理部分叫循環體,循環次數由條件取值決定。
從軟件工程的觀點來看,一個結構化的程序遠比非結構化的程序質量高。由於結構化的程序具有結構清晰、可讀性強、易維護的特點,而非結構化程序是程序設計者隨意施展自己技能所構成的程序,所以這樣的程序可讀性相對來說就差一些,而且不易維護。因此,當前廣泛采用結構化程序設計方法。結構化程序設計的實質是將問題按“自頂向下,逐步求精”的原則進行分解,使其分解成若幹較小的問題,然後再用結構化編碼技術編製出各個較小問題的程序塊,進而構造出整個問題的求解程序。
結構化程序設計是以有條理的方式構造程序的一種技術。一個結構化程序的構造應符合以下幾條規則:
(1)任何一個程序單位(例如一個函數)都隻能由順序、分支和循環三種基本結構所組成。
(2)任何一個程序單位可視為若幹程序塊排列而成。
(3)每個程序塊隻能有一個入口和一個出口。
(4)下列任何一個程序結構都可視為一個程序塊。
?一個或若幹個不進行轉移的執行語句(視為順序結構)是一個程序塊。
?一個如下形式的分支或switch結構(視為分支結構)是一個程序塊。
if…else…
switch…case…
?一個如下形式的循環結構,也視為一個程序塊。
while…
do…while
for(e1;e2;e3)
?由若幹程序塊按順序排列所構成的結構也視為一個程序塊。所以,一個結構化程序實質上是由有限個順序、分支和循環三種基本結構經排列、嵌套而成。
編寫結構化程序應注意如下兩點:
(1)要保證結構的完整性
不允許結構層次之間的交叉,例如圖3-4所示的結構是符合完整性要求的,而圖3-5則不符合。因此,循環結構和循環結構、分支結構與分支結構、循環與分支結構的交叉都是不準許的。
圖3-4 符合完整性要求的結構
圖3-5 不符合完整性要求的結構
(2)操作上的完整性
對於結構化的程序設計,一個基本結構就是下一個完整的操作單元,程序隻能從入口進入,出口退出,不能從其外部進入內部,也就是說,一個程序結構不能有多個入口和多個出口。但是程序設計語言往往並不限定操作上的完整性,例如C語言,在循環內部可用goto語句退出循環(或exit()結束等),但這是不符合結構化程序設計的完整性要求的。
【例3-2】語法上是合法的,但不符合結構化程序設計的完整性要求。
float a;
…
scanf(" % f,& a);
sum = 0;
while(a ! = 0.0)
{
sum + =a;
if(sum > =100)
goto endloop; / * 從內部退出循環,不符合結構化程序設計的完整性要求 * /
scanf("% f",&a);
}
……
endloop:
printf("sum= % d",sum);
…
因此,圖3-6是符合操作完整性的,而圖3-7是不符合的。
圖3-6 符合完整性要求的結構 圖3-7 不符合完整性要求的結構
3.2 順序結構程序設計
順序結構的程序設計是最簡單的程序設計。順序結構的程序是由一組順序執行的程序塊所組成。最簡單的程序塊是由若幹個順序執行的語句所構成。這些語句可以是賦值語句,輸入/輸出語句等。為此,我們首先介紹賦值語句和簡單的輸入/輸出語句,然後說明順序程序設計的方法。
3.2.1 賦值語句
以分號結尾的賦值表達式叫表達式語句,也叫賦值語句。例如:
a=b+c-d*e;
在賦值語句中,首先計算等號左邊的表達式的值,然後將其值賦給等號左邊的變量。如果等號右邊的表達式的類型與左邊變量的類型不一致,係統將自動把等號右邊的表達式的值轉換為與左邊變量相同的類型,然後再賦值。
3.2.2 順序程序設計及舉例
計算機處理的問題難易程序差別很大。簡單問題的程序,其結構可能完全是順序的,即在執行時,是順序逐句執行。而複雜問題的程序則不僅包含順序結構,還可能包含分支結構和循環結構。本節主要介紹順序結構的程序設計方法。下麵通過一些簡單的順序結構程序設計的例子,使讀者了解並總結順序結構程序設計的方法與特點。
【例3-3】從鍵盤上輸入一個小寫字母,要求用其對應的大寫字母輸出。
算法設計:解決問題分為三步:輸入一個小寫字母,將小寫字母轉換為對應的大寫字母,輸出轉換後的大寫字母。其程序如下:
# include < stdio.h > / * 嵌入頭文件 * /
main()
{
char c1,c2 / * 定義字符型變量c1和c2 * /
printf("please input a Lower-caae:"); / * 輸出提示語句 * /
c1 = getchar(); / * 從鍵盤接收一個字符 */
printf(" \ n Lower-case is % c",c1);
c2 = c1 - 32; / * 將小寫字母通過ASCII值的計算轉換成大寫 */
printf( " \ n upper - case is % c",c2); / * 輸出轉換後的結果 * /
}
運行時的輸出情況如下:
please input a Lower-case:'d'<CR>
Lower-case is d
upper-case is D
【例3-4】從鍵盤分別輸入兩個複數的實部和虛部,求它們的和、差、積、商並分別在屏幕上輸出。
算法設計:每個複數均有實部和虛部,所以輸入兩個複數實際上是輸入四個實數,然後按數學公式順序地求出它們的和、差、積、商(每個結果都是兩個實數)並使用printf函數按複數的格式打印出來。應該注意的是,既可以定義若幹個變量來存放計算的結果,也可以將計算的表達式放在printf函數的變量列表中直接打印出來。其程序如下:
# include
main()
{float a,b,c,d,resultR,resultI;
printf("please input the first complex number: \ n");
scanf(" % f % f,&a,&b); / * 輸入第一個複數的實部和虛部 */
printf("please input the second complex number: \ n");
scanf(" % f % f",&c,&d); / * 輸入第二年複數的實部虛部 * /
printf("the sum of them is % f + % f i\ n",a + c,b+d); / *求兩實數的和並打印 * /
printf("the difference between them is % f + % f i \ n",a-c,b-d);
resultR=a * c-b * d;
resultI=a * d + b * c; / * 求兩實數乘積的實部和虛部* /
printf("the mass of them is % f+ % f i \ n",resultR,resultI)
resultR=(a * c + b *d)/(c * c + d * d);
resultR=(b * c - a *d)/(c * c + d * d);
printf( "the quotient of them is % f+ % f i \ n",resultR,resultI);
}
執行情況如下:
please input the first complex number:
3.4 5.6<CR>
please input the second complex number:
7.8 -99.1<CR>
the sum of them is 11.200000+-93.499999;
the difference of them is -4.400000+104.699998;
the mass of them is 581.479980+-293.260010;
the quotient of ehem is -0.053477+0.038518;
【例3-5】從鍵盤輸入一個字符,求出其前後相鄰的兩個字符,然後按由大到小的順序輸出這三個字符及對應的ASCII碼。
算法設計:輸入字符的前麵下一個字符,其ASCII碼比此字符小1,同樣,後一個字符的ASCII碼比此字符大1,對字符型變量進行算術運算時,使用的正是它們的ASCII碼,所以直接將輸入的字符加1或減1,就可以得到它前後的相鄰字符。輸出時,使用格式控製符%c可輸出字符本身,而使用%d則可輸出字符對應的ASCII碼。其程序如下:
# include < stdio.h >
main()
{
char c,cf,cb; / * 定義字符型變量* /
printf(" \ n please input a character:");
c =getchar(); / *從鍵盤讀入字符* /
cf =c - 1; / *求前導字符 * /
cb =c + 1; / * 求後續字符 * /
printf(" % c % c % c\ n",cf,c,cb); / * 輸出字符 * /
printf(" % d % d % d\ n",cf,c,cb); 輸出字符的ASCII值 * /
}
執行情況如下:
please input a character:m<CR>
1 m n
108 109 110
從上述三個例子中,我們可以看到編程的關鍵是設計算法。一個簡單的問題,其處理算法比較簡單,我們一下子就能把它的算法設計出來。但對處理較複雜問題的算法,就很難一下子分析得十分透徹,而需要逐步分析清楚。即先從總體上分析其粗略的算法框架,再對粗略算法的每一步進一步細化,如此下去,直至細化到每一步都足夠簡單,能用一個或幾個C的語句來實現。於是就出現算法的頂層設計,第二層設計…。這是十分有效而且常用的算法設計方法,即“自頂向下,逐步求精”的設計方法。
【例3-6】從鍵盤輸入一個正三棱錐的邊長、分別計算其表麵積和體積,並打印輸出,輸入輸出時都要有提示信息,輸出結果隻保留3位小數。其程序如下:
# include < stdio.h >
# include < math.h >
main()
{float x;
printf("please input thegth of the edge of the pyramid:\ n");
scanf(" % f",& x);
printf("the total area of the sruface is % -8.3f\ n",pow(3,0,5)*x * x);
/ * 求出並打印表麵積。* /
printf("the volume of the pyramid is % -8.3f\ n",pow(2,0,5)*pow(x,3)/12);
/ * 求出並打印體積。* /
}
由於使用了標準數學函數,所以應在程序開頭添加包含命令#include
3.3 分支結構程序設計
與順序結構一樣,分支結構也是程序的基本結構之一,也是常用的一種結構。所謂分支結構,就是根據不同的條件,選擇不同的處理塊(或程序塊,或分程序)。例如,對於形如aX2-bX+c的方程,應根據係數a是否等於零分別當作一次方程和二次方程求解。因此,分支結構又叫條件分支結構。
在C語言中,條件分支結構可通過if語句和switch語句實現。if語句有if、if-else和if-else if三種形式。
3.3.1 If-else分支
If-else分支是標準形式的條件分支,其執行流程如圖3-8所示。
圖3-8 if-else條件分支
其語句形式為:
if(條件表達式c)
{
程序段sl;
{
else
{
程序段s2;
}
其中,程序段sl,s2可以是語句或者複合語句。
if-else語句的處理過程是,先計算if後麵的條件表達式c;若結果為非0值(即邏輯上為真),則執行程序段sl,否則執行程序段s2。可見,if-else是二選一的分支結構。
【例3-7】輸入一個字符,判斷它是否是數字。
main()
{
char c;
printf("\ nplease input a character:");
c =getchar() / * 從鍵盤輸入一個字符 * /
if(c > =48 & & c < =57) / * 根據輸入字符的ASCII碼判斷其是否是一個數字* /
printf("It is a number. \ n"); / * 若是數字,輸出相應的提示 * /
else
printf("It is not a number.\ n"); / * 若不是數字,也輸出相應的提示 * /
}
該程序執行時,當輸入數字時,則顯示“It is a number.”,否則顯示“It is not a number.”。if後麵用圓括號“(”和“)”括住的表達式經常是條件表達式或邏輯表達式(但可以是任何表達式),表達式必須用圓括號括住。
3.3.2 if分支
if分支是if-else分支的特殊情況,其執行流程如圖3-9所示。
圖3-9 if分支的流程圖
其程序形式如下:
if(c) 或 if(! c)
程序段s; 程序段s;
其中c是條件表達式。
if分支實際是if-else分支形式的簡化,若if-else的兩個分支中的一個為空,則可以寫成if分支的形式:
if(c) 或 if(c)
程序段s; ;
else else
; 程序段s;
【例3-8】判斷輸入的字符是否是小寫字母。
main()
{
char c;
printf(" \ n Input a character:");
c =getchar();
if(c > =97 & &c < =122) / * 判斷所輸入的字符是否是小寫字母 * /
printf("Yes,it a small letter ! \ n");
}
3.3.3 條件分支的嵌套
進行程序設計時,經常要用到條件分支嵌套。所謂條件分支嵌套就是在一個分支中可以嵌套另一個分支。例如,在下麵的條件分支中:
if(條件表達式c)
程序段s1;
else
程序段s2;
程序段s1和s2中又可包含條件分支。利用條件分支嵌套可以實現多分支控製。下麵是一個條件分支嵌套的例子。
【例3-9】從鍵盤輸入一串字符,將其中的大寫字母轉換成小寫字母,小寫字母保留不變,其他字符忽略後輸出。
# include < stdio.h >
main()
{char c;
printf("input a $: \ n");
while((c =getchar()) ! _' \ n")
{
if(c > =97 & &c < =122)
putchar(c);
else
{
if(c > =65 & &c < =90)
putchar(c+32);
else;
}
}
}
在此例中,內嵌的花括號可以省略。因為它僅括住了一個語句,即if-else語句。如果被括住的是多個語句,則花括號不能省略。在多個分支嵌套中,如果缺省花括號時,要特別注意if和else的配對關係,一個else總是與其上麵距它最近的,並且沒有其他else與其配對的if相配對,例如,有如下的嵌套形式:
if(條件表達式c1) (1)
if(條件表達式c2) (2)
程序段s1; (3)
else
if(條件表達式c3) (4)
程序段s2; (5)
else (6)
程序段s3;
其if和else的配對關係應當如圖3-10所示。
圖3-10 if與else配對關係
由圖3-10可以看出,整個嵌套形式相當於:
if(條件表達式c1)
{if(條件表達式c2)
程序段s1;
else
{ if(條件表達式c3)
程序段s2;
else
程序段s3;
}
}
使用括號可以改變if和else的配對關係,比如,想讓if(1)和else(3)配對,可采用如下方法:
if(條件表達式c1) (1)
{if(條件表達式c2) (2)
程序段s1;
else
{ if(條件表達式c3) (3)
程序段s2; (4)
else(5)
程序段s3;
其實,為了使程序清晰易讀並減少出錯幾率,建議;對所有的嵌套if…else語句組,都使用括號來明確其配對關係。
3.3.4 if-else if結構
if-else if結構是條件分支嵌套常用的一種形式,其一般形式如下:
if(條件表達式c1)
程序段s1;
else if(條件表達式c2)
程序段s2;
else if(條件表達式c3)
程序段s3;
……
else if(條件表達式cn)
程序段sn+1;
該結構實質上是if-else分支的多層嵌套。因為如果嵌套的層數過多,會使程序寫的很靠右,因此,把它簡化為if-else結構。其流程是從條件表達式c1開始向下逐一判斷,一旦滿足(即條件ci的邏輯值為1),則執行對應的程序段si,若所有的條件表達式的值都為零,就執行sn+1語句。圖3-11是if-else if結構的示意圖。
圖3-11 if-else if結構的流程圖
【例3-10】有一個函數,定義如下:
請使用if…else if…else…語句編寫程序,根據用戶輸入的自變量x的值,計算函數值。
程序如下:
# include < stdio.h >
main()
{float x;
printf(" input the value of x:");
scanf(" % f",&x);
if (x <0.0)
printf("y =0 \ n");
else if(x > =0.0& &x < =10.0)
printf("y =% .2f \ n",x);
else
printf("y = % .2f \ n",x * x * + 1);
}
應該注意的是,一般情況下,嵌套if…else語句if…else…if…else…語句和下麵將要講到的switch語句之間是可以相互轉換的。
3.3.5 開關(switch)分支結構
開關分支是分支結構的另一種形式,執行時它根據條件的取值為選擇程序中的一個分支。
其程序形式如下:
switch(表達式c)
{
case 常量表達式c1:
程序段s1;
break;
…
case 常量表達式s2;
程序段s2;
break;
…
case 常量表達式cn:
程序段sn;
break;
default:
程序段sn+1;
break;
}
其執行流程是首先計算表達式的值,然後判斷表達式的值與c1、c2、…cn中的哪個值相等,若與某個ci值相等,則執行其下的si語句組。若不與任何一個ci值相等,則執行sn+1語句組。在執行某一分支中的語句組時,遇break語句則退出switch-case結構,即程序控製轉移至該結構中花括號之後的語句,圖3-12是開關分支的流程圖。