前一章雖然是針對變數與式子做說明,不過範例程式的一些地方if跟while也有登場,本章將以包含if跟while的「制御構造」為中心做學習。所謂制御構造,可以想成決定C語言句子的實行順序。如果可以好好理解的話,就可以用C語言寫出像程式的東西了。至今為止不得不記起來的東西並不多,請好好學習。
9.1 程式的執行由3個構造決定
程式除了數值計算的部份以外,大部分是由制御構造組成。以寫的順序執行句子式子(逐一執行)、根據條件分歧處理的內容(分歧)、或是重複執行(重複),這些都是制御構造; 所以制御構造跟程式有密不可分的關係。
另外,要用程式語言實做演算法(algorithm)的話,只要會使用「逐一執行」「分歧」「重複」就足夠了。實際上,雖然使用函數等等的概念可以讓我們寫出更漂亮的程式,但是並非不知道就不能寫程式了,制御構造則是不知道不行,制御構造就是這麼重要。
9.2 筆直前進的逐一執行
照程式所寫一句一句執行,叫做「逐一執行」。關於逐一執行,就是從上往下的執行,寫的時候也是這樣的順序。比如說寫了
a = 1;
b = 2;
就會照順序先給a代入1,之後再給b代入2。分歧跟重複不用的場合下,請記得逐一執行就是照順序執行。下圖是三種制御構造的圖示。
9.3 依條件分開的分歧 if else
關於分歧,用的是if。if的意思是「如果」,用起來就像「如果是~那就」這樣的意思。
「如果是~那就」的 if
最基本的if文法就像下面這樣:
if(條件){
//處理}
條件是以式子記述下來。一般來說,條件式就是是否相等、大於小於這樣。在第八章也有說明過,C語言的式子全部都有值,像1 + 1這式子就有2這個值。C語言的if,式子的值是0的話就是偽,反之為真。像下面的式子就可以沒問題的通過編譯。
if (1 + 1){
printf("true\n");
}
只是,這樣寫的話分歧就沒有意義了(因為必定為真,一定會執行),要注意。
「如果不是,那就」的 else
除了if的「如果是~那就」之外,還有「如果是~那就...,如果不是就...」的記述。這種時候,就要用到else。
if(條件){
//處理}else{
//處理}
此外,else還可以追加條件。這種情況,就用下面這樣記述:
if(條件1){
//處理}else if (條件2){
//處理}else if (條件3){
//處理}else{
//處理}
目前為止的範例程式有if的登場,但else只有在這裡說明而已,來實驗看看範例程式吧。
source code
if_else.c
#include
int main(){
int a = 1;
if(a == 1){
printf("a is 1\n");
}
else if(a == 2){
printf("a is 2\n");
}
else{
printf("a is neither 1 nor 2\n");
}
return 0;
}
執行結果:
a is 1
範例中把1代入a裡面,所以畫面顯示了「a is 1」。改一下a的初期值在實驗看看吧。
block的意義
if在分歧的處理會用{}括起來,這叫做block,因為if條條件成立之後的處理可能有好幾句。但是只有一句的話,就不需要{}括起來,如下:
if (a == 1)
printf("a is 1\n");
雖然看起來有減少行數的好處,但其中有些陷阱。
恐怖實驗: 沒有block的if
不用block的if會有怎樣的陷阱呢?來實驗看看吧。首先把剛剛的if_else.c不用block看看吧。
source code
if_else_2.c
#include
int main(){
int a = 1;
if (a == 1)
printf("a is 1\n");
else if(a == 2)
printf("a is 2\n");
else
printf("a is neither 1 nor 2\n");
return 0;
}
執行過後可以發現它跟原本的程式行為是一樣的。那麼,我們把printf("a is 1\n");註解掉,變成//printf("a is 1\n");。所謂註解掉,是用在第六章介紹過的//或/**/把程式中的句子無效化。請記住,//還有/**/除了寫註釋以外,還可以在實驗時暫時把句子無效化。
在編譯的時候,應該會出現以下錯誤訊息:
if_else_2.c: In function ‘main’:
if_else_2.c:8:2: error: expected expression before ‘else’
else if(a == 2)
^
會有這錯誤訊息是因為if接下來一定要有一個執行的句子或是一個block,但是我們把printf("a is 1\n");註解掉,突然接else了。對於compiler來說,這個程式長成底下這個樣子:
if (a == 1)
else if(a == 2)
如果是if_else.c的話,把printf("a is 1\n");註解掉也沒事的,因為block({})還在。總之if不用block的話,就容易有這些想不到的錯誤。特別是新手,if(或其他的制御文),推薦一定要用block。舉個同樣不危險的例子,來考慮下面:
source code:
if_else_3.c
#include
int main(){
int a = 1;
if(a != 0)
printf("a is %d\n", a);
return 0;
}
執行結果:
a is 1
如果在printf("a is %d\n", a);的後面加上一句printf("a have a value\n");的話,如下:
if(a != 0)
printf("a is %d\n", a);
printf("a have a value\n");
雖然不會有error,但是這程式的意義跟我們所想像的不同。
if(a != 0)所處理的分歧,在沒有block的前提下,就只會處理到printf("a is %d\n", a);這一句。用block寫的簡單易懂一點,就像下面這樣:
if(a != 0){
printf("a is %d\n", a);
}
printf("a have a value\n");
犯下這種錯誤的話,因為compiler不會出現錯誤,所以執行時可能會覺得「好像哪裡奇怪阿」的煩惱。
9.4 基本的重複 (while)
利用電腦強力的情報處理能力,可以用很快的速度重複同一個處理。「重複」有while, do while, for三個種類,有各自的特徵。while在英語是「當~」的意思,在滿足條件的時候,進行重複的處理。
基本的while
了解if的話,文法是一樣的所以很簡單。只要像下面這樣寫,在滿足條件的時候,處理都會重複。
while (條件){
//處理}
因為在第七章的範例已經出現過while,所以來試試不一樣的例子吧。下面是求出1加到10的範例程式。喜歡數學的人的話會知道這題可以用等差數列的公式簡單的求出,這裡是展現重複的範例,所以用while。
順代一提,使用公式的話是(10*(1+10))/2來求出。
source code
while.c
#include
int main(){
int answer;
int cnt;
answer = 0;
cnt = 0;
while(cnt < 10){ //> answer = answer + (cnt + 1);
cnt = cnt + 1;
}
printf("answer = %d\n", answer);
return 0;
}
執行結果:
answer = 55
再詳細的說明一下這程式吧。變數answer跟cnt在while開始之前都是0。所以在一開始0 < 10是滿足的,就會執行block內的指令。block裡面,是把answer加上cnt再加上1代入answer。也就是說,第一次是answer = 0 + (0 + 1)。注意,()是不需要的,只是為了好懂加上去的而已。
接下來cnt的值加1,再次檢查while內的條件是否滿足。因為1 < 10,依然滿足條件,所以block內的東西還會再實行一次,直到cnt < 10為止。
這個while執行的過程以表格整理的話就像下面這樣。
answer =
cnt =
代入answer的式子
0
0
0 + (0 + 1)
1
1
1 + (1 + 1)
3
2
3 + (2 + 1)
6
3
6 + (3 + 1)
10
4
10 + (4 + 1)
15
5
15 + (5 + 1)
21
6
21 + (6 + 1)
28
7
28 + (7 + 1)
36
8
36 + (8 + 1)
45
9
45 + (9 + 1)
55
10
不滿足cnt < 10,所以不再執行block
對answer代入的式子,就是1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10的照順序加下去。因為意外的囉唆,在無法理解的時候就自己寫一張表,像下面的程式這樣插入printf()來追蹤值。
while(cnt < 10){ //> answer = answer + (cnt + 1);
printf("answer = %d + (%d + 1)\n", answer, cnt);
cnt = cnt + 1;
printf("cnt = %d + 1\n", cnt);
}
恐怖實驗: 不會結束的無限loop(又稱無窮迴圈)
雖然寫程式一定會遇到重複處理,但是走錯一步的話可能就會發生重大的bug,最常見的就是被稱為無窮迴圈的現象: 不管經過多久都滿足條件,永遠都會執行while的block。
比如說,把這次的while.c中的while(cnt < 10)換成while(cnt < cnt + 10)的話,不管何時都會滿足條件,所以程式永遠不會結束。常見電腦程式的「freeze」「固定住了」等等大半都是因為無窮迴圈這個bug。複雜的程式在通常範圍的利用下是可以好好的返回值然後正常結束的,但是在特定的條件下可能就會變成無窮迴圈。
不使用重複的話就無法寫出像樣的程式,請小心不注意的話就會發生bug。機會難得,可以試試看把while內的條件換一下實驗看看無窮迴圈。想結束的話,只要同時按下Ctrl跟C就可以了,請放心。
這次的範例程式改成while(cnt != 10)也是沒有錯的,但是如果我們把程式改成cnt每一次是+3而不是+1的話,就會產生無窮迴圈。但是寫while(cnt < 10)至少不會出現無窮迴圈。用「重複」的時候盡量用來寫條件式。
停止重複 break
重複處理在不滿足條件的話就會自動結束。但是,我們有時會希望在不滿足條件之前,遇到特定的狀況時也可以結束重複。這種時候,要用到的就是break。
在「重複」中遇到break的話,那個時間點就會無條件結束重複。
while(條件){
//處理 break;
}
因為這個特性,break通常與if一起使用,範例如下:
while(條件){
//處理 if(條件){
break; //在遇到這個條件時中止重複 }
}
機會難得,讓我們介紹求質數的範例程式吧。
source code
prime.c
#include
int main(){
int i,j,end;
int flag_not_prime; /*flag_not_prime = 0 is prime, 1 is not prime*/
end = 100; //find prime until 100
i = 1;
while(i <= end){ //>find prime from 1~end flag_not_prime = 0;
j = 2;
/*check whether i is prime, if i%j==0, i is not prime.
Otherwise, i is prime. j is 2~i-1*/
while(j < i){//> if(i % j == 0){
flag_not_prime = 1;
break;
}
j = j + 1;
}
if(flag_not_prime == 0){
printf("%d\n", i); // }
i = i + 1;
}
return 0;
}
執行結果會印出1還有其他2~100內的質數; 1嚴格來說不是質數,下一節講述另一個用在重複上的keyword: continue時,會說明如何把1去掉。
這個程式最初的重複條件,是為了調查1~100內有沒有質數。
while(i <= end){ //>find prime from 1~end i = i + 1;
}
這個while的block內的另一個while,則是從2開始當除數進行mod看能不能整除。要說為何是從二開始的話,一方面是因為1一定會整除,一方面質數的定義是「除了1跟自身以外都不能整除的數」。
j = 2;
/*check whether i is prime, if i%j==0, i is not prime.
Otherwise, i is prime. j is 2~i-1*/
while(j < i){//> if(i % j == 0){
flag_not_prime = 1;
break;
}
j = j + 1;
}
假設現在變數i是4的話,這個while內的block會進行4%2。由於4%2會是0代表整除,所以if這個block裡面的變數flag_not_prime會被代入1。再來,因為被整除已經可以確定它不是質數,即使繼續4%3也沒意義,所以直接用break從這個除數字的重複中跳出。跟break沒什麼直接關係,flag_not_prime這種只會有0或1兩種值的變數,我們稱之為「flag」。
雖然出現兩個while進行重複很複雜,不過可以瞬間算出人來計算需要花很多時間的東西,果然是很像電腦的範例程式。如果覺得困難的話,可以在途中插入printf(),用眼追蹤執行。假設我們把變數end的值換成5,把重複的部份寫成下面這樣:
while(i <= end){ //>find prime from 1~end printf("\nTest for %d\n", i);
flag_not_prime = 0;
j = 2;
/*check whether i is prime, if i%j==0, i is not prime.
Otherwise, i is prime. j is 2~i-1*/
while(j < i){//> printf(" Can %d divide by %d?\n", i, j);
if(i % j == 0){
printf(" Yes, not prime\n");
flag_not_prime = 1;
break;
}
printf(" No, continuing\n");
j = j + 1;
}
if(flag_not_prime == 0){
printf(" %d is prime\n", i);
}
i = i + 1;
}
執行結果:
Test for 1
1 is prime
Test for 2
2 is prime
Test for 3
Can 3 divide by 2?
No, continuing
3 is prime
Test for 4
Can 4 divide by 2?
Yes, not prime
Test for 5
Can 5 divide by 2?
No, continuing
Can 5 divide by 3?
No, continuing
Can 5 divide by 4?
No, continuing
5 is prime
繼續重複(continue)
除了break,還有另一個在重複中使用的continue。break是在用的當下停止重複,continue是在用的當下回到重複的開頭重來一次,如下圖所示:
while (條件){//1 //處理 continue;//執行到這一句時回到1}
也跟break一樣,通常搭配if來使用。
while (條件){//1 //處理 if(條件){
continue;//執行到這一句時回到1 }
}
雖然prime.c這個範例程式是拿來求質數的,但是一開始會印出1。嚴格來說,質數為了質因數分解的唯一性而不含1。在這裡,我們用continue來讓程式不顯示出1。
source code
prime_2.c
#include
int main(){
int i,j,end;
int flag_not_prime; /*flag_not_prime = 0 is prime, 1 is not prime*/
end = 100; //find prime until 100
i = 1;
while(i <= end){ //>find prime from 1~end flag_not_prime = 0;
j = 2;
/*check whether i is prime, if i%j==0, i is not prime.
Otherwise, i is prime. j is 2~i-1*/
while(j < i){//> if(i % j == 0){
flag_not_prime = 1;
break;
}
j = j + 1;
}
if(i == 1){
i = i + 1;
continue;
}
if(flag_not_prime == 0){
printf("%d\n", i); // }
i = i + 1;
}
return 0;
}
這樣顯示出的結果就是列出了從2開始到100的質數。本來應該把i = 1;改成i = 2;就好了,這裡為了舉例才用了continue。在continue之前如果不執行i = i + 1;的話變數i的值將永遠不會變,就會成為無窮迴圈。break還有continue都是把制御流程給強制改變,注意不要發生這種事。
求圓周率
結束了while的說明,來介紹一下求圓周率的範例程式吧; 雖然是要用到一些之後的章節才會講到的內容。要用到的之後才會講的東西,就只有「cast(強制轉型)」跟「亂數」而已,對數學有興趣的應該很開心吧。
source code
pi.c
#include
#include
#include
int main(){
double x, y, insect_cnt;
int i;
insect_cnt = 0.0;
i = 0;
srand(time(NULL));
while(i < 100000000){//> x = (double) rand() / RAND_MAX; //x get a random number of 0~1 y = (double) rand() / RAND_MAX;
if(x * x + y * y < 1.0){ //> // insect_cnt = insect_cnt + 1.0;
}
i = i + 1;
}
// insect_cnt / i = probability of x^2 + y^2 = 1.0 //*4 = 1 cycle printf("%f\n", insect_cnt / i * 4.0);
return 0;
}
執行結果:
3.141654
這個程式是利用到了「Monte Carlo method」來求出圓周率的程式。所謂Monte Carlo method,就是把跟機率沒什麼關係的問題用機率解決。
x = (double) rand() / RAND_MAX; //x get a random number of 0~1 y = (double) rand() / RAND_MAX;
if(x * x + y * y < 1.0){ //> }
求圓周率的情況下,先隨便用亂數決定x座標y座標,再代入圓方程式。因為圓方程式是x^2+y^2 = r^2(圓心在座標(0, 0)),所以x * x + y * y < 1.0代表亂數的x, y落在圓心(0, 0)半徑為1的圓。
這個處理在經過相當次數(越多次越準)的重複之後,數有幾次會收斂在這個圓裡。這個程式,以(double) rand() / RAND_MAX來取得介於0~1之間的亂數。由於這樣只會取到正數,所以嚴格來說我們算的是收斂在1/4圓(就是第一象限那1/4)裡的次數。
重複結束後,我們計算落在1/4圓的機率(insect_cnt / i),把它再*4,就是亂數落在整個圓的機率。如果改變重複的次數,精度也會跟著改變; 比如說重複次數1000,結果就會變成3.12這種不是很準的數字。來改變條件實驗看看吧。
順代一提我們是用執行的時刻來當「亂數種子(seed)」,所以不同時間冊同一程式結果會有些不同。學習到現在能做的事情也越來越廣泛,務必實驗一下除了範例以外的程式。
9.5 另外的重複 (for, do~while)
學習了if跟while後,應該就可以實做各種各樣的演算法了。作為例子,我們介紹了求質數跟求圓周率的範例程式。看了這些範例,應該可以知道while寫法就是下面的模式:
跟條件有關的變數的初始化
while(條件){
增加變數的值
}
上面這個部份以求質數來說寫成下面這樣:
i = 1;
while(i <= end){ //> 調查1~end之間的數字 i = i + 1;
}
雖然不是說必要,大部分的重複處理都是這種型態。而C語言有準備了可以清楚的表達這種記述方式的「for」。
for
for基本上跟while一樣,不過for可以把「重複前的處理(對應上面「跟條件有關的變數的初始化」)」「重複的條件」「在重複的過程中做的處理(對應上面「增加變數的值」)」,這三個東西寫成一行,範例如下:
for(重複前的處理; 重複的條件; 在重複的過程中做的處理){
}
以下面這例子來說:
i = 1;
while(i <= end){ //> 調查1~end之間的數字 i = i + 1;
}
可以寫成
for(i = 1; i <= end; i = i + 1)
//>
怎麼樣呢? 比起while來更加簡潔吧。for()中的i = i + 1,是在block裡面的事都處理完才執行的,之後才會判定中間的重複條件。
把prime_2.c用for重寫執行一遍,看看實際上該怎麼做吧。
source code
prime_3.c
#include
int main(){
int i,j,end;
int flag_not_prime; /*flag_not_prime = 0 is prime, 1 is not prime*/
end = 100; //find prime until 100
for(i = 1; i <= end; i = i + 1){ //>find prime from 1~end flag_not_prime = 0;
/*check whether i is prime, if i%j==0, i is not prime.
Otherwise, i is prime. j is 2~i-1*/
for(j = 2;j < i; j = j + 1){//> if(i % j == 0){
flag_not_prime = 1;
break;
}
}
if(i == 1){
//i = i + 1; continue;
}
if(flag_not_prime == 0){
printf("%d\n", i); // }
}
return 0;
}
執行結果跟prime_2.c是一樣的,不過程式變得簡潔許多了。尤其是當時的while版本在continue那裡還一定要寫i = i + 1,for在重複時會自動執行i = i + 1。
counter (loop counter)
使用for的話,雖然可以不用在block中寫i = i + 1,使得程式清爽許多,但是利用第八章介紹過的increament運算元(++)可以更簡潔。因為i = i + 1是把i加1的處理,所以可以用i++代替,如下例
for(i = 1; i <= end; i = i + 1){ //>
變成
for(i = 1; i <= end; i++){ //>
實際的程式,幾乎沒有寫i = i + 1的,都是寫i++,原因是重複處理的情況都是按照順序一個一個處理。像這個數重複回數的變數總稱為counter或loop counter。因為是C語言常用的文法,請記起來。也可以把while.c的範例程式用for來改寫看看。
至少會重複一次的do~while
while在開始重複之前,如果沒有符合條件的話就不會開始重複。比如說下面的程式碼:
while(cnt < 10){//>}
如果在執行到while之前,cnt就已經大於等於10的話,那就根本不會執行{}裡的東西。但是在某些場合還是需要執行的,這就是do while的用處。do while就是一定會執行一次{}內的處理,第一次處理完後才會檢查while內的條件,寫法如下:
do{
//處理}while(條件);
雖然同樣是重複,跟while還有for沒什麼區別,但是不管怎樣的條件都一定會執行一次block內的東西,在寫對話方面的處理是很方便的。來實驗看看單純的對話處理的範例程式吧,內容是擲骰子程式。
source code
dice.c
#include
#include
#include
int main(){
int c;
srand(time(NULL));
do{
printf("%d\n", rand() % 6 + 1);
//get a random number between 1 to 6 c = getchar();
//get a character from keyboard }while(c != 'q');//if input is q, quit
return 0;
}
執行結果:
6
2
1
4
1
6
2
q
執行這個程式的話,一執行就會先出現一個介於1~6之間的數字,之後如果輸入q以外的鍵(比如Enter)就會繼續出現下一個數字,直到按下q再按Enter程式才結束。雖然這個程式不需要用do while也可以寫,但是用while來寫的話,getchar()就需要寫兩次,source code不好看,改法如下:
do{
printf("%d\n", rand() % 6 + 1);
c = getchar();
}while(c != 'q');
變成
c = getchar();
while(c != 'q'){
printf("%d\n", rand() % 6 + 1);
c = getchar();
}
雖然do while的使用頻率比while還有for低很多,但是為了在適當時候能夠做出最佳選擇,還是把do while記起來。
9.6 分開處理複數條件
雖然不像if用的這麼多,但還有另一個處理分歧很便利的switch。
switch用法
if的話遇到複數的情況時,就像下面這樣使用:
if(條件1){
//處理} else if (條件2){
//處理} else if (條件3){
//處理} else{
//處理}
如果每個條件所用到的變數都不同的話,那就沒辦法了,只能用上面這種寫法; 不過如果是因為同一個變數的不同值要做不同處理的話,有更簡潔的寫法,就是利用switch。寫法如下:
switch (處理對象的變數) {
//這裡的條件寫法不是if那種像句子的,而是寫「常數式」 case 條件1:
//處理 case 條件2:
//處理 case 條件3:
//處理 default:
//處理}
舉個例子,if_else_2.c中的分歧,是像下面這麼寫的:
if(a == 1)
printf("a is 1\n");
else if(a == 2)
printf("a is 2\n");
else
printf("a is neither 1 nor 2\n");
這些分歧,都是依照變數a的值去分的。用switch的話,可以寫成像下面這樣:
switch (a){
case 1:
printf("a is 1\n");
break;
case 2:
printf("a is 2\n");
break;
default:
printf("a is neither 1 nor 2\n");
break;
}
不符合任何情況的時候,就是執行default。這裡不寫一個範例程式介紹switch,請自己把if_else_2.c改造做實驗。
switch()的陷阱
雖然很輕鬆的就介紹掉了,不過switch意外的是個難的文法,原書作者在初學時也常搞錯。其實,switch有許多陷阱,比起用恐怖實驗來說明,還是用基本事項來說明這些陷阱吧。
首先,switch的()裡面就只能寫「常數式」。if的話,可以依據條件結果是0是1的「比較」(< > <= >=)還有「等價」(== !=)的式子,也就是「條件式」。那麼,什麼是「常數式」呢? 如名所示,是一個只有常數的式子,以if_else_2.c來說就是case後面的1還有2這種的。然後,就算說是常數,也不能是一個代入的式子。而這個常數式的部份,被稱為label。
如果是if的話,就算是代入式,那個式子也會持有值,所以依然可以做分歧的動作。比如說,第八章的時候的form_5.c
if (a = 1 - 1){
printf("True");
}
也是可以正常動作的。但是switch就無法像if一樣用這些條件式。接下來的寫法也會造成compile error。
switch (a){
case b = 1 - 1;
break;
}
switch (a){
case b > 1 - 1;
break;
}
這樣寫會出現error: case label does not reduce to an integer constant。
再來,if可以選擇要不要用block,雖然基本上為了減少錯誤而應該用block,但是也可以不用,比如說:
if (a == 1)
printf("a is 1\n");
還有
if (a == 1){
printf("a is 1\n");
}
都是有效的。switch雖然也可以不用block,但是不用的話就只能寫一行命令而已。這樣的話,像上例就只能寫成
switch (a)
case 1:
printf("a is 1\n");
但是把同樣的條件再寫一次以補足處理,這樣的switch就沒有意義了。所以switch一定要用block。
為了不忘記break,推薦使用//FALLTHRU
最後關於switch的break有需要注意的點。switch從上面開始找到符合條件的常數式並做完那個block的處理,如果沒有break的話就依然會繼續往下做。
switch (處理對象的變數) {
//這裡的條件寫法不是if那種像句子的,而是寫「常數式」 case 條件1:
//處理 case 條件2:
//變數滿足條件2的情況下就做處理 //處理 case 條件3:
//在條件2的處理結束後依然繼續條件3的處理 //處理 default:
//處理}
舉個例子,下面的程式變數a的值如果是2,那麼除了printf("a is 2\n");會執行以外,printf("a is neither 1 nor 2\n");也會被執行。
switch (a){
case 1:
printf("a is 1\n");
case 2:
printf("a is 2\n");
default:
printf("a is neither 1 nor 2\n");
}
為了防止這點,一定是需要break的。如下所示:
switch (a){
case 1:
printf("a is 1\n");
break;
case 2:
printf("a is 2\n");
break;
default:
printf("a is neither 1 nor 2\n");
break;
}
也有故意在switch中不用break的例子。比如說下面一個if例子:
if (a == 1 || a == 2){
printf("a is 1 or 2\n");
} else{
printf("other\n");
}
可以用switch改成
switch (a){
case 1:
//FALLTHRU case 2:
printf("a is 1 or 2\n");
break;
default:
printf("other\n");
break;
}
這樣的一個故意不用break的switch寫法。只是,一般程式設計師看到的話,可能會有「switch不用break不會有潛在bug嗎?」的疑惑,這時的註釋就寫FALLTHRU。這樣以後看到的人就會知道這是故意不寫break的。
不會執行到break的時候,推薦用//NOT REACHED
還有一個同樣是註釋的NOT REACHED。因為switch幾乎都會用break,但寫了break卻執行不到break時,就會寫這個註釋。比如說,在switch的block裡,break之前就遇到了return,或是遇到了goto(之後會解釋)跳到了別的地方去。
在這種時候,為了表明「雖然因為是switch所以用了break,但實際上不會執行喔」的意思,我們會用註解寫上NOT REACHED。下面的程式就是遇到return後函數的執行就中止了,並沒有執行到break,所以就寫個NOT REACHED註釋。
switch (a){
case 1:
printf("a is 1\n");
return (1);
//NOT REACHED break;
case 2:
printf("a is 2\n");
return (2);
//NOT REACHED break;
default:
printf("a is neither 1 nor 2\n");
return (0);
//NOT REACHED break;
}
因為是註釋,所以加上FALLTHRU跟NOT REACHED是沒影響的。因為switch很難,為了防止錯誤還是寫一寫吧。
做出計算機
為了習慣之前所講的東西,這裡就來實行一個範例吧。
source code
switch.c
#include
int main(){
int a,b;
int op;
int answer;
a = 1;
b = 2;
op = '+';
switch(op){
case '+':
answer = a + b;
break;
case '-':
answer = a - b;
break;
case '*':
answer = a * b;
break;
case '/':
if(b == 0){
printf("divide by zero\n");
}
else{
answer = a / b;
}
break;
default:
printf("operator unknown\n");
break;
}
printf("%d %c %d = %d\n", a, op, b, answer);
return 0;
}
執行結果:
1 + 2 = 3
把想計算的值代入變數a及b,把運算元的文字代入變數op進行運算。如果b = 0, op = '/'的話,會產生除以0的error。說到case '+'為何是常數式,那是因為+這個文字本身就被當作數字處理,+是10進位的43,16進位的2B。C語言用''括起來的東西都會被當作ascii code的數值處理,當然,直接把'+'寫成43也是可以的,但要人家看得懂還是用+比較好吧。詳細的可以參照第七章的「不存在字串」的表。
像這程式有這麼多分歧的話switch是很有用的。雖然很複雜容易出錯,但請像if一樣好好記起來。
9.7 可以跳躍到任何地方的goto
最後的制御構造是goto。goto跟其他的制御文不同,是可以任意的跳到任何地方的。像BASIC這樣的語言用這個是很普通的,但是像C語言這樣結構化的語言很少用,這是因為跟其他的制御文不同,太自由了容易產生bug。那麼,來看看該怎麼寫吧。
goto label名稱;
label名稱:
把label名稱寫在程式的任何地方,goto label名稱後,就會立刻跳到label名稱執行指令。來看看範例程式吧。
source code
goto.c
#include
int main(){
goto label_1;
printf("This statement doesn't execute.\n");
label_1:
return 0;
}
執行後可以看到沒有東西印出來,這是因為goto直接跳到label_1的return 0;,所以沒執行到printf()就結束了。可以從重複中脫出的break,只能跳出現在執行中的while跟for而已,想要一口氣從多重的迴圈中跳出的話,用goto比較方便。比如下例:
while(條件){ // while 1 //處理 while(條件){ // while 2 //處理 while(條件){ // while 3 //這裡的break只能跳出while 3,想從這裡跳出全部的while //就使用goto,如下面的程式碼 break;
}
}
}
while(條件){ // while 1 //處理 while(條件){ // while 2 //處理 while(條件){ // while 3 goto label;;
}
}
}
label:
但是,濫用的話會很難搞清楚程式的流程,也很容易造成bug,所以要注意。
恐怖實驗: 如果用goto寫重複
如果對寫程式不熟悉的話,可能無法好好利用while, for,造成濫用goto。那麼,為何會使用goto呢? 首先,是用goto來寫重複吧。把第七章的ascii.c拿來用goto改造一下:
source code
ascii_2.c
#include
int main(){
int character = 0;
loop:
printf("%c ", character);
character = character + 1;
if((character % 16) == 0){
printf("\n");
}
if(character < 256){ //> goto loop;
}
return 0;
}
執行之後可以發現結果跟ascii.c是一樣的,但是程式碼跟while的版本比起來比較不好讀了; 不過還是讀的下去吧。那麼,來把這一章求質數的程式用goto來寫吧。
source code
prime_4.c
#include
int main(){
int i,j,end;
int flag_not_prime; /*flag_not_prime = 0 is prime, 1 is not prime*/
end = 100; //find prime until 100 i = 1;
label_1: //>find prime from 1~end if(i <= end){//> flag_not_prime = 0;
j = 2;
/*check whether i is prime, if i%j==0, i is not prime.
Otherwise, i is prime. j is 2~i-1*/
label_2:
if(j < i){//> if(i % j == 0){
flag_not_prime = 1;
goto label_3;
}
j = j + 1;
goto label_2;
}
label_3:
if(i == 1){
i = i + 1;
goto label_1;
}
if(flag_not_prime == 0){
printf("%d\n", i); // }
i = i + 1;
goto label_1;
}
return 0;
}
執行結果跟prime_3.c是相同的,但是source code變得很囉唆吧。原書作者為了寫這個也花了比其他source code更多的時間。BASIC還有assembly language是只有goto可以用,所以寫起程式來就是prime_4.c這種感覺。但是像C語言這樣有治癒構造可以使用的話,應該要極力避免使用goto。也因為這種理由,比C新的語言Java已經把goto刪除了。
9.8 結語
已經說明了data type與變數、式子與運算元、制御構造。學到這裡的話已經可以寫出像是pi.c這個積算圓周率的程式一樣複雜的程式。因為還沒說明函數,所以不能使用有關輸出入的機能,這樣很難寫對話性的程式。
但是到這個階段已經可以作到很多事了,可以試試寫很多程式。如果搞不清楚的話,就回到前一章吧。
還有,本章介紹的範例程式有些複雜,如果沒法理解內容的話,就用質數範例程式介紹的printf()加到程式裡,來追蹤程式的動作。