本章介紹以下內容:
關鍵字:if、else、switch、continue、break、case、default、goto
運算符:&&、||、?:
函數:getchar、putchar、ctype.h系列
如何使用if和if else語句,如何嵌套它們
在更複雜的測試表達式中用邏輯運算符組合關係表達式
C的條件運算符
switch語句
break、continue和goto語句
使用C的字符I/O函數:getchar和putchar
ctype.h頭文件提供的字符分析函數系列
隨著越來越熟悉C,可以嘗試用C程序解決一些更複雜的問題。這時候,需要一些方法來控制和組織程序,為此C提供了一些工具。前面已經學過如何在程序中用循環重複執行任務。本章將介紹分支結構(如, if和switch),讓程序根據測試條件執行相應的行為。另外,還將介紹C語言的邏輯運算符,使用邏輯運算符能在 while 或 if 的條件中測試更多關係。此外,本章還將介紹跳轉語句,它將程序流轉換到程序的其他部分。學完本章後,讀者就可以設計按自己期望方式運行的程序。
7.1 if語句
我們從一個有if語句的簡單示例開始學習,請看程序清單7.1。該程序讀取一列數據,每個數據都表示每日的最低溫度(℃),然後打印統計的總天數和最低溫度在0℃以下的天數占總天數的百分比。程序中的循環通過scanf讀入溫度值。while循環每迭代一次,就遞增計數器增加天數,其中的if語句負責判斷0℃以下的溫度並單獨統計相應的天數。
程序清單7.1 colddays.c程序
// colddays.c -- 找出0℃以下的天數占總天數的百分比
#include <stdio.h>
int main(void)
{
const int FREEZING = 0;
float temperature;
int cold_days = 0;
int all_days = 0;
printf("Enter the list of daily low temperatures.\n");
printf("Use Celsius, and enter q to quit.\n");
while (scanf("%f", &temperature) == 1)
{
all_days++;
if (temperature < FREEZING)
cold_days++;
}
if (all_days != 0)
printf("%d days total: %.1f%% were below freezing.\n",
all_days, 100.0 * (float) cold_days / all_days);
if (all_days == 0)
printf("No data entered!\n");
return 0;
}
下面是該程序的輸出示例:
Enter the list of daily low temperatures.
Use Celsius, and enter q to quit.
12 5 -2.5 0 6 8 -3 -10 5 10 q
10 days total: 30.0% were below freezing.
while循環的測試條件利用scanf的返回值來結束循環,因為scanf在讀到非數字字符時會返回0。temperature的類型是float而不是int,這樣程序既可以接受-2.5這樣的值,也可以接受8這樣的值。
while循環中的新語句如下:
if (temperature < FREEZING)
cold_days++;
if 語句指示計算機,如果剛讀取的值(remperature)小於 0,就把 cold_days 遞增 1;如果temperature不小於0,就跳過cold_days++;語句,while循環繼續讀取下一個溫度值。
接著,該程序又使用了兩次if語句控制程序的輸出。如果有數據,就打印結果;如果沒有數據,就打印一條消息(稍後將介紹一種更好的方法來處理這種情況)。
為避免整數除法,該程序示例把計算後的百分比強制轉換為 float類型。其實,也不必使用強制類型轉換,因為在表達式100.0 * cold_days / all_days中,將首先對表達式100.0 * cold_days求值,由於C的自動轉換類型規則,乘積會被強制轉換成浮點數。但是,使用強制類型轉換可以明確表達轉換類型的意圖,保護程序免受不同版本編譯器的影響。if語句被稱為分支語句(branching statement)或選擇語句(selection statement),因為它相當於一個交叉點,程序要在兩條分支中選擇一條執行。if語句的通用形式如下:
if ( expression )
statement
如果對expression求值為真(非0),則執行statement;否則,跳過statement。與while循環一樣,statement可以是一條簡單語句或復合語句。if語句的結構和while語句很相似,它們的主要區別是:如果滿足條件可執行的話,if語句只能測試和執行一次,而while語句可以測試和執行多次。
通常,expression 是關係表達式,即比較兩個量的大小(如,表達式 x > y 或 c == 6)。如果expression為真(即x大於y,或c == 6),則執行statement。否則,忽略statement。概括地說,可以使用任意表達式,表達式的值為0則為假。
statement部分可以是一條簡單語句,如本例所示,或者是一條用花括號括起來的復合語句(或塊):
if (score > big)
printf("Jackpot!\n"); // 簡單語句
if (joe > ron)
{ // 復合語句
joecash++;
printf("You lose, Ron.\n");
}
注意,即使if語句由復合語句構成,整個if語句仍被視為一條語句。
7.2 if else語句
簡單形式的if語句可以讓程序選擇執行一條語句,或者跳過這條語句。C還提供了if else形式,可以在兩條語句之間作選擇。我們用if else形式修正程序清單7.1中的程序段。
if (all_days != 0)
printf("%d days total: %.1f%% were below freezing.\n",
all_days, 100.0 * (float) cold_days / all_days);
if (all_days == 0)
printf("No data entered!\n");
如果程序發現all_days不等於0,那麼它應該知道另一種情況一定是all_days等於0。用if else形式只需測試一次。重寫上面的程序段如下:
if (all_days!= 0)
printf("%d days total: %.1f%% were below freezing.\n",
all_days, 100.0 * (float) cold_days / all_days);
else
printf("No data entered!\n");
如果if語句的測試表達式為真,就打印溫度數據;如果為假,就打印警告消息。
注意,if else語句的通用形式是:
if ( expression )
statement1
else
statement2
如果expression為真(非0),則執行statement1;如果expression為假或0,則執行else後面的statement2。statement1和statement2可以是一條簡單語句或復合語句。C並不要求一定要縮進,但這是標準風格。縮進讓根據測試條件的求值結果來判斷執行哪部分語句一目瞭然。
如果要在if和else之間執行多條語句,必須用花括號把這些語句括起來成為一個塊。下面的代碼結構違反了C語法,因為在if和else之間只允許有一條語句(簡單語句或復合語句):
if (x > 0)
printf("Incrementing x:\n");
x++;
else // 將產生一個錯誤
printf("x <= 0 \n");
編譯器把printf語句視為if語句的一部分,而把x++;看作一條單獨的語句,它不是if語句的一部分。然後,編譯器發現else並沒有所屬的if,這是錯誤的。上面的代碼應該這樣寫:
if (x > 0)
{
printf("Incrementing x:\n");
x++;
}
else
printf("x <= 0 \n");
if語句用於選擇是否執行一個行為,而else if語句用於在兩個行為之間選擇。圖7.1比較了這兩種語句。
圖7.1 if語句和if else語句
7.2.1 另一個示例:介紹getchar和putchar
到目前為止,學過的大多數程序示例都要求輸入數值。接下來,我們看看輸入字符的示例。相信讀者已經熟悉了如何用 scanf和 printf根據%c 轉換說明讀寫字符,我們馬上要講解的示例中要用到一對字符輸入/輸出函數:getchar和putchar。
getchar函數不帶任何參數,它從輸入隊列中返回下一個字符。例如,下面的語句讀取下一個字符輸入,並把該字符的值賦給變量ch:
ch = getchar;
該語句與下面的語句效果相同:
scanf("%c", &ch);
putchar函數打印它的參數。例如,下面的語句把之前賦給ch的值作為字符打印出來:
putchar(ch);
該語句與下面的語句效果相同:
printf("%c", ch);
由於這些函數只處理字符,所以它們比更通用的scanf和printf函數更快、更簡潔。而且,注意 getchar和 putchar不需要轉換說明,因為它們只處理字符。這兩個函數通常定義在 stdio.h頭文件中(而且,它們通常是預處理宏,而不是真正的函數,第16章會討論類似函數的宏)。
接下來,我們編寫一個程序來說明這兩個函數是如何工作的。該程序把一行輸入重新打印出來,但是每個非空格都被替換成原字符在ASCII序列中的下一個字符,空格不變。這一過程可描述為「如果字符是空白,原樣打印;否則,打印原字符在ASCII序列中的下一個字符」。
C代碼看上去和上面的描述很相似,請看程序清單7.2。
程序清單7.2 cypher1.c程序
// cypher1.c -- 更改輸入,空格不變
#include <stdio.h>
#define SPACE ' ' // SPACE表示單引號-空格-單引號
int main(void)
{
char ch;
ch = getchar; // 讀取一個字符
while (ch != '\n') // 當一行未結束時
{
if (ch == SPACE) // 留下空格
putchar(ch); // 該字符不變
else
putchar(ch + 1); // 改變其他字符
ch = getchar;// 獲取下一個字符
}
putchar(ch);// 打印換行符
return 0;
}
(如果編譯器警告因轉換可能導致數據丟失,不用擔心。第8章在講到EOF時再解釋。)
下面是該程序的輸入示例:
CALL ME HAL.
DBMM NF IBM/
把程序清單7.1中的循環和該例中的循環作比較。前者使用scanf返回的狀態值判斷是否結束循環,而後者使用輸入項的值來判斷是否結束循環。這使得兩程序所用的循環結構略有不同:程序清單7.1中在循環前面有一條「讀取語句」,程序清單7.2中在每次迭代的末尾有一條「讀取語句」。不過,C的語法比較靈活,讀者也可以模仿程序清單7.1,把讀取和測試合併成一個表達式。也就是說,可以把這種形式的循環:
ch = getchar; /* 讀取一個字符 */
while (ch != '\n')/* 當一行未結束時 */
{
... /* 處理字符 */
ch = getchar; /* 獲取下一個字符 */
}
替換成下面形式的循環:
while ((ch = getchar) != '\n')
{
... /* 處理字符 */
}
關鍵的一行代碼是:
while ((ch = getchar) != '\n')
這體現了C特有的編程風格——把兩個行為合併成一個表達式。C對代碼的格式要求寬鬆,這樣寫讓其中的每個行為更加清晰:
while (
(ch = getchar) // 給ch賦一個值
!= '\n') // 把ch和\n作比較
以上執行的行為是賦值給ch和把ch的值與換行符作比較。表達式ch = getchar兩側的圓括號使之成為!=運算符的左側運算對象。要對該表達式求值,必須先調用getchar函數,然後把該函數的返回值賦給 ch。因為賦值表達式的值是賦值運算符左側運算對象的值,所以 ch = getchar的值就是 ch 的新值,因此,讀取ch的值後,測試條件相當於是ch != '\n'(即,ch不是換行符)。
這種獨特的寫法在C編程中很常見,應該多熟悉它。還要記住合理使用圓括號組合子表達式。上面例子中的圓括號都必不可少。假設省略ch = getchar兩側的圓括號:
while (ch = getchar != '\n')
!=運算符的優先級比=高,所以先對表達式getchar != '\n'求值。由於這是關係表達式,所以其值不是1就是0(真或假)。然後,把該值賦給ch。省略圓括號意味著賦給ch的值是0或1,而不是 getchar的返回值。這不是我們的初衷。
下面的語句:
putchar(ch + 1); /* 改變其他字符 */
再次演示了字符實際上是作為整數儲存的。為方便計算,表達式ch + 1中的ch被轉換成int類型,然後int類型的計算結果被傳遞給接受一個int類型參數的putchar,該函數只根據最後一個字節確定顯示哪個字符。
7.2.2 ctype.h系列的字符函數
注意到程序清單7.2的輸出中,最後輸入的點號(.)被轉換成斜槓(/),這是因為斜槓字符對應的ASCII碼比點號的 ASCII 碼多 1。如果程序只轉換字母,保留所有的非字母字符(不只是空格)會更好。本章稍後討論的邏輯運算符可用來測試字符是否不是空格、不是逗號等,但是列出所有的可能性太繁瑣。C 有一系列專門處理字符的函數,ctype.h頭文件包含了這些函數的原型。這些函數接受一個字符作為參數,如果該字符屬於某特殊的類別,就返回一個非零值(真);否則,返回0(假)。例如,如果isalpha函數的參數是一個字母,則返回一個非零值。程序清單7.3在程序清單7.2的基礎上使用了這個函數,還使用了剛才精簡後的循環。
程序清單7.3 cypher2.c程序
// cypher2.c -- 替換輸入的字母,非字母字符保持不變
#include <stdio.h>
#include <ctype.h> // 包含isalpha的函數原型
int main(void)
{
char ch;
while ((ch = getchar) != '\n')
{
if (isalpha(ch)) // 如果是一個字符,
putchar(ch + 1); // 顯示該字符的下一個字符
else// 否則,
putchar(ch); // 原樣顯示
}
putchar(ch); // 顯示換行符
return 0;
}
下面是該程序的一個輸出示例,注意大小寫字母都被替換了,除了空格和標點符號:
Look! It's a programmer!
Mppl! Ju't b qsphsbnnfs!
表7.1和表7.2列出了ctype.h頭文件中的一些函數。有些函數涉及本地化,指的是為適應特定區域的使用習慣修改或擴展 C 基本用法的工具(例如,許多國家在書寫小數點時,用逗號代替點號,於是特殊的本地化可以指定C編譯器使用逗號以相同的方式輸出浮點數,這樣123.45可以顯示為123,45)。注意,字符映射函數不會修改原始的參數,這些函數只會返回已修改的值。也就是說,下面的語句不改變ch的值:
tolower(ch); // 不影響ch的值
這樣做才會改變ch的值:
ch = tolower(ch); // 把ch轉換成小寫字母
表7.1 ctype.h頭文件中的字符測試函數
表7.2 ctype.h頭文件中的字符映射函數
7.2.3 多重選擇else if
現實生活中我們經常有多種選擇。在程序中也可以用else if擴展if else結構模擬這種情況。來看一個特殊的例子。電力公司通常根據客戶的總用電量來決定電費。下面是某電力公司的電費清單,單位是千瓦時(kWh):
首 360kWh: $0.13230/kWh
續 108kWh: $0.15040/kWh
續 252kWh: $0.30025/kWh
超過 720kWh: $0.34025/kWh
如果對用電管理感興趣,可以編寫一個計算電費的程序。程序清單7.4是完成這一任務的第1步。
程序清單7.4 electric.c程序
// electric.c -- 計算電費
#include <stdio.h>
#define RATE1 0.13230 // 首次使用 360 kwh 的費率
#define RATE2 0.15040 // 接著再使用 108 kwh 的費率
#define RATE3 0.30025 // 接著再使用 252 kwh 的費率
#define RATE4 0.34025 // 使用超過 720kwh 的費率
#define BREAK1 360.0// 費率的第1個分界點
#define BREAK2 468.0// 費率的第2個分界點
#define BREAK3 720.0// 費率的第3個分界點
#define BASE1 (RATE1 * BREAK1)
// 使用360kwh的費用
#define BASE2 (BASE1 + (RATE2 * (BREAK2 - BREAK1)))
// 使用468kwh的費用
#define BASE3 (BASE1 + BASE2 + (RATE3 *(BREAK3 - BREAK2)))
// 使用720kwh的費用
int main(void)
{
double kwh; // 使用的千瓦時
double bill;// 電費
printf("Please enter the kwh used.\n");
scanf("%lf", &kwh); // %lf對應double類型
if (kwh <= BREAK1)
bill = RATE1 * kwh;
else if (kwh <= BREAK2) // 360~468 kwh
bill = BASE1 + (RATE2 * (kwh - BREAK1));
else if (kwh <= BREAK3) // 468~720 kwh
bill = BASE2 + (RATE3 * (kwh - BREAK2));
else// 超過 720 kwh
bill = BASE3 + (RATE4 * (kwh - BREAK3));
printf("The charge for %.1f kwh is $%1.2f.\n", kwh, bill);
return 0;
}
該程序的輸出示例如下:
Please enter the kwh used.
580
The charge for 580.0 kwh is $97.50.
程序清單 7.4 用符號常量表示不同的費率和費率分界點,以便把常量統一放在一處。這樣,電力公司在更改費率以及費率分界點時,更新數據非常方便。BASE1和BASE2根據費率和費率分界點來表示。一旦費率或分界點發生了變化,它們也會自動更新。預處理器是不進行計算的。程序中出現BASE1的地方都會被替換成 0.13230*360.0。不用擔心,編譯器會對該表達式求值得到一個數值(47.628),以便最終的程序代碼使用的是47.628而不是一個計算式。
程序流簡單明瞭。該程序根據kwh的值在3個公式之間選擇一個。特別要注意的是,如果kwh大於或等於360,程序只會到達第1個else。因此,else if (kwh <= BREAK2)這行相當於要求kwh在360~482之間,如程序註釋所示。類似地,只有當kwh的值超過720時,才會執行最後的else。最後,注意BASE1、BASE2和BASE3分別代表360、468和720千瓦時的總費用。因此,當電量超過這些值時,只需要加上額外的費用即可。
實際上,else if 是已學過的 if else 語句的變式。例如,該程序的核心部分只不過是下面代碼的另一種寫法:
if (kwh <= BREAK1)
bill = RATE1 * kwh;
else
if (kwh <= BREAK2) // 360~468 kwh
bill = BASE1 + (RATE2 * (kwh - BREAK1));
else
if (kwh <= BREAK3) // 468~720 kwh
bill = BASE2 + (RATE3 * (kwh - BREAK2));
else// 超過720 kwh
bill = BASE3 + (RATE4 * (kwh - BREAK3));
也就是說,該程序由一個ifelse語句組成,else部分包含另一個if else語句,該if else語句的else部分又包含另一個if else語句。第2個if else語句嵌套在第 1個if else語句中,第3個if else語句嵌套在第2個if else語句中。回憶一下,整個if else語句被視為一條語句,因此不必把嵌套的if else語句用花括號括起來。當然,花括號可以更清楚地表明這種特殊格式的含義。
這兩種形式完全等價。唯一不同的是使用空格和換行的位置不同,不過編譯器會忽略這些。儘管如此,第1種形式還是好些,因為這種形式更清楚地顯示了有4種選擇。在瀏覽程序時,這種形式讓讀者更容易看清楚各項選擇。在需要時要縮進嵌套的部分,例如,必須測試兩個單獨的量時。本例中,僅在夏季對用電量超過720kWh的用戶加收10%的電費,就屬於這種情況。
可以把多個else if語句連成一串使用,如下所示(當然,要在編譯器的限制範圍內):
if (score < 1000)
bonus = 0;
else if (score < 1500)
bonus = 1;
else if (score < 2000)
bonus = 2;
else if (score < 2500)
bonus = 4;
else
bonus = 6;
(這可能是一個遊戲程序的一部分,bonus表示下一局遊戲獲得的光子炸彈或補給。)
對於編譯器的限制範圍,C99標準要求編譯器最少支持127層套嵌。
7.2.4 else與if配對
如果程序中有許多if和else,編譯器如何知道哪個if對應哪個else?例如,考慮下面的程序段:
if (number > 6)
if (number < 12)
printf("You're close!\n");
else
printf("Sorry, you lose a turn!\n");
何時打印Sorry, you lose a turn!?當number小於或等於6時,還是number大於12時?換言之,else與第1個if還是第2個if匹配?答案是,else與第2個if匹配。也就是說,輸入的數字和匹配的響應如下:
數字 響應
5 None
10You』re close!
15Sorry, you lose a turn!
規則是,如果沒有花括號,else與離它最近的if匹配,除非最近的if被花括號括起來(見圖7.2)。
圖7.2 if else匹配的規則
注意:要縮進「語句」,「語句」可以是一條簡單語句或復合語句。
第1個例子的縮進使得else看上去與第1個if相匹配,但是記住,編譯器是忽略縮進的。如果希望else與第1個if匹配,應該這樣寫:
if (number > 6)
{
if (number < 12)
printf("You're close!\n");
}
else
printf("Sorry, you lose a turn!\n");
這樣改動後,響應如下:
數字 響應
5 Sorry, you lose a turn!
10You』re close!
15None
7.2.5 多層嵌套的if語句
前面介紹的if...else if...else序列是嵌套if的一種形式,從一系列選項中選擇一個執行。有時,選擇一個特定選項後又引出其他選擇,這種情況可以使用另一種嵌套 if。例如,程序可以使用 if else選擇男女,if else的每個分支裡又包含另一個if else來區分不同收入的群體。
我們把這種形式的嵌套if應用在下面的程序中。給定一個整數,顯示所有能整除它的約數。如果沒有約數,則報告該數是一個素數。
在編寫程序的代碼之前要先規劃好。首先,要總體設計一下程序。為方便起見,程序應該使用一個循環讓用戶能連續輸入待測試的數。這樣,測試一個新的數字時不必每次都要重新運行程序。下面是我們為這種循環開發的一個模型(偽代碼):
提示用戶輸入數字
當scanf返回值為1
分析該數並報告結果
提示用戶繼續輸入
回憶一下在測試條件中使用scanf,把讀取數字和判斷測試條件確定是否結束循環合併在一起。
下一步,設計如何找出約數。也許最直接的方法是:
for (p = 2; p < num; p++)
if (num % p == 0)
printf("%d is pisible by %d\n", num, p);
該循環檢查2~num之間的所有數字,測試它們是否能被num整除。但是,這個方法有點浪費時間。我們可以改進一下。例如,考慮如果144%2得0,說明2是144的約數;如果144除以2得72,那麼72也是144的一個約數。所以,num % p測試成功可以獲得兩個約數。為了弄清其中的原理,我們分析一下循環中得到的成對約數:2和72、2和48、4和36、6和24、8和18、9和16、12和12、16和9、18和8,等等。在得到12和12這對約數後,又開始得到已找到的相同約數(次序相反)。因此,不用循環到143,在達到12以後就可以停止循環。這大大地節省了循環時間!
分析後發現,必須測試的數只要到num的平方根就可以了,不用到num。對於9這樣的數字,不會節約很多時間,但是對於10000這樣的數,使用哪一種方法求約數差別很大。不過,我們不用在程序中計算平方根,可以這樣編寫測試條件:
for (p = 2; (p * p) <= num; p++)
if (num % p == 0)
printf("%d is pisible by %d and %d.\n",num, p, num / p);
如果num是144,當p = 12時停止循環。如果num是145,當p = 13時停止循環。
不使用平方根而用這樣的測試條件,有兩個原因。其一,整數乘法比求平方根快。其二,我們還沒有正式介紹平方根函數。
還要解決兩個問題才能準備編程。第1個問題,如果待測試的數是一個完全平方數怎麼辦?報告144可以被12和12整除顯得有點傻。可以使用嵌套if語句測試p是否等於num /p。如果是,程序只打印一個約數:
for (p = 2; (p * p) <= num; p++)
{
if (num % p == 0)
{
if (p * p != num)
printf("%d is pisible by %d and %d.\n",num, p, num / p);
else
printf("%d is pisible by %d.\n", num, p);
}
}
注意
從技術角度看,if else語句作為一條單獨的語句,不必使用花括號。外層if也是一條單獨的語句,也不必使用花括號。但是,當語句太長時,使用花括號能提高代碼的可讀性,而且還可防止今後在if循環中添加其他語句時忘記加花括號。
第2個問題,如何知道一個數字是素數?如果num是素數,程序流不會進入if語句。要解決這個問題,可以在外層循環把一個變量設置為某個值(如,1),然後在if語句中把該變量重新設置為0。循環完成後,檢查該變量是否是1,如果是,說明沒有進入if語句,那麼該數就是素數。這樣的變量通常稱為標記(flag)。
一直以來,C都習慣用int作為標記的類型,其實新增的_Bool類型更合適。另外,如果在程序中包含了stdbool.h頭文件,便可用bool代替_Bool類型,用true和false分別代替1和0。
程序清單7.5體現了以上分析的思路。為擴大該程序的應用範圍,程序用long類型而不是int類型(如果系統不支持_Bool類型,可以把isPrime的類型改為int,並用1和0分別替換程序中的true和false)。
程序清單7.5 pisors.c程序
// pisors.c -- 使用嵌套if語句顯示一個數的約數
#include <stdio.h>
#include <stdbool.h>
int main(void)
{
unsigned long num; // 待測試的數
unsigned long p; // 可能的約數
bool isPrime; // 素數標記
printf("Please enter an integer for analysis; ");
printf("Enter q to quit.\n");
while (scanf("%lu", &num) == 1)
{
for (p = 2, isPrime = true; (p * p) <= num; p++)
{
if (num % p == 0)
{
if ((p * p) != num)
printf("%lu is pisible by %lu and %lu.\n",
num, p, num / p);
else
printf("%lu is pisible by %lu.\n",
num, p);
isPrime = false; // 該數不是素數
}
}
if (isPrime)
printf("%lu is prime.\n", num);
printf("Please enter another integer for analysis; ");
printf("Enter q to quit.\n");
}
printf("Bye.\n");
return 0;
}
注意,該程序在for循環的測試表達式中使用了逗號運算符,這樣每次輸入新值時都可以把isPrime設置為true。
下面是該程序的一個輸出示例:
Please enter an integer for analysis; Enter q to quit.
123456789
123456789 is pisible by 3 and 41152263.
123456789 is pisible by 9 and 13717421.
123456789 is pisible by 3607 and 34227.
123456789 is pisible by 3803 and 32463.
123456789 is pisible by 10821 and 11409.
Please enter another integer for analysis; Enter q to quit.
149
149 is prime.
Please enter another integer for analysis; Enter q to quit.
2013
2013 is pisible by 3 and 671.
2013 is pisible by 11 and 183.
2013 is pisible by 33 and 61.
Please enter another integer for analysis; Enter q to quit.
q
Bye.
該程序會把1認為是素數,其實它不是。下一節將要介紹的邏輯運算符可以排除這種特殊的情況。
小結:用if語句進行選擇
關鍵字:if、else
一般註解:
下面各形式中,statement可以是一條簡單語句或復合語句。表達式為真說明其值是非零值。
形式1:
if (expression)
statement
如果expression為真,則執行statement部分。
形式2:
if (expression)
statement1
else
statement2
如果expression為真,執行statement1部分;否則,執行statement2部分。
形式3:
if (expression1)
statement1
else if (expression2)
statement2
else
statement3
如果expression1為真,執行statement1部分;如果expression2為真,執行statement2部分;否則,執行statement3部分。
示例:
if (legs == 4)
printf("It might be a horse.\n");
else if (legs > 4)
printf("It is not a horse.\n");
else /* 如果legs < 4 */
{
legs++;
printf("Now it has one more leg.\n");
}
7.3 邏輯運算符
讀者已經很熟悉了,if 語句和 while 語句通常使用關係表達式作為測試條件。有時,把多個關係表達式組合起來會很有用。例如,要編寫一個程序,計算輸入的一行句子中除單引號和雙引號以外其他字符的數量。這種情況下可以使用邏輯運算符,並使用句點(.)標識句子的末尾。程序清單7.6用一個簡短的程序進行演示。
程序清單7.6 chcount.c程序
// chcount.c -- 使用邏輯與運算符
#include <stdio.h>
#define PERIOD '.'
int main(void)
{
char ch;
int charcount = 0;
while ((ch = getchar) != PERIOD)
{
if (ch != '"' && ch != '\'')
charcount++;
}
printf("There are %d non-quote characters.\n", charcount);
return 0;
}
下面是該程序的一個輸出示例:
I didn't read the "I'm a Programming Fool" best seller.
There are 50 non-quote characters.
程序首先讀入一個字符,並檢查它是否是一個句點,因為句點標誌一個句子的結束。接下來,if語句的測試條件中使用了邏輯與運算符&&。該 if 語句翻譯成文字是「如果待測試的字符不是雙引號,並且它也不是單引號,那麼charcount遞增1」。
邏輯運算符兩側的條件必須都為真,整個表達式才為真。邏輯運算符的優先級比關係運算符低,所以不必在子表達式兩側加圓括號。
C有3種邏輯運算符,見表7.3。
表7.3 種邏輯運算符
假設exp1和exp2是兩個簡單的關係表達式(如car > rat或debt == 1000),那麼:
當且僅當exp1和exp2都為真時,exp1 && exp2才為真;
如果exp1或exp2為真,則exp1 || exp2為真;
如果exp1為假,則!exp1為真;如果exp1為真,則!exp1為假。
下面是一些具體的例子:
5 > 2 && 4 > 7為假,因為只有一個子表達式為真;
5 > 2 || 4 > 7為真,因為有一個子表達式為真;
!(4 > 7)為真,因為4不大於7。
順帶一提,最後一個表達式與下面的表達式等價:
4 <= 7
如果不熟悉邏輯運算符或者覺得很彆扭,請記住:(練習&&時間)== 完美。
7.3.1 備選拼寫:iso646.h頭文件
C 是在美國用標準美式鍵盤開發的語言。但是在世界各地,並非所有的鍵盤都有和美式鍵盤一樣的符號。因此,C99標準新增了可代替邏輯運算符的拼寫,它們被定義在ios646.h頭文件中。如果在程序中包含該頭文件,便可用and代替&&、or代替||、not代替!。例如,可以把下面的代碼:
if (ch != '"' && ch != '\'')
charcount++;
改寫為:
if (ch != '"' and ch != '\'')
charcount++;
表7.4列出了邏輯運算符對應的拼寫,很容易記。讀者也許很好奇,為何C不直接使用and、or和not?因為C一直堅持盡量保持較少的關鍵字。參考資料V「新增C99和C11的標準ANSI C庫」列出了一些運算符的備選拼寫,有些我們還沒見過。
表7.4 邏輯運算符的備選拼寫
7.3.2 優先級
!運算符的優先級很高,比乘法運算符還高,與遞增運算符的優先級相同,只比圓括號的優先級低。&&運算符的優先級比||運算符高,但是兩者的優先級都比關係運算符低,比賦值運算符高。因此,表達式a >b && b > c || b > d相當於((a > b) && (b > c)) || (b > d)。
也就是說,b介於a和c之間,或者b大於d。
儘管對於該例沒必要使用圓括號,但是許多程序員更喜歡使用帶圓括號的第 2 種寫法。這樣做即使不記得邏輯運算符的優先級,表達式的含義也很清楚。
7.3.3 求值順序
除了兩個運算符共享一個運算對象的情況外,C 通常不保證先對複雜表達式中哪部分求值。例如,下面的語句,可能先對表達式5 + 3求值,也可能先對表達式9 + 6求值:
apples = (5 + 3) * (9 + 6);
C 把先計算哪部分的決定權留給編譯器的設計者,以便針對特定系統優化設計。但是,對於邏輯運算符是個例外,C保證邏輯表達式的求值順序是從左往右。&&和||運算符都是序列點,所以程序在從一個運算對像執行到下一個運算對像之前,所有的副作用都會生效。而且,C 保證一旦發現某個元素讓整個表達式無效,便立即停止求值。正是由於有這些規定,才能寫出這樣結構的代碼:
while ((c = getchar) != ' ' && c != '\n')
如上代碼所示,讀取字符直至遇到第1 個空格或換行符。第1 個子表達式把讀取的值賦給c,後面的子表達式會用到c的值。如果沒有求值循序的保證,編譯器可能在給c賦值之前先對後面的表達式求值。
這裡還有一個例子:
if (number != 0 && 12/number == 2)
printf("The number is 5 or 6.\n");
如果number的值是0,那麼第1個子表達式為假,且不再對關係表達式求值。這樣避免了把0作為除數。許多語言都沒有這種特性,知道number為0後,仍繼續檢查後面的條件。
最後,考慮這個例子:
while ( x++ < 10 && x + y < 20)
實際上,&&是一個序列點,這保證了在對&&右側的表達式求值之前,已經遞增了x。
小結:邏輯運算符和表達式
邏輯運算符:
邏輯運算符的運算對像通常是關係表達式。!運算符只需要一個運算對象,其他兩個邏輯運算符都需要兩個運算對象,左側一個,右側一個。
邏輯表達式:
當且僅當expression1和expression2都為真,expression1 && expression2才為真。如果 expression1 或 expression2 為真,expression1 || expression2 為真。如果expression為假,!expression則為真,反之亦然。
求值順序:
邏輯表達式的求值順序是從左往右。一旦發現有使整個表達式為假的因素,立即停止求值。
示例:
6 > 2 && 3 == 3 真
!(6 > 2 && 3 == 3) 假
x != 0 && (20 / x) < 5 只有當x不等於0時,才會對第2個表達式求值
7.3.4 範圍
&&運算符可用於測試範圍。例如,要測試score是否在90~100的範圍內,可以這樣寫:
if (range >= 90 && range <= 100)
printf("Good show!\n");
千萬不要模仿數學上的寫法:
if (90 <= range <= 100)// 千萬不要這樣寫!
printf("Good show!\n");
這樣寫的問題是代碼有語義錯誤,而不是語法錯誤,所以編譯器不會捕獲這樣的問題(雖然可能會給出警告)。由於<=運算符的求值順序是從左往右,所以編譯器把測試表達式解釋為:
(90 <= range) <= 100
子表達式90 <= range的值要麼是1(為真),要麼是0(為假)。這兩個值都小於100,所以不管range的值是多少,整個表達式都恆為真。因此,在範圍測試中要使用&&。
許多代碼都用範圍測試來確定一個字符是否是小寫字母。例如,假設ch是char類型的變量:
if (ch >= 'a' && ch <= 'z')
printf("That's a lowercase character.\n");
該方法僅對於像ASCII這樣的字符編碼有效,這些編碼中相鄰字母與相鄰數字一一對應。但是,對於像EBCDIC這樣的代碼就沒用了。相應的可移植方法是,用ctype.h系列中的islower函數(參見表7.1):
if (islower(ch))
printf("That's a lowercase character.\n");
無論使用哪種特定的字符編碼,islower函數都能正常運行(不過,一些早期的編譯器沒有ctype.h系列)。
7.4 一個統計單詞的程序
現在,我們可以編寫一個統計單詞數量的程序(即,該程序讀取並報告單詞的數量)。該程序還可以計算字符數和行數。先來看看編寫這樣的程序要涉及那些內容。
首先,該程序要逐個字符讀取輸入,知道何時停止讀取。然後,該程序能識別並計算這些內容:字符、行數和單詞。據此我們編寫的偽代碼如下:
讀取一個字符
當有更多輸入時
遞增字符計數
如果讀完一行,遞增行數計數
如果讀完一個單詞,遞增單詞計數
讀取下一個字符
前面有一個輸入循環的模型:
while ((ch = getchar) != STOP)
{
...
}
這裡,STOP表示能標識輸入末尾的某個值。以前我們用過換行符和句點標記輸入的末尾,但是對於一個通用的統計單詞程序,它們都不合適。我們暫時選用一個文本中不常用的字符(如,|)作為輸入的末尾標記。第8章中會介紹更好的方法,以便程序既能處理文本文件,又能處理鍵盤輸入。
現在,我們考慮循環體。因為該程序使用getchar進行輸入,所以每次迭代都要通過遞增計數器來計數。為了統計行數,程序要能檢查換行字符。如果輸入的字符是一個換行符,該程序應該遞增行數計數器。這裡要注意 STOP 字符位於一行的中間的情況。是否遞增行數計數?我們可以作為特殊行計數,即沒有換行符的一行字符。可以通過記錄之前讀取的字符識別這種情況,即如果讀取時發現 STOP 字符的上一個字符不是換行符,那麼這行就是特殊行。
最棘手的部分是識別單詞。首先,必須定義什麼是該程序識別的單詞。我們用一個相對簡單的方法,把一個單詞定義為一個不含空白(即,沒有空格、製表符或換行符)的字符序列。因此,「glymxck」和「r2d2」都算是一個單詞。程序讀取的第 1 個非空白字符即是一個單詞的開始,當讀到空白字符時結束。判斷非空白字符最直接的測試表達式是:
c != ' ' && c != '\n' && c != '\t' /* 如果c不是空白字符,該表達式為真*/
檢測空白字符最直接的測試表達式是:
c == ' ' || c == '\n' || c == '\t' /*如果c是空白字符,該表達式為真*/
然而,使用ctype.h頭文件中的函數isspace更簡單,如果該函數的參數是空白字符,則返回真。所以,如果c是空白字符,isspace(c)為真;如果c不是空白字符,!isspace(c)為真。
要查找一個單詞裡是否有某個字符,可以在程序讀入單詞的首字符時把一個標記(名為 inword)設置為1。也可以在此時遞增單詞計數。然後,只要inword為1(或true),後續的非空白字符都不記為單詞的開始。下一個空白字符,必須重置標記為0(或false),然後程序就準備好讀取下一個單詞。我們把以上分析寫成偽代碼:
如果c不是空白字符,且inword為假
設置inword為真,並給單詞計數
如果c是空白字符,且inword為真
設置inword為假
這種方法在讀到每個單詞的開頭時把inword設置為1(真),在讀到每個單詞的末尾時把inword設置為0(假)。只有在標記從0設置為1時,遞增單詞計數。如果能使用_Bool類型,可以在程序中包含stdbool.h頭文件,把inword的類型設置為bool,其值用true和false表示。如果編譯器不支持這種用法,就把inword的類型設置為int,其值用1和0表示。
如果使用布爾類型的變量,通常習慣把變量自身作為測試條件。如下所示:
用if (inword)代替if (inword == true)
用if (!inword)代替if (inword == false)
可以這樣做的原因是,如果 inword為true,則表達式 inword == true為true;如果 inword為false,則表達式inword == true為false。所以,還不如直接用inword作為測試條件。類似地,!inword的值與表達式inword == false的值相同(非真即false,非假即true)。
程序清單7.7把上述思路(識別行、識別不完整的行和識別單詞)翻譯了成C代碼。
程序清單7.7 wordcnt.c程序
// wordcnt.c -- 統計字符數、單詞數、行數
#include <stdio.h>
#include <ctype.h> // 為isspace函數提供原型
#include <stdbool.h> // 為bool、true、false提供定義
#define STOP '|'
int main(void)
{
char c;// 讀入字符
char prev; // 讀入的前一個字符
long n_chars = 0L;// 字符數
int n_lines = 0; // 行數
int n_words = 0; // 單詞數
int p_lines = 0; // 不完整的行數
bool inword = false; // 如果c在單詞中,inword 等於 true
printf("Enter text to be analyzed (| to terminate):\n");
prev = '\n'; // 用於識別完整的行
while ((c = getchar) != STOP)
{
n_chars++; // 統計字符
if (c == '\n')
n_lines++; // 統計行
if (!isspace(c) && !inword)
{
inword = true;// 開始一個新的單詞
n_words++; // 統計單詞
}
if (isspace(c) && inword)
inword = false;// 打到單詞的末尾
prev = c; // 保存字符的值
}
if (prev != '\n')
p_lines = 1;
printf("characters = %ld, words = %d, lines = %d, ",
n_chars, n_words, n_lines);
printf("partial lines = %d\n", p_lines);
return 0;
}
下面是運行該程序後的一個輸出示例:
Enter text to be analyzed (| to terminate):
Reason is a
powerful servant but
an inadequate master.
|
characters = 55, words = 9, lines = 3, partial lines = 0
該程序使用邏輯運算符把偽代碼翻譯成C代碼。例如,把下面的偽代碼:
如果c不是空白字符,且inword為假
翻譯成如下C代碼:
if (!isspace(c) &&!inword)
再次提醒讀者注意,!inword 與 inword == false 等價。上面的整個測試條件比單獨判斷每個空白字符的可讀性高:
if (c != ' ' && c != '\n' && c != '\t' && !inword)
上面的兩種形式都表示「如果c不是空白字符,且如果c不在單詞裡」。如果兩個條件都滿足,則一定是一個新單詞的開頭,所以要遞增n_words。如果位於單詞中,滿足第1個條件,但是inword為true,就不遞增 n_word。當讀到下一個空白字符時,inword 被再次設置為 false。檢查代碼,查看一下如果單詞之間有多個空格時,程序是否能正常運行。第 8 章講解了如何修正這個問題,讓該程序能統計文件中的單詞量。
7.5 條件運算符:?:
C提供條件表達式(conditional expression)作為表達if else語句的一種便捷方式,該表達式使用?:條件運算符。該運算符分為兩部分,需要 3 個運算對象。回憶一下,帶一個運算對象的運算符稱為一元運算符,帶兩個運算對象的運算符稱為二元運算符。以此類推,帶 3 個運算對象的運算符稱為三元運算符。條件運算符是C語言中唯一的三元運算符。下面的代碼得到一個數的絕對值:
x = (y < 0) ? -y : y;
在=和;之間的內容就是條件表達式,該語句的意思是「如果y小於0,那麼x = -y;否則,x = y」。用if else可以這樣表達:
if (y < 0)
x = -y;
else
x = y;
條件表達式的通用形式如下:
expression1 ? expression2 : expression3
如果 expression1 為真(非 0),那麼整個條件表達式的值與 expression2 的值相同;如果expression1為假(0),那麼整個條件表達式的值與expression3的值相同。
需要把兩個值中的一個賦給變量時,就可以用條件表達式。典型的例子是,把兩個值中的最大值賦給變量:
max = (a > b) ? a : b;
如果a大於b,那麼將max設置為a;否則,設置為b。
通常,條件運算符完成的任務用 if else 語句也可以完成。但是,使用條件運算符的代碼更簡潔,而且編譯器可以生成更緊湊的程序代碼。
我們來看程序清單7.8中的油漆程序,該程序計算刷給定平方英尺的面積需要多少罐油漆。基本算法很簡單:用平方英尺數除以每罐油漆能刷的面積。但是,商店只賣整罐油漆,不會拆分來賣,所以如果計算結果是1.7罐,就需要兩罐。因此,該程序計算得到帶小數的結果時應該進1。條件運算符常用於處理這種情況,而且還要根據單複數分別打印can和cans。
程序清單7.8 paint.c程序
/* paint.c -- 使用條件運算符 */
#include <stdio.h>
#define COVERAGE 350 // 每罐油漆可刷的面積(單位:平方英尺)
int main(void)
{
int sq_feet;
int cans;
printf("Enter number of square feet to be painted:\n");
while (scanf("%d", &sq_feet) == 1)
{
cans = sq_feet / COVERAGE;
cans += ((sq_feet % COVERAGE == 0)) ? 0 : 1;
printf("You need %d %s of paint.\n", cans,
cans == 1 ? "can" : "cans");
printf("Enter next value (q to quit):\n");
}
return 0;
}
下面是該程序的運行示例:
Enter number of square feet to be painted:
349
You need 1 can of paint.
Enter next value (q to quit):
351
You need 2 cans of paint.
Enter next value (q to quit):
q
該程序使用的變量都是int類型,除法的計算結果(sq_feet / COVERAGE)會被截斷。也就是說, 351/350得1。所以,cans被截斷成整數部分。如果sq_feet % COVERAGE得0,說明sq_feet被COVERAGE整除,cans的值不變;否則,肯定有餘數,就要給cans加1。這由下面的語句完成:
cans += ((sq_feet % COVERAGE == 0)) ? 0 : 1;
該語句把+=右側表達式的值加上cans,再賦給cans。右側表達式是一個條件表達式,根據sq_feet是否能被COVERAGE整除,其值為0或1。
printf函數中的參數也是一個條件表達式:
cans == 1 ? "can" : "cans");
如果cans的值是1,則打印can;否則,打印cans。這也說明了條件運算符的第2個和第3個運算對象可以是字符串。
小結:條件運算符
條件運算符:?:
一般註解:
條件運算符需要3個運算對象,每個運算對象都是一個表達式。其通用形式如下:
expression1 ? expression2 : expression3
如果expression1為真,整個條件表達式的值是expression2的值;否則,是expression3的值。
示例:
(5 > 3) ? 1 : 2 值為1
(3 > 5) ? 1 : 2 值為2
(a > b) ? a : b 如果a >b,則取較大的值
7.6 循環輔助:continue和break
一般而言,程序進入循環後,在下一次循環測試之前會執行完循環體中的所有語句。continue 和break語句可以根據循環體中的測試結果來忽略一部分循環內容,甚至結束循環。
7.6.1 continue語句
3種循環都可以使用continue語句。執行到該語句時,會跳過本次迭代的剩餘部分,並開始下一輪迭代。如果continue語句在嵌套循環內,則只會影響包含該語句的內層循環。程序清單7.9中的簡短程序演示了如何使用continue。
程序清單7.9 skippart.c程序
/* skippart.c -- 使用continue跳過部分循環 */
#include <stdio.h>
int main(void)
{
const float MIN = 0.0f;
const float MAX = 100.0f;
float score;
float total = 0.0f;
int n = 0;
float min = MAX;
float max = MIN;
printf("Enter the first score (q to quit): ");
while (scanf("%f", &score) == 1)
{
if (score < MIN || score > MAX)
{
printf("%0.1f is an invalid value.Try again: ",score);
continue; // 跳轉至while循環的測試條件
}
printf("Accepting %0.1f:\n", score);
min = (score < min) ? score : min;
max = (score > max) ? score : max;
total += score;
n++;
printf("Enter next score (q to quit): ");
}
if (n > 0)
{
printf("Average of %d scores is %0.1f.\n", n, total / n);
printf("Low = %0.1f, high = %0.1f\n", min, max);
}
else
printf("No valid scores were entered.\n");
return 0;
}
在程序清單7.9中,while循環讀取輸入,直至用戶輸入非數值數據。循環中的if語句篩選出無效的分數。假設輸入 188,程序會報告:188 is an invalid value。在本例中,continue 語句讓程序跳過處理有效輸入部分的代碼。程序開始下一輪循環,準備讀取下一個輸入值。
注意,有兩種方法可以避免使用continue,一是省略continue,把剩餘部分放在一個else塊中:
if (score < 0 || score > 100)
/* printf語句 */
else
{
/* 語句*/
}
另一種方法是,用以下格式來代替:
if (score >= 0 && score <= 100)
{
/* 語句 */
}
這種情況下,使用continue的好處是減少主語句組中的一級縮進。當語句很長或嵌套較多時,緊湊簡潔的格式提高了代碼的可讀性。
continue還可用作佔位符。例如,下面的循環讀取並丟棄輸入的數據,直至讀到行末尾:
while (getchar != '\n')
;
當程序已經讀取一行中的某些內容,要跳至下一行開始處時,這種用法很方便。問題是,一般很難注意到一個單獨的分號。如果使用continue,可讀性會更高:
while (getchar != '\n')
continue;
如果用了continue沒有簡化代碼反而讓代碼更複雜,就不要使用continue。例如,考慮下面的程序段:
while ((ch = getchar ) != '\n')
{
if (ch == '\t')
continue;
putchar(ch);
}
該循環跳過製表符,並在讀到換行符時退出循環。以上代碼這樣表示更簡潔:
while ((ch = getchar) != '\n')
if (ch != '\t')
putchar(ch);
通常,在這種情況下,把if的測試條件的關係反過來便可避免使用continue。
以上介紹了continue語句讓程序跳過循環體的餘下部分。那麼,從何處開始繼續循環?對於while和 do while 循環,執行 continue 語句後的下一個行為是對循環的測試表達式求值。考慮下面的循環:
count = 0;
while (count < 10)
{
ch = getchar;
if (ch == '\n')
continue;
putchar(ch);
count++;
}
該循環讀取10個字符(除換行符外,因為當ch是換行符時,程序會跳過count++;語句)並重新顯示它們,其中不包括換行符。執行continue後,下一個被求值的表達式是循環測試條件。
對於for循環,執行continue後的下一個行為是對更新表達式求值,然後是對循環測試表達式求值。例如,考慮下面的循環:
for (count = 0; count < 10; count++)
{
ch = getchar;
if (ch == '\n')
continue;
putchar(ch);
}
該例中,執行完continue後,首先遞增count,然後將遞增後的值和10作比較。因此,該循環與上面while循環的例子稍有不同。while循環的例子中,除了換行符,其餘字符都顯示;而本例中,換行符也計算在內,所以讀取的10個字符中包含換行符。
7.6.2 break語句
程序執行到循環中的break語句時,會終止包含它的循環,並繼續執行下一階段。把程序清單7.9中的continue替換成break,在輸入188時,不是跳至執行下一輪循環,而是導致退出當前循環。圖7.3比較了break和continue。如果break語句位於嵌套循環內,它只會影響包含它的當前循環。
圖7.3 比較break和continue
break還可用於因其他原因退出循環的情況。程序清單7.10用一個循環計算矩形的面積。如果用戶輸入非數字作為矩形的長或寬,則終止循環。
程序清單7.10 break.c程序
/* break.c -- 使用 break 退出循環 */
#include <stdio.h>
int main(void)
{
float length, width;
printf("Enter the length of the rectangle:\n");
while (scanf("%f", &length) == 1)
{
printf("Length = %0.2f:\n", length);
printf("Enter its width:\n");
if (scanf("%f", &width) != 1)
break;
printf("Width = %0.2f:\n", width);
printf("Area = %0.2f:\n", length * width);
printf("Enter the length of the rectangle:\n");
}
printf("Done.\n");
return 0;
}
可以這樣控制循環:
while (scanf("%f %f", &length, &width) == 2)
但是,用break可以方便顯示用戶輸入的值。
和continue一樣,如果用了break代碼反而更複雜,就不要使用break。例如,考慮下面的循環:
while ((ch = getchar) != '\n')
{
if (ch == '\t')
break;
putchar(ch);
}
如果把兩個測試條件放在一起,邏輯就更清晰了:
while ((ch = getchar ) != '\n' && ch != '\t')
putchar(ch);
break語句對於稍後討論的switch語句而言至關重要。
在for循環中的break和continue的情況不同,執行完break語句後會直接執行循環後面的第1條語句,連更新部分也跳過。嵌套循環內層的break只會讓程序跳出包含它的當前循環,要跳出外層循環還需要一個break:
int p, q;
scanf("%d", &p);
while (p > 0)
{
printf("%d\n", p);
scanf("%d", &q);
while (q > 0)
{
printf("%d\n", p*q);
if (q > 100)
break; // 跳出內層循環
scanf("%d", &q);
}
if (q > 100)
break; // 跳出外層循環
scanf("%d", &p);
}
7.7 多重選擇:switch和break
使用條件運算符和 if else 語句很容易編寫二選一的程序。然而,有時程序需要在多個選項中進行選擇。可以用if else if...else來完成。但是,大多數情況下使用switch語句更方便。程序清單7.11演示了如何使用switch語句。該程序讀入一個字母,然後打印出與該字母開頭的動物名。
程序清單7.11 animals.c程序
/* animals.c -- 使用switch語句 */
#include <stdio.h>
#include <ctype.h>
int main(void)
{
char ch;
printf("Give me a letter of the alphabet, and I will give ");
printf("an animal name\nbeginning with that letter.\n");
printf("Please type in a letter; type # to end my act.\n");
while ((ch = getchar) != '#')
{
if ('\n' == ch)
continue;
if (islower(ch))/* 只接受小寫字母*/
switch (ch)
{
case 'a':
printf("argali, a wild sheep of Asia\n");
break;
case 'b':
printf("babirusa, a wild pig of Malay\n");
break;
case 'c':
printf("coati, racoonlike mammal\n");
break;
case 'd':
printf("desman, aquatic, molelike critter\n");
break;
case 'e':
printf("echidna, the spiny anteater\n");
break;
case 'f':
printf("fisher, brownish marten\n");
break;
default:
printf("That's a stumper!\n");
}/* switch結束*/
else
printf("I recognize only lowercase letters.\n");
while (getchar != '\n')
continue; /* 跳過輸入行的剩餘部分 */
printf("Please type another letter or a #.\n");
} /* while循環結束 */
printf("Bye!\n");
return 0;
}
篇幅有限,我們只編到f,後面的字母以此類推。在進一步解釋該程序之前,先看看輸出示例:
Give me a letter of the alphabet, and I will give an animal name
beginning with that letter.
Please type in a letter; type # to end my act.
a [enter]
argali, a wild sheep of Asia
Please type another letter or a #.
dab [enter]
desman, aquatic, molelike critter
Please type another letter or a #.
r [enter]
That's a stumper!
Please type another letter or a #.
Q [enter]
I recognize only lowercase letters.
Please type another letter or a #.
# [enter]
Bye!
該程序的兩個主要特點是:使用了switch語句和它對輸出的處理。我們先分析switch的工作原理。
7.7.1 switch語句
要對緊跟在關鍵字 switch 後圓括號中的表達式求值。在程序清單 7.11 中,該表達式是剛輸入給 ch的值。然後程序掃瞄標籤(這裡指,case 'a' :、case 'b' :等)列表,直到發現一個匹配的值為止。然後程序跳轉至那一行。如果沒有匹配的標籤怎麼辦?如果有default :標籤行,就跳轉至該行;否則,程序繼續執行在switch後面的語句。
break語句在其中起什麼作用?它讓程序離開switch語句,跳至switch語句後面的下一條語句(見圖7.4)。如果沒有break語句,就會從匹配標籤開始執行到switch末尾。例如,如果刪除該程序中的所有break語句,運行程序後輸入d,其交互的輸出結果如下:
圖7.4 switch中有break和沒有break的程序流
Give me a letter of the alphabet, and I will give an animal name
beginning with that letter.
Please type in a letter; type # to end my act.
d [enter]
desman, aquatic, molelike critter
echidna, the spiny anteater
fisher, a brownish marten
That's a stumper!
Please type another letter or a #.
# [enter]
Bye!
如上所示,執行了從case 'd':到switch語句末尾的所有語句。
順帶一提,break語句可用於循環和switch語句中,但是continue只能用於循環中。儘管如此,如果switch語句在一個循環中,continue便可作為switch語句的一部分。這種情況下,就像在其他循環中一樣,continue讓程序跳出循環的剩餘部分,包括switch語句的其他部分。
如果讀者熟悉Pascal,會發現switch語句和Pascal的case語句類似。它們最大的區別在於,如果只希望處理某個帶標籤的語句,就必須在switch語句中使用break語句。另外,C語言的case一般都指定一個值,不能使用一個範圍。
switch在圓括號中的測試表達式的值應該是一個整數值(包括char類型)。case標籤必須是整數類型(包括char類型)的常量或整型常量表達式(即,表達式中只包含整型常量)。不能用變量作為case標籤。switch的構造如下:
switch ( 整型表達式)
{
case 常量1:
語句 <--可選
case 常量2:
語句 <--可選
default : <--可選
語句 <--可選
}
7.7.2 只讀每行的首字符
animals.c(程序清單7.11)的另一個獨特之處是它讀取輸入的方式。運行程序時讀者可能注意到了,當輸入dab時,只處理了第1個字符。這種丟棄一行中其他字符的行為,經常出現在響應單字符的交互程序中。可以用下面的代碼實現這樣的行為:
while (getchar != '\n')
continue;/* 跳過輸入行的其餘部分 */
循環從輸入中讀取字符,包括按下Enter鍵產生的換行符。注意,函數的返回值並沒有賦給ch,以上代碼所做的只是讀取並丟棄字符。由於最後丟棄的字符是換行符,所以下一個被讀取的字符是下一行的首字母。在外層的while循環中,getchar讀取首字母並賦給ch。
假設用戶一開始就按下Enter鍵,那麼程序讀到的首個字符就是換行符。下面的代碼處理這種情況:
if (ch == '\n')
continue;
7.7.3 多重標籤
如程序清單7.12所示,可以在switch語句中使用多重case標籤。
程序清單7.12 vowels.c程序
// vowels.c -- 使用多重標籤
#include <stdio.h>
int main(void)
{
char ch;
int a_ct, e_ct, i_ct, o_ct, u_ct;
a_ct = e_ct = i_ct = o_ct = u_ct = 0;
printf("Enter some text; enter # to quit.\n");
while ((ch = getchar) != '#')
{
switch (ch)
{
case 'a':
case 'A': a_ct++;
break;
case 'e':
case 'E': e_ct++;
break;
case 'i':
case 'I': i_ct++;
break;
case 'o':
case 'O': o_ct++;
break;
case 'u':
case 'U': u_ct++;
break;
default: break;
} // switch結束
} // while循環結束
printf("number of vowels: A E I O U\n");
printf(" %4d %4d %4d %4d %4d\n",
a_ct, e_ct, i_ct, o_ct, u_ct);
return 0;
}
假設如果ch是字母i,switch語句會定位到標籤為case 'i' :的位置。由於該標籤沒有關聯break語句,所以程序流直接執行下一條語句,即i_ct++;。如果 ch是字母I,程序流會直接定位到case 'I' :。本質上,兩個標籤都指的是相同的語句。
嚴格地說,case 'U'的 break 語句並不需要。因為即使刪除這條 break 語句,程序流會接著執行switch中的下一條語句,即default : break;。所以,可以把case 'U'的break語句去掉以縮短代碼。但是從另一方面看,保留這條break語句可以防止以後在添加新的case(例如,把y作為元音)時遺漏break語句。
下面是該程序的運行示例:
Enter some text; enter # to quit.
I see under the overseer.#
number of vowels: A E I O U
07 1 1 1
在該例中,如果使用ctype.h系列的toupper函數(參見表7.2)可以避免使用多重標籤,在進行測試之前就把字母轉換成大寫字母:
while ((ch = getchar) != '#')
{
ch = toupper(ch);
switch (ch)
{
case 'A': a_ct++;
break;
case 'E': e_ct++;
break;
case 'I': i_ct++;
break;
case 'O': o_ct++;
break;
case 'U': u_ct++;
break;
default: break;
} // switch結束
} // while循環結束
或者,也可以先不轉換ch,把toupper(ch)放進switch的測試條件中:switch(toupper(ch))。
小結:帶多重選擇的switch語句
關鍵字:switch
一般註解:
程序根據expression的值跳轉至相應的case標籤處。然後,執行剩下的所有語句,除非執行到break語句進行重定向。expression和case標籤都必須是整數值(包括char類型),標籤必須是常量或完全由常量組成的表達式。如果沒有case標籤與expression的值匹配,控制則轉至標有default的語句(如果有的話);否則,將轉至執行緊跟在switch語句後面的語句。
形式:
switch ( expression )
{
case label1 : statement1//使用break跳出switch
case label2 : statement2
default : statement3
}
可以有多個標籤語句,default語句可選。
示例:
switch (choice)
{
case 1 :
case 2 : printf("Darn tootin'!\n"); break;
case 3 : printf("Quite right!\n");
case 4 : printf("Good show!\n"); break;
default: printf("Have a nice day.\n");
}
如果choice的值是1或2,打印第1條消息;如果choice的值是3,打印第2條和第3條消息(程序繼續執行後續的語句,因為case 3後面沒有break語句);如果choice的值是4,則打印第3條消息;如果choice的值是其他值只打印最後一條消息。
7.7.4 switch和if else
何時使用switch?何時使用if else?你經常會別無選擇。如果是根據浮點類型的變量或表達式來選擇,就無法使用 switch。如果根據變量在某範圍內決定程序流的去向,使用 switch 就很麻煩,這種情況用if就很方便:
if (integer < 1000 && integer > 2)
使用switch要涵蓋以上範圍,需要為每個整數(3~999)設置case標籤。但是,如果使用switch,程序通常運行快一些,生成的代碼少一些。
7.8 goto語句
早期版本的BASIC和FORTRAN所依賴的goto語句,在C中仍然可用。但是C和其他兩種語言不同,沒有goto語句C程序也能運行良好。Kernighan和Ritchie提到goto語句「易被濫用」,並建議「謹慎使用,或者根本不用」。首先,介紹一下如何使用goto語句;然後,講解為什麼通常不需要它。
goto語句有兩部分:goto和標籤名。標籤的命名遵循變量命名規則,如下所示:
goto part2;
要讓這條語句正常工作,函數還必須包含另一條標為part2的語句,該語句以標籤名後緊跟一個冒號開始:
part2: printf("Refined analysis:\n");
7.8.1 避免使用goto
原則上,根本不用在C程序中使用goto語句。但是,如果你曾經學過FORTRAN或BASIC(goto對這兩種語言而言都必不可少),可能還會依賴用goto來編程。為了幫助你克服這個習慣,我們先概述一些使用goto的常見情況,然後再介紹C的解決方案。
處理包含多條語句的if語句:
if (size > 12)
goto a;
goto b;
a: cost = cost * 1.05;
flag = 2;
b: bill = cost * flag;
對於以前的BASIC和FORTRAN,只有直接跟在if條件後面的一條語句才屬於if,不能使用塊或復合語句。我們把以上模式轉換成等價的C代碼,標準C用復合語句或塊來處理這種情況:
if (size > 12)
{
cost = cost * 1.05;
flag = 2;
}
bill = cost * flag;
二選一:
if (ibex > 14)
goto a;
sheds = 2;
goto b;
a: sheds= 3;
b: help = 2 * sheds;
C通過if else表達二選一更清楚:
if (ibex > 14)
sheds = 3;
else
sheds = 2;
help = 2 * sheds;
實際上,新版的BASIC和FORTRAN已經把else納入新的語法中。
創建不確定循環:
readin: scanf("%d", &score);
if (score < O)
goto stage2;
lots of statements
goto readin;
stage2: more stuff;
C用while循環代替:
scanf("%d", &score);
while (score <= 0)
{
lots of statements
scanf("%d", &score);
}
more stuff;
跳轉至循環末尾,並開始下一輪迭代。C使用continue語句代替。
跳出循環。C使用break語句。實際上,break和continue是goto的特殊形式。使用break和 continue 的好處是:其名稱已經表明它們的用法,而且這些語句不使用標籤,所以不用擔心把標籤放錯位置導致的危險。
胡亂跳轉至程序的不同部分。簡而言之,不要這樣做!
但是,C程序員可以接受一種goto的用法——出現問題時從一組嵌套循環中跳出(一條break語句只能跳出當前循環):
while (funct > 0)
{
for (i = 1, i <= 100; i++)
{
for (j = 1; j <= 50; j++)
{
其他語句
if (問題)
goto help;
其他語句
}
其他語句
}
其他語句
}
其他語句
help: 語句
從其他例子中也能看出,程序中使用其他形式比使用goto的條理更清晰。當多種情況混在一起時,這種差異更加明顯。哪些goto語句可以幫助if語句?哪些可以模仿if else?哪些控制循環?哪些是因為程序無路可走才不得已放在那裡?過度地使用 goto 語句,會讓程序錯綜複雜。如果不熟悉goto語句,就不要使用它。如果已經習慣使用goto語句,試著改掉這個毛病。諷刺地是,雖然C根本不需要goto,但是它的goto比其他語言的goto好用,因為C允許在標籤中使用描述性的單詞而不是數字。
小結:程序跳轉
關鍵字:break、continue、goto
一般註解:
這3種語句都能使程序流從程序的一處跳轉至另一處。
break語句:
所有的循環和switch語句都可以使用break語句。它使程序控制跳出當前循環或switch語句的剩餘部分,並繼續執行跟在循環或switch後面的語句。
示例:
switch (number)
{
case 4: printf("That's a good choice.\n");
break;
case 5: printf("That's a fair choice.\n");
break;
default: printf("That's a poor choice.\n");
}
continue語句:
所有的循環都可以使用continue語句,但是switch語句不行。continue語句使程序控制跳出循環的剩餘部分。對於while或for循環,程序執行到continue語句後會開始進入下一輪迭代。對於do while循環,對出口條件求值後,如有必要會進入下一輪迭代。
示例:
while ((ch = getchar) != '\n')
{
if (ch == ' ')
continue;
putchar(ch);
chcount++;
}
以上程序段把用戶輸入的字符再次顯示在屏幕上,並統計非空格字符。
goto語句:
goto語句使程序控制跳轉至相應標籤語句。冒號用於分隔標籤和標籤語句。標籤名遵循變量命名規則。標籤語句可以出現在goto的前面或後面。
形式:
goto label ;
label : statement
示例:
top : ch = getchar;
if (ch != 'y')
goto top;
7.9 關鍵概念
智能的一個方面是,根據情況做出相應的響應。所以,選擇語句是開發具有智能行為程序的基礎。C語言通過if、if else和switch語句,以及條件運算符(?:)可以實現智能選擇。
if 和 if else 語句使用測試條件來判斷執行哪些語句。所有非零值都被視為 true,零被視為false。測試通常涉及關係表達式(比較兩個值)、邏輯表達式(用邏輯運算符組合或更改其他表達式)。
要記住一個通用原則,如果要測試兩個條件,應該使用邏輯運算符把兩個完整的測試表達式組合起來。例如,下面這些是錯誤的:
if (a < x < z) // 錯誤,沒有使用邏輯運算符
…
if (ch != 'q' && != 'Q') // 錯誤,缺少完整的測試表達式
…
正確的方式是用邏輯運算符連接兩個關係表達式:
if (a < x && x < z) // 使用&&組合兩個表達式
…
if (ch != 'q' && ch != 'Q')// 使用&&組合兩個表達式
…
對比這兩章和前幾章的程序示例可以發現:使用第6章、第7章介紹的語句,可以寫出功能更強大、更有趣的程序。
7.10 本章小結
本章介紹了很多內容,我們來總結一下。if語句使用測試條件控制程序是否執行測試條件後面的一條簡單語句或復合語句。如果測試表達式的值是非零值,則執行語句;如果測試表達式的值是零,則不執行語句。if else語句可用於二選一的情況。如果測試條件是非零,則執行else前面的語句;如果測試表達式的值是零,則執行else後面的語句。在else後面使用另一個if語句形成else if,可構造多選一的結構。
測試條件通常都是關係表達式,即用一個關係運算符(如,<或==)的表達式。使用C的邏輯運算符,可以把關係表達式組合成更複雜的測試條件。
在多數情況下,用條件運算符(?:)寫成的表達式比if else語句更簡潔。
ctype.h系列的字符函數(如,issapce和isalpha)為創建以分類字符為基礎的測試表達式提供了便捷的工具。
switch 語句可以在一系列以整數作為標籤的語句中進行選擇。如果緊跟在 switch 關鍵字後的測試條件的整數值與某標籤匹配,程序就轉至執行匹配的標籤語句,然後在遇到break之前,繼續執行標籤語句後面的語句。
break、continue和goto語句都是跳轉語句,使程序流跳轉至程序的另一處。break語句使程序跳轉至緊跟在包含break語句的循環或switch末尾的下一條語句。continue語句使程序跳出當前循環的剩餘部分,並開始下一輪迭代。
7.11 複習題
複習題的參考答案在附錄A中。
1.判斷下列表達式是true還是false。
a100 > 3 && 'a'>'c'
b100 > 3 || 'a'>'c'
c!(100>3)
2.根據下列描述的條件,分別構造一個表達式:
a umber等於或大於90,但是小於100
b h不是字符q或k
c umber在1~9之間(包括1和9),但不是5
d umber不在1~9之間
3.下面的程序關係表達式過於複雜,而且還有些錯誤,請簡化並改正。
#include <stdio.h>
int main(void)/* 1 */
{ /* 2 */
int weight, height; /* weight以磅為單位,height以英吋為單位 *//* 4 */
scanf("%d , weight, height);/* 5 */
if (weight < 100 && height > 64)/* 6 */
if (height >= 72) /* 7 */
printf("You are very tall for your weight.\n");
else if (height < 72 &&> 64)/* 9 */
printf("You are tall for your weight.\n");/* 10 */
else if (weight > 300 && !(weight <= 300)/* 11 */
&& height < 48)/* 12 */
if (!(height >= 48)) /* 13 */
printf(" You are quite short for your weight.\n");
else /* 15 */
printf("Your weight is ideal.\n"); /* 16 */
/* 17 */
return 0;
}
4.下列個表達式的值是多少?
a.5 > 2
b.3 + 4 > 2 && 3 < 2
c.x >= y || y > x
d.d = 5 + ( 6 > 2 )
e.'X' > 'T' ? 10 : 5
f.x > y ? y > x : x > y
5.下面的程序將打印什麼?
#include <stdio.h>
int main(void)
{
int num;
for (num = 1; num <= 11; num++)
{
if (num % 3 == 0)
putchar('$');
else
putchar('*');
putchar('#');
putchar('%');
}
putchar('\n');
return 0;
}
6.下面的程序將打印什麼?
#include <stdio.h>
int main(void)
{
int i = 0;
while (i < 3) {
switch (i++) {
case 0: printf("fat ");
case 1: printf("hat ");
case 2: printf("cat ");
default: printf("Oh no!");
}
putchar('\n');
}
return 0;
}
7.下面的程序有哪些錯誤?
#include <stdio.h>
int main(void)
{
char ch;
int lc = 0; /* 統計小寫字母
int uc = 0; /* 統計大寫字母
int oc = 0; /* 統計其他字母
while ((ch = getchar) != '#')
{
if ('a' <= ch >= 'z')
lc++;
else if (!(ch < 'A') || !(ch > 'Z')
uc++;
oc++;
}
printf(%d lowercase, %d uppercase, %d other, lc, uc, oc);
return 0;
}
8.下面的程序將打印什麼?
/* retire.c */
#include <stdio.h>
int main(void)
{
int age = 20;
while (age++ <= 65)
{
if ((age % 20) == 0) /* age是否能被20整除? */
printf("You are %d.Here is a raise.\n", age);
if (age = 65)
printf("You are %d.Here is your gold watch.\n", age);
}
return 0;
}
9.給定下面的輸入時,以下程序將打印什麼?
q
c
h
b
#include <stdio.h>
int main(void)
{
char ch;
while ((ch = getchar) != '#')
{
if (ch == '\n')
continue;
printf("Step 1\n");
if (ch == 'c')
continue;
else if (ch == 'b')
break;
else if (ch == 'h')
goto laststep;
printf("Step 2\n");
laststep: printf("Step 3\n");
}
printf("Done\n");
return 0;
}
10.重寫複習題9,但這次不能使用continue和goto語句。
7.12 編程練習
1.編寫一個程序讀取輸入,讀到#字符停止,然後報告讀取的空格數、換行符數和所有其他字符的數量。
2.編寫一個程序讀取輸入,讀到#字符停止。程序要打印每個輸入的字符以及對應的ASCII碼(十進制)。一行打印8個字符。建議:使用字符計數和求模運算符(%)在每8個循環週期時打印一個換行符。
3.編寫一個程序,讀取整數直到用戶輸入 0。輸入結束後,程序應報告用戶輸入的偶數(不包括 0)個數、這些偶數的平均值、輸入的奇數個數及其奇數的平均值。
4.使用if else語句編寫一個程序讀取輸入,讀到#停止。用感歎號替換句號,用兩個感歎號替換原來的感歎號,最後報告進行了多少次替換。
5.使用switch重寫練習4。
6.編寫程序讀取輸入,讀到#停止,報告ei出現的次數。
注意
該程序要記錄前一個字符和當前字符。用「Receive your eieio award」這樣的輸入來測試。
7.編寫一個程序,提示用戶輸入一周工作的小時數,然後打印工資總額、稅金和淨收入。做如下假設:
a.基本工資 = 1000美元/小時
b.加班(超過40小時) = 1.5倍的時間
c.稅率: 前300美元為15%
續150美元為20%
餘下的為25%
用#define定義符號常量。不用在意是否符合當前的稅法。
8.修改練習7的假設a,讓程序可以給出一個供選擇的工資等級菜單。使用switch完成工資等級選擇。運行程序後,顯示的菜單應該類似這樣:
*****************************************************************
Enter the number corresponding to the desired pay rate or action:
1) $8.75/hr 2) $9.33/hr
3) $10.00/hr 4) $11.20/hr
5) quit
*****************************************************************
如果選擇 1~4 其中的一個數字,程序應該詢問用戶工作的小時數。程序要通過循環運行,除非用戶輸入 5。如果輸入 1~5 以外的數字,程序應提醒用戶輸入正確的選項,然後再重複顯示菜單提示用戶輸入。使用#define創建符號常量表示各工資等級和稅率。
9.編寫一個程序,只接受正整數輸入,然後顯示所有小於或等於該數的素數。
10.1988年的美國聯邦稅收計劃是近代最簡單的稅收方案。它分為4個類別,每個類別有兩個等級。
下面是該稅收計劃的摘要(美元數為應徵稅的收入):
例如,一位工資為20000美元的單身納稅人,應繳納稅費0.15×17850+0.28×(20000−17850)美元。編寫一個程序,讓用戶指定繳納稅金的種類和應納稅收入,然後計算稅金。程序應通過循環讓用戶可以多次輸入。
11.ABC 郵購雜貨店出售的洋薊售價為 2.05 美元/磅,甜菜售價為 1.15 美元/磅,胡蘿蔔售價為 1.09美元/磅。在添加運費之前,100美元的訂單有5%的打折優惠。少於或等於5磅的訂單收取6.5美元的運費和包裝費,5磅~20磅的訂單收取14美元的運費和包裝費,超過20磅的訂單在14美元的基礎上每續重1磅增加0.5美元。編寫一個程序,在循環中用switch語句實現用戶輸入不同的字母時有不同的響應,即輸入a的響應是讓用戶輸入洋薊的磅數,b是甜菜的磅數,c是胡蘿蔔的磅數,q 是退出訂購。程序要記錄累計的重量。即,如果用戶輸入 4 磅的甜菜,然後輸入 5磅的甜菜,程序應報告9磅的甜菜。然後,該程序要計算貨物總價、折扣(如果有的話)、運費和包裝費。隨後,程序應顯示所有的購買信息:物品售價、訂購的重量(單位:磅)、訂購的蔬菜費用、訂單的總費用、折扣(如果有的話)、運費和包裝費,以及所有的費用總額。