本章重點
u 語句和語句塊
u 變量的作用域
u 順序結構
u 判斷結構
u 循環結構
從本章開始,讀者將可以真真正正地寫一個完整的C程序了!我們將在本章中介紹C程序的三種最基本的結構:順序結構,選擇結構和循環結構。讀者們很快就會發現,掌握了這些最基本的程序結構之後,我們就可以用C語言寫出很多很多有意思的程序。
如果把一個C程序比喻成一棟高樓,那麼一行行的語句就是壘成高樓的磚塊。程序員的工作就是把這些相對簡單的磚塊組織在一起,建造出風格不同,功能不同的建築。
粗略地說,一條簡單語句就是一個最基本的執行單元。在前面幾章中我們已經接觸了很多簡單語句。比如:Hello world中的printf:
printf("Hello world!\n");
和緊接著的返回語句:
return 0;
變量的定義和聲明:
int i = 10;
簡單的運算:
sum = 1.7f + 2.5f;
這是一條賦值語句,它將右邊數學表達式的結果賦值給左邊的變量。在書寫簡單語句時,不要忘記在結尾加上分號。
語句塊是用花括號括起來的一行或多行語句。比如:
{
float sum = 0.0f;
sum += 3.5f;
printf("%f", sum);
}
這是一個語句塊,它包含了三條語句:第一條語句定義了一個變量sum,並將sum的值初始化為0;第二條語句將sum的值自增了3.5;第三條語句將sum的值輸出到控制台上。
將語句組織成語句塊一般並不會影響語句的執行效果,不過合理的使用語句塊可以讓程序在邏輯上顯得更加清晰。我們在本章隨後將要看到的判斷結構和循環結構中都包含了語句塊,這些語句塊能讓我們更清楚地識別這些結構。
我們在第三章中簡單地介紹過變量。現在我們已經知道,每一個變量需要有自己的名字和類型。如果變量被初始化,那麼它還會有一個初值。在這一節中,我們將介紹變量的作用域。讀者可以把變量的作用域理解為這個變量在程序的哪些地方是可以被有效訪問的:在這個有效范圍之內,我們可以正確操作這個變量,如給變量賦值,對變量進行計算,輸入輸出等;在這個有效范圍之外,我們不能使用這個變量。
變量包括局部變量和全局變量。局部變量定義在一個函數內,只在這個函數內使用,比如我們之前的所有例程中,所有的變量都是定義在main函數內部的,也只能在main函數裡使用;全局變量定義在函數外,可以被程序的所有函數使用。在這一節中,我們重點向大家介紹局部變量的作用域。
在C中,如果你想要使用一個變量,你必須要事先聲明它。在聲明變量之前使用它是不合法的,比如下面的例子:
#include
int main()
{
sum = 3;
int sum;
return 0;
}
在main函數中我們聲明了一個變量sum,但是在聲明它之前我們對sum進行了賦值。問題就來了:在對sum進行賦值時,sum還沒有定義,於是編譯器並不知道sum究竟是個什麼東西。如果你在VS中嘗試編譯,編譯器會提示你sum未定義。
我們之前說過局部變量是定義在函數內的,上一節中的例子又告訴了我們變量要在定義之後才能使用。把兩者結合起來,我們就可以得到局部變量的作用域:
一般地,局部變量的作用域從定義開始,到函數的右括號結束。
以我們最熟悉的main函數為例,上面的作用域定義相當於告訴我們:在main函數中定義的局部變量,從定義的地方開始,到
如果程序中包含了語句塊,語句塊可能會影響變量的作用域。請看下面的例子:
:動手體驗:在語句塊內使用變量
#include
int main()
{
int sum = 3;
{
int sum = 6;
printf("%d\n", sum);
}
printf("%d\n", sum);
return 0;
}
我們首先在main函數中定義了一個變量sum,並將它賦值為3。隨後我們定義了一個語句塊,在語句塊內重新定義了一個變量sum,並初始化為6,同時在語句塊內輸出sum的值。猜猜看這時候輸出的sum是多少?
跳出了語句塊之後,我們再次輸出sum的值,猜猜看,這次輸出的sum又是多少?
如果在語句塊中定義了變量,那麼這個變量的作用域從定義開始,一直延伸到語句塊結束。如果它和語句塊外部的某個變量重名了,在語句塊內訪問到的將是語句塊內定義的變量。換句話說,外部的重名變量在語句塊內失效了。
在剛剛的例子中,進入main函數首先定義的變量(我們稱它為sum1)
int sum = 3;
它的作用域是從定義開始,一直到main函數結束。而在語句塊內定義的sum(我們稱它為sum2)
int sum = 6;
它的作用域是從定義開始,一直到語句塊結束。而且,由於它和語句塊外部的sum重名了,在語句塊內的printf能看到的和能使用的都將是sum2。
執行完了第一個printf之後,我們跳出了語句塊,也就離開了sum2的作用域。但是,由於我們還在main函數內,因此這個時候我們還在sum1的作用域裡,所以,第二個printf能夠訪問的sum是sum1。最後我們的輸出結果如下:
6
3
介紹完了語句和變量的作用域之後,我們就可以開始編寫一些真正的程序了!在本節中我們會向大家介紹順序結構,這是最常見的語句執行順序。讓我們從一個例子開始:
想象你是一個剛上路的出租車司機,這時來了一位乘客:“請把我送到機場。”
很不幸的是,作為一個菜鳥,你自己根本不認識去機場的路,於是你很無奈地表示自己恐怕不能送他去機場了。“哦,沒關系。”乘客說,“你出了城區,沿著高速公路一直向前開就可以了,到了出口我會告訴你的。”
C語言中的順序結構和上面的這個例子很相似。在順序結構中,程序也只有唯一的一條“路”可以走,那就是從前向後一條一條地執行程序中的語句。我們其實早就看過這樣的程序:
#include
int main()
{
printf("Hello world!\n");
return 0;
}
在這個例子中,程序從main函數開始後,按照從上往下的順序一步一步地執行printf和return兩行語句。執行完成後,整個程序也就結束了。
讀者可能會疑惑:所有的程序不都應該是這樣嗎?並不完全是這樣。正如同在路上開車會遇到岔路口一樣,在程序執行過程中也會遇到一些“岔路口”。在這些岔路口處我們需要做出判斷,決定走岔路口中的哪一條路,有的時候我們會跳過一些代碼,這就是後面要介紹的判斷結構。還有的時候,我們要在一小段代碼中來回跑,這時候我們的程序就像是北京地鐵二號線的環線一樣,在一小段程序中來回繞圈,直到找到一個合適的地方再出去,這就是循環結構。我們將在後面兩節更加詳細地介紹這兩種結構。在此之前,還是讓我們首先從最簡單的順序結構開始。
讓我們首先從基本的輸入輸出語句開始。嚴格地說,它們和順序結構沒有什麼關系,但是學會了基本輸入輸出語句可以讓我們快速地開始編寫有意思的程序,因此我們把基本輸入輸出語句放在這裡介紹,以方便讀者們快速上手。
我們首先從基本的輸出語句printf開始。printf是一個很神奇的東西,它可以向控制台輸出我們想要顯示的內容,比如最簡單的Hello world:
printf("Hello, world!");
這是我們要介紹的printf的第一種用法:直接輸出一個字符串。不要忘記用一對英文雙引號把要輸出的字符串括起來。如果你還記得字符類型中的\n,你也可以試試在Hello world後面加上一個\n:
printf("Hello, world!\n");
仔細看看,輸出的結果有什麼變化沒有?記住:字符串無非就多個連接在一起的字符類型數據,printf會將每一個字符忠實地顯示在屏幕上。
:動手體驗:用printf輸出字符串
新建一個工程,或者找到之前你的Hello world程序,嘗試下面的printf語句:
printf("我是反斜槓\\");
以及:
printf("如何打印雙引號 \"");
在運行程序之前,先猜猜看輸出的結果會是什麼樣的?
當然,如果你新建了一個工程,不要忘了加上必要的include
除了輸出一個字符串,printf還有更加靈活的用法:輸出格式控制字符串。假設我們有一個float類型的變量a,我們將它初始化為0:
float a = 0;
如果我想在屏幕上輸出a的值,應該怎麼辦呢?按照我們此前的方法,我們可以這樣:
printf("0");
好吧,這算是一種解決方法。可是,如果我們在寫程序的時候不知道a的值,卻希望在程序運行到這裡的查看a的大小,應該怎麼辦呢?
舉個例子:我正在寫一個能將千克轉成磅的小程序,由於我的同事fish恰好寫過這樣的一小段代碼。作為一個懶惰的程序員,我懶得去google這兩個單位的轉換公式,而是將fish的那幾行關鍵代碼復制了過來:
int weight = 62.0f;//以千克為單位
//從這裡開始是fish的代碼,負責把weight的值轉成以磅為單位的重量
blablabla......
blablabla......
blablabla......
//計算完成了!這個時刻weight裡面保存以磅為單位的重量,我想在屏幕上顯示weight的值:
printf(???);
這裡就是我們要介紹的printf的另一種用法:格式化輸出字符串。對於上面這個例子,我們可以用這樣的printf語句來完成:
printf("weight的值是 %f.", weight);
在這裡我們的字符串當中出現了一個新的符號%f,它表示在字符串的這個地方我們要輸出一個浮點數,浮點數的值在後面的weight變量裡面。在這裡,%f像是一個占位符,告訴printf函數:我在這裡要輸出一個浮點數了,請到後面去查找這個浮點數的值,然後用那個值來替換我!
如果你還是不太清楚%f是什麼意思,我們來看一個生活中的例子:假設我正在組織大學同學的聚會,我統計了一張表,按照當年的學號順序,挨個打電話問大家現在的家庭住址:
學號
住址
1
A1市B1小區C1室
2
A2市B2街C2號
3
4
A3市B3鎮C3鄉
5
6
很遺憾,我發現我暫時打不通3號,5號和6號同學的手機,於是我給他們發了短信:“我在統計大家的家庭住址,打不通你的電話,看到短信後請回復。”現在我的這張表就變成了這個樣子:
學號
住址
1
A1市B1小區C1室
2
A2市B2街C2號
3
等回復
4
A3市B3鎮C3鄉
5
等回復
6
等回復
很快,3號同學回復了一個“A4市B4小區C4棟”,我找到表格中3號的地址那一欄,把原來的“等回復”刪了,換成了他的回復。
類比到printf的例子當中,這裡的“等回復”就像是printf當中的%f一樣,我們並不是要在這裡顯示“%f”或者是“等回復”這三個字,而是要用別的內容來替換它們。在printf的例子中,我們是把隨後float類型的變量weight的值放在%f的位置上輸出;在同學會的例子中,我們是把同學回復的短信放在“等回復”的位置上。
類似地,我們用%d來表示輸出一個整數,%c表示輸出一個字符,%x表示用十六進制形式輸出一個整數,等等。
表 51常用printf格式字符
常用格式字符
含義
%c
輸出一個字符
%d
以十進制輸出一個整型
%x
以十六進制輸出一個整型
%f
輸出一個浮點數
%s
輸出一個字符串
需要指出的是:printf不限於每次只能輸出一個格式化字符。這就好像在同學會的例子中,我們的表格中可能有不止一個地方是“等回復”一樣。當我們想要同時輸出多個格式化字符時,我們要按照順序給出這些格式字符的值。比如下面的這個例子:
int i = 15;
float f = 15.0f;
char c = 'c';
printf("i = %d, f = %f, c = %c.", i, f, c);
顯示結果如下:
i = 15, f = 15.000000, c = c.
請讀者注意printf後面幾個變量的順序,他們和格式控制字符的順序(整型,浮點,字符)的順序必須是一致的。
解釋完了printf,用於格式化輸入的scanf就很好理解了。scanf函數用來從控制台讀入用戶的輸入信息。假設我們希望用戶輸入自己的身高:
printf("請輸入你的身高\n");
int height;
scanf("%d", &height);
printf("你的身高是%dcm.\n", height);
這個程序運行起來之後效果是這樣的:
請輸入你的身高
172
你的身高是172cm.
我們看到這裡出現了之前大家從未見過的scanf語句:
scanf("%d", &height);
scanf用來從控制台讀入用戶的輸入。和printf一樣,%d在這裡表示從控制台上要讀入的是一個整數。後面的height表示將讀入的整數保存在height變量中。和printf稍有不同的是,我們發現height前面還有一個新符號&,這個符號,加上後面跟著的height,表示我們要取得height變量在內存中的地址,這是printf函數和scanf函數不太一樣的地方。
對於新接觸計算機的讀者來說,這裡的&往往會讓人無比困惑。沒有關系,我們將在之後的函數和指針等章節中為大家詳細介紹&的含義和用法。現在讀者們只需要知道,當我們想要調用scanf的時候,我們需要告訴scanf一個變量的地址,讓scanf把讀入的輸入保存在這個地址當中。
為了方便大家理解和記憶printf和scanf的用法,我們在這裡舉一個不太准確的例子來類比一下:
在計算機市有一條內存大街,內存大街上住著很多很多變量居民,每一個變量都有自己的門牌號(變量在內存中的地址)。計算機市還有一家控制台郵局,這個郵局負責收發變量居民們的信件。
變量weight住在內存大街的196號。有一天,它給控制台郵局寫了一封信,信的內容就是它自己的名字62(變量的值)。它把信封好之後,寫上
控制台郵局 收,
內存大街196號 寄
交給了郵遞員printf。不幸的是,printf送信送到一半的時候下起大雨,信封上的寄件人地址被淋濕看不清了。可是對於printf來說,寄件人的地址重要嗎?不重要!printf只要把信件本身送到控制台郵局就可以了!從printf的觀點來看,這兩段代碼沒有什麼區別:
代碼一:
int weight = 62;
printf("我的體重是%d.", weight);
代碼二:
printf("我的體重是%d.", 62);
對於printf來說,傳進去一個變量weight後,不用關心這個變量叫什麼名字,也不關心變量住在哪裡,只關心這個變量裡面的值是多少,把變量值取出來,打印在控制台上就可以了!
然而,有一天控制台郵局收到了外地寄來的一封信:
XX用戶 寄,
內存大街224號, height 收
這個時候,控制台郵局的另一名郵遞員scanf負責把這封信交給住在內存大街224號的變量height。printf每次送信可以不關心內存大街上那些寄信人的地址,但是scanf不能!如果不知道height的具體住址,scanf就不能把這封信准確地投遞給height家裡。對於scanf來說,變量名height不重要,height裡面存的值也不重要,scanf只想知道的是自己要把寄來的信(用戶從控制台的輸入)投遞到哪一家住戶那裡(保存在內存中的什麼位置)。所以在使用scanf的時候,我們必須要用&來取出變量的地址,並把這個地址告訴scanf。
以上是對printf和scanf的一個粗糙類比,僅僅用於幫助大家理解。讀者可以耐心地等到學習後面的內容時再回想printf和scanf的調用過程,現在我們只要知道怎麼使用它們就可以了!
scanf也可以一次讀取多個變量:
int i;
float f;
char c;
scanf("%d %f %c", &i, &f, &c);
printf("i = %d, f = %f, c = %c\n", i, f, c);
在我的電腦上,運行結果如下:
17 32.5 h
i = 17, f = 32.500000, c = h
和printf一樣,scanf當中格式控制字符的順序和隨後變量的順序也必須一致。
怪獸大學是一所致力於培養驚嚇專員的大學。驚嚇專員的目的是恐嚇人類嬰兒,利用嬰兒的尖叫聲充滿電量瓶來獲取驚嚇能量。小怪獸麥克華斯基明天就要參加期末考試了。它要潛入模擬房間去恐嚇床上的機器嬰兒。機器嬰兒受到驚嚇後發出的分貝聲越高,華斯基的考試分數就越高。在考試前,主考官郝刻薄院長公布了今年的量化評分標准:
考生得分 = 嬰兒分貝 / 40 + 2.5
其中,嬰兒分貝的取值范圍為0到100間(含0和100)的整數。
郝刻薄院長希望今年的評分能夠用計算機在現場完成:只要輸入一個嬰兒的分貝數,計算機就可以返回該名考生的得分。然而怪獸大學沒有計算機系,因此郝刻薄院長希望你能夠寫一個小程序來幫助她。
程序輸入:一個0到100之間(包含0和100)的整數
程序輸出:考生的得分,用浮點數表示。
程序示例:
程序輸入:100
程序輸出:5.000000
(本題背景來自皮克斯工作室和迪斯尼合作的3D喜劇動畫片Monster University)
分析:
這個程序的結構非常清晰。我們可以把整個程序分解成三個部分:
從控制台讀入一個整數;
計算考生得分;
輸出考生的得分;
按照這個思路,我們首先寫出整體的框架。首先,我們定義兩個變量voice和score:
#include
int main()
{
int voice;
float score;
//從控制台讀入一個整數
//計算考生得分
//輸出考生的得分
return 0;
}
我們接下來一步一步來完成整個程序。首先從第一步開始:根據前面的知識,讀入一個整數可以用scanf來完成:
scanf("%d", &voice);
接下來,我們來計算考生的得分:
score = voice / 40 + 2.5;
最後,我們把score輸出到控制台上:
printf("%f", score);
我們的程序完成了!
#include
int main()
{
int voice;
float score;
//從控制台讀入一個整數
scanf("%d", &voice);
//計算考生得分
score = voice / 40 + 2.5;
//輸出考生的得分
printf("%f", score);
return 0;
}
來運行一下:
100
4.500000Press any key to continue . . .
這個結果不太對啊,輸入100的時候輸出應該是5分,怎麼會是4.5呢?我們來一步一步看:
printf會有錯嗎?不太可能,我們寫的是%f,變量名也沒有寫錯。這說明到了這一步score的值確實是4.5;
那這個4.5是怎麼計算出來的呢?回頭看看程序:
score = voice / 40 + 2.5;
score如果是4.5的話,那voice / 40的計算結果就是2,難道voice是80?可是我們輸入的確實是100。是scanf把輸入的100改成了80嗎?也不太可能,我們用了%d,也寫了&voice,scanf不太可能把值改掉。
那就只剩下最後一種可能了:100 / 40 = 2?哦,原來我們在這裡犯了錯誤:我們使用的100是一個int類型的整數,除數40也是一個整數,當兩個整數相除時,結果會自動取整(請回憶4.2.4節)!難怪我們算出來的2.5被硬生生地改成了2!
明白了錯誤在哪裡,程序就很好改了:我們只需要把100強制轉換成浮點數,浮點數除以整數的時候會按照浮點數除法來運算,這樣我們就可以得到正確的結果了:
#include
int main()
{
int voice;
float score;
//從控制台讀入一個整數
scanf("%d", &voice);
//計算考生得分
score = (float)voice / 40 + 2.5;
//輸出考生的得分
printf("%f", score);
return 0;
}
運行一下試試:
100
5.000000
OK!這次結果就對了。
盡管這是一個非常簡單的例子,它還是可以給我們帶來很多啟示的:首先,如果拿到了一個程序不知從何下手,不妨把它分解成幾個部分,一步一步來完成;第二,程序出錯了不要緊,要逐步縮小出錯的范圍,找到錯誤,改正它!
本書附錄中介紹了程序調試的技巧,讀者現在就可以對照附錄開始學習如何進行程序的調試,並且從寫自己的第一個程序開始實踐程序調試的方法。
:動手體驗:
蓋住上面的源代碼,把這個例題重新寫一遍,你能夠一次通過嗎?如果不能的話,是哪裡出錯了?利用附錄中的調試技巧找出錯誤並改正。
回到那個出租車司機的例子。當我們在路上遇到三岔路口或是十字路口時,我們需要從多種可能中選擇一條路前進。在程序中也有這樣的“岔路口”。當遇到這些岔路口時,程序的執行順序就不再是簡單地一條一條從上往下順序執行,而是會按照一定的規則來決定接下來執行哪一條語句。這就是本節的if判斷結構和下一節要介紹的switch判斷結構。它們的基本思路是:首先判斷一個條件的真假,隨後根據判斷的結果選擇接下來執行哪一部分語句。
我們首先來介紹if-else語句。讓我們從最簡單的if語句開始:
if (條件判斷)
{
...
}
if語句應該這樣翻譯:如果條件判斷的值為真,那麼就進入花括號內的語句塊去執行裡面的程序。當然,如果條件判斷的值為假,那就跳過if的這一部分,繼續執行後面的代碼。我們還是來看一個簡單的例子:
int number;
scanf("%d", &number);
if (number % 2 == 0)
{
printf("%d是個偶數\n", number);
}
讓我們來一句一句地解釋這一段代碼在干什麼:
int number;
這沒有什麼問題,我們定義了一個int類型的變量number;
scanf("%d", &number);
這個也不難,這是我們上一節學習的scanf語句,用戶從控制台上輸入一個整數,我們把它保存在number中;
if (number % 2 == 0)
這就有點意思了。回憶一下%和==都是什麼意思?%表示的是整數間的取模運算,而==用來判斷左右兩邊的值是否相等。我們把這句話翻譯成漢語那就是:number這個變量的值除以2的余數等於0。如果這句話為真了,我們就進入if語句下面的花括號:
{
printf("%d是個偶數\n", number);
}
我們在這裡打印了一行字:number代表的這個數是一個偶數。現在我們把if語句的這一塊連在一起理解,這段代碼的功能就是:如果number除以2的余數是0,我們就在屏幕上輸出:number是個偶數。
M腳下留心:==與=
初學者最容易犯的錯誤是忘記區分==和=兩個符號,尤其是在if語句當中,一不小心就會寫出下面這樣的語句:
if (number % 2 = 0)
{
...
}
再次強調,==和=的含義是完全不一樣的!==是判斷左右兩邊的值是否相等,而=是將右邊的值賦給左邊。上面的這種錯誤還可以在編譯時檢查出來,因為number % 2 是不能被賦值的。更隱蔽的錯誤是這種:
int value = number % 2; //value保存了number除以2的余數
if (value = 0)
{
...
}
你會發現,無論number是奇數還是偶數,你都進入不了if裡面去執行語句。為什麼呢?因為value=0的真值是value最終的值,而value的值已經被賦為0了,所以,它的值永遠都是假。此外,在條件判斷時,非零即為真。所以,不僅僅if(value=1)是一個永真的判斷,只要value被賦予了一個非零的值,比如if(value=2)或者if(value=10)都是永真的判斷。如果你的程序中條件判斷的執行總是和你的預期不一致,請首先檢查一下自己是不是犯了上述錯誤。
解釋完了if之後,我們接下來介紹else:
if (條件判斷)
{
...
}
else
{
...
}
不要被else嚇到,它只是補充了一下if當中條件判斷為假時執行的代碼:如果條件判斷為真,我們就進入if;如果條件判斷為假,我們就進入else。無論真假,我們總要執行,且僅執行if和else當中的一個。
還是回到奇偶數的例子上來。我們將剛剛的例子稍加改動:
int number;
scanf("%d", &number);
if (number % 2 == 0)
{
printf("%d是個偶數\n", number);
}
else
{
printf("%d是個奇數\n", number);
}
通過剛剛的介紹,你應該很容易理解這段代碼在干什麼:如果我們輸入的是一個偶數,那麼條件判斷為真,我們進入if語句塊,在屏幕上輸出這個數是偶數;否則,條件判斷為假,我們就進入else,並且在屏幕上輸出這個數是個奇數。
多學一招:
如果if或是else裡只有一條語句,那麼外面的花括號可以省略。比如剛剛的例子也可以寫成:
int number;
scanf("%d", &number);
if (number % 2 == 0)
printf("%d是個偶數\n", number);
else
printf("%d是個奇數\n", number);
當然你也可以一直把一對{}帶著,這取決於你喜歡哪種代碼風格。
If加上else只能幫我們處理二選一的情況。然而,正如同在城市裡開車還會遇到三岔路口和十字路口一樣,有的時候我們還需要能夠處理三選一,四選一乃至N選一的情況。比如在美劇《生活大爆炸》中,男主角謝耳朵有一張嚴格的日程表,精確到每星期幾應該吃什麼:
表 52謝耳朵的菜單
周一
周二
周三
周四
周五
周六
周日
燕麥粥
漢堡
奶油土豆湯
披薩
法式面包
中餐
?
謝耳朵周日吃啥是個謎,筆者沒有考證出來。我們就假定謝耳朵周日自己做飯吧。
現在,如果你要給謝耳朵寫一個日程管理程序,輸入星期幾,輸出今天要吃什麼。這是一個N選一的選擇結構。由於N選一可以被拆成很多很多個二選一,利用手頭已知的if和else語句,機智的讀者可以寫出如下的嵌套結構:
if (今天是星期一)
{
我們吃燕麥粥;
}
else
{
if (今天是星期二)
{
我們吃漢堡;
}
else
{
if (今天是星期三)
{
我們吃奶油土豆湯;
}
else
...
}
}
這個程序當然是對的,但是這樣的程序既難讀又難寫。幸好C語言中除了if和else之外還有專門為N選一設計的else if結構,它可以讓我們不必寫上面這種嵌套的if程序:
if (今天是星期一)
{
我們吃燕麥粥;
}
else if (今天是星期二)
{
我們吃漢堡;
}
else if (今天是星期三)
{
我們吃奶油土豆湯;
}
else if (今天是星期四)
{
我們吃披薩;
}
else if (今天是星期五)
{
我們吃法式面包;
}
else if (今天是星期六)
{
我們吃中餐;
}
else
{
//今天是星期天
謝耳朵要自己做飯!
}
結合這個例子,else if干了什麼應該很好懂了。首先,判斷if的條件是否成立,如果成立,意味著今天是星期一,進入if;如果不成立,我們就去看第一個else if的條件,如果第一個else if的條件為真(今天是星期二),就進入這個else if,否則就接著看第二個else if,第三個else if,直到最後一個else if。如果上面的else if統統都不成立,我們就進入最後一個else,執行“謝耳朵要自己做飯!”
關於上面的else if結構,有幾點需要留意的地方:首先,在語法上else if可以有多個(當然也可以有0個,這時候就退化成了之前的if else);其次,最後的else也是可有可無的。你可以只帶著N個else if結束這段程序。這時候如果if和所有的else if中的條件判斷統統為假,那麼程序什麼也不做。
M腳下留心:
正文中給出的都只是偽代碼,僅用於講解語法,不是可以在電腦上運行的程序!請不要試圖直接復制粘貼“謝耳朵要自己做飯”然後編譯運行程序!
:動手體驗:
回到我們為怪獸大學寫的程序上來(忘記題目設定的讀者可以查閱5.1.2節):
#include
int main()
{
int voice;
float score;
//從控制台讀入一個整數
scanf("%d", &voice);
//計算考生得分
score = (float)voice / 40 + 2.5;
//輸出考生的得分
printf("%f", score);
return 0;
}
郝刻薄院長發現一個問題:不管嬰兒的分貝數有多麼低,考生們至少都能拿到2.5分保底。正如她的名字顯示的那樣,郝刻薄院長是一個嚴厲的老師,她覺得2.5分對於那些懶惰的學生來說太寬容了,因此她決定著手修改量化評分標准:
如果嬰兒的分貝低於60,小怪獸的得分為0;
如果嬰兒的分貝不低於60而低於80,小怪獸得分為2.5分;
否則,按照原來的方法評分:得分=嬰兒分貝/40+2.5。
由於怪獸大學沒有計算機系,請你再次幫助郝刻薄院長完成這個程序。
我們之前用if和else if為謝耳朵寫了一小段管理每天菜單的代碼。作為一個Geek,謝耳朵覺得如此多的else if堆在一起還是不夠優美,因此他把你的程序改動成了這個樣子:
int day;
scanf("%d", &day);
switch(day)
{
case 1:
printf("燕麥粥\n");
break;
case 2:
printf("漢堡\n");
break;
case 3:
printf("奶油土豆湯\n");
break;
case 4:
printf("披薩\n");
break;
case 5:
printf("法式面包\n");
break;
case 6:
printf("中餐\n");
break;
default:
printf("謝耳朵要自己做飯\n");
}
在這裡謝耳朵為我們介紹了新的語句——switch語句。這是一種升級版的if else語句,專門用於處理多種分支間的選擇:
switch(表達式)
{
case 值1:
語句1;
break;
case 值2:
語句2;
break;
...
default:
語句N;
}
switch是相對比較復雜的語句,我們來詳細解釋一下:
首先,switch一開始會計算表達式的值。這個值可以是整型,可以是字符,可以是枚舉,但是,不能是浮點數float或double。不嚴謹地說,只要這個值可以被解讀成是一個整數,就可以用switch。
接下來,根據表達式的值,選擇進入合適的case。如果表達式的值恰好符合某一個case後面的值,就跳到那個case當中,開始執行case下面的語句。需要強調的是,這裡的語句1,語句2,語句N等都不一定是一行語句,它們可以是多行語句,而且不需要用花括號{}括起來。如果,表達式的值不符合任何一個case,那麼就跳轉到default,去執行default下面的語句N。正如在if-else-if當中最後的else可以沒有一樣,在switch中,最後的default也可以不寫,這時如果表達式不符合任何一個case,就直接跳過整個switch代碼,相當於什麼都不做。
最後,也是switch當中最神奇最費解的,是每一個case內部都跟了一個break。我們將在循環結構中再次見到break語句。在這裡,break語句表示當我們進入了某個case後,執行完了這個case下的語句之後,遇到了一句break,我們就跳出整個switch語句,這一段代碼結束。如果,在某一個case下面沒有break,那麼當執行完了當前的case之後,程序會接著進入下一個case繼續執行裡面的代碼,這個過程會一直持續到遇見第一個break為止。當然,如果接下來的case裡面都沒有break的話,那就一直執行到底,包括最後的default,然後跳出switch。
上面這幾段話是非常抽象的,如果你看了一遍之後就完全明白了switch的執行順序,那麼恭喜你,你很適合學習編程!如果你看得一頭霧水,沒有關系,我們還是用謝耳朵的程序來說明一下:
int day;
scanf("%d", &day);
switch(day)
{
case 1:
printf("燕麥粥\n");
break;
case 2:
printf("漢堡\n");
break;
case 3:
printf("奶油土豆湯\n");
break;
case 4:
printf("披薩\n");
break;
case 5:
printf("法式面包\n");
break;
case 6:
printf("中餐\n");
break;
default:
printf("謝耳朵要自己做飯\n");
}
今天是星期二,謝耳朵在控制台上輸入了2。這個時候,整型變量day的值就是2。現在我們進入switch:很顯然,case 2匹配上了我們的day,因此我們跳到case 2開始執行裡面的語句:
case 2:
printf("漢堡\n");
break;
我們首先執行printf:在控制台上,我們輸出了“漢堡”,這一句完成。接下來,我們遇到了一個break。按照我們剛剛的講解,遇到break之後我們就跳出整個switch語句,這一部分就結束了。
所以,當謝耳朵輸入了2之後,整個switch語句的執行效果就是:在屏幕上打印了一句
“漢堡”,然後結束。謝耳朵輸入其他case的值(1,3,4,5,6)效果是類似的。
如果今天是星期天,謝耳朵輸入了7,此時day的值是7,那麼在switch中我們挨個遍歷所有的case,發現和day的值都不符合,於是我們最後就落腳在default上,執行printf,輸出一句“謝耳朵要自己做飯”,然後switch這一段結束。
好,截止到目前為止,這個程序應該還是很清晰的。接下來我們來對謝耳朵的程序做一些小改動來說明break的作用:
int day;
scanf("%d", &day);
switch(day)
{
case 1:
printf("燕麥粥\n");
break;
case 2:
printf("漢堡\n");
case 3:
printf("奶油土豆湯\n");
case 4:
printf("披薩\n");
break;
case 5:
printf("法式面包\n");
break;
case 6:
printf("中餐\n");
default:
printf("謝耳朵要自己做飯\n");
}
如果謝耳朵輸入了1,那麼結果和之前一樣——在屏幕上打印一句“燕麥粥”,結束。
如果,謝耳朵輸入的是2,這個時候程序的結果就不一樣了——首先,我們會跳到case 2當中,在屏幕上輸出“漢堡”。執行完了這句printf之後,我們發現沒有break,於是程序會接著跳到case 3中,執行“奶油土豆湯”。接下來由於case 3中也沒有break,於是程序繼續向下執行,進入case,輸出“披薩”,終於我們這次看到了break,於是執行完了case 4之後,跳出switch,結束。所以這時如果謝耳朵輸入了2,他看到的結果將是:
2
漢堡
奶油土豆湯
披薩
:動手體驗:
加上main函數和必要的頭文件,在你的電腦上實現上一節的程序,輸入6,猜猜輸出會是什麼?輸出和你預想的一致嗎?
介紹完了順序和選擇結構之後。我們在本節向大家介紹循環結構。顧名思義,循環結構就是一遍又一遍地做重復的事情。我們還是用開車的例子來說明:在F1比賽中,10只車隊的20位車手們駕駛著各自的賽車在一條數公裡的環形賽道上風馳電掣,每場比賽中選手需要環繞賽道數十圈,最先跑完全程的車手獲得冠軍。假設在一次比賽中,車手們需要跑完50圈,利用此前學習的順序和選擇結構,我們可以用如下方式表示這個過程:
int count = 0;
if (count < 50)
{
printf ("我跑了一圈\n");
count++;//count = 1
}
if (count < 50)
{
printf ("我跑了一圈\n");
count++;//count = 2
}
...
當然,這樣手工寫50遍if語句是非常非常不可取的。幸運的是,C語言的表達能力不會如此低下,循環語句就可以幫助我們簡化上述過程。
在本節中,我們首先向大家介紹for循環,它特別適合循環次數已知的情況;隨後我們將介紹while循環和do while循環,這兩種循環都比較適合應用在循環次數未知的情況。
for循環有可能是最常見的循環了,我們以後會經常和它打交道。for循環的語法如下:
for (表達式1;條件判斷;表達式3)
{
表達式2;
}
for循環的執行過程是這樣的:
1. 首先計算表達式1;
2. 判斷條件是否為真,如果為假,跳出循環不再執行;
3. 如果條件判斷為真,進入循環,執行表達式2;
4. 執行表達式3,轉到步驟2。
我們來看一個具體的例子:
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i;
}
上述循環完成了求前100個自然數和的工作。如果你還沒有反應過來,沒有關系,我們可以把它拆開來一步一步執行:
1. 首先,i被賦值為1;
2. 條件判斷:i<=100嗎?正確,繼續執行;
3. 執行sum+=i,此時sum=1;
4. 執行i++,此時i=2;
5. 再次進行條件判斷:i<=100正確;
6. 執行sum+=i,此時sum=1+2;
7. 執行i++,此時i=3;
等等。我們跳過中間的過程,直接觀察這個循環什麼時候結束:假設某一次循環執行完i++之後i=100,我們進行條件判斷i<=100為真,開始執行sum+=i,這時sum裡面保存了1加到100的值。接下來,執行i++,i被修改為101了,此時不滿足i<=100了,因此跳出循環。最終sum中的值為前100個自然數的和。
需要強調的是,如果在表達式1中定義了新變量i,那麼這個變量只在循環內是可見的,也就是說只有在這個循環內可以使用變量i,當我們跳出了這個循環之後,程序就不認識i是什麼了。如果你在循環外使用i的話:
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i;
}
printf("%d\n", i);
你會發現編譯無法通過——你的編譯器不認識printf中的i是什麼。當然如果你事先在循環體外部聲明了i的話:
int sum = 0;
int i;
for (i = 1; i <= 100; i++)
{
sum += i;
}
printf("%d\n", i);
那printf就能夠認出需要輸出的i是一個int類型的變量。
如果循環體當中只有一條語句,那麼循環體的花括號可以省略。在上面的例子中,我們也可以把它精簡成如下形式:
int sum = 0;
for (int i = 1; i <= 100; i++)
sum += i;
當然是否省略花括號只反映了不同的編程風格,並不會影響程序的結果。讀者可以根據自己的喜好選擇適合自己的編程風格。
M腳下留心:為什麼我的循環程序不對?
如果一切正常的話,上述循環的執行結果是5050。然而,假設有一個粗心的程序員把代碼敲成了這樣:
int sum = 0;
for (int i = 1; i <= 100; i++);
{
sum += i;
}
你會發現你的程序編譯無法通過了。我們查看VS的報錯,發現是sum+=i當中的i沒有定義。這說明sum+=i根本就不在循環體內——循環究竟在哪裡呢?答案是:這是一個空循環!注意我們加在for語句後面的分號事實上形成了一條什麼都不干的空語句:
for (int i = 1; i <= 100; i++)
{
;
}
{
sum += i;
}
所以整個for循環空轉了100次,跟在最後的sum+=i只執行了一遍,而且還不知道i是個什麼值,自然編譯器就報錯了。
我們之前說過for循環特別適合已知循環次數的情況。正如同上面求前100個自然數的和,我們知道循環100次就可以完成這件事。然而,在有些情況下我們事先無法知道循環次數,這時候使用for循環就不太自然(當然,並不是說for循環不可以完成這樣的工作):
:動手體驗:前多少個自然數的和大於1000?
我們已經學會了利用for循環求解前N個自然數的和,其中N是給定的整數。現在假設有某一個N滿足前N個自然數的和大於1000,但是前N-1個自然數的和小於等於1000(你可以想象成當我們求和時,加到了N的時候,和剛剛好超過了1000)。你能用for循環實現這個程序嗎?
:動手體驗:猜數字
假設Foo和Bar兩個人在玩猜數字的游戲。Foo在心中想了一個0到1000的整數(包括0和1000),Bar每次可以向Foo提問“你心裡想的數大於XXX嗎?”,Foo會給出真實的回答。假設Bar每次都采用二分的策略來提問,即每次都將Foo心中的數所在區間縮小一半。舉個例子:
Bar:這個數大於500嗎?Foo:對 ([501, 1000])
Bar:這個數大於750嗎?Foo:錯([501, 750])
Bar:這個數大於625嗎?Foo:對([626, 750])
等等。如果我們用C語言來模擬上述過程的話,循環的次數(Bar猜測的次數)和Foo心中一開始想的數字有關,我們根本無法事先得知循環的次數。
對於上面這些不知道循環次數的情況,C語言提供了比for更合適的while語句來解決這個問題:
while (條件判斷)
{
表達式;
}
while語句看上去要比for簡潔不少:首先while會進行條件判斷,如果為假就跳出循環,如果為真就進入循環體執行表達式,執行完之後繼續條件判斷,如果為假跳出,為真則進入循環體繼續執行,等等。可以看出,while循環和下面的for循環是一致的:
for (;條件判斷;)
{
表達式;
}
在這個特殊的for循環中,初始化語句為空,每次循環結束的更新語句也為空。
我們用本節開頭的第一個例子來展示while循環的用法:
int sum = 0;
int i = 1;
while (sum <= 1000)
{
sum += i;
i++;
}
printf("%d\n", i-1);
我們來順著while循環的執行過程將上述程序走一遍:
1. 初始化sum和i;
2. 判斷sum<=1000,正確;
3. sum+= i;此時sum=1;
4. i++;此時i=2;即下一次要加的數;
5. 判斷sum<=1000,正確;
6. sum+=i,此時sum=1+2;
7. i++;此時i=3;
等等。這裡跳出循環時i的值需要我們稍微推敲一下:當使得sum恰好超過1000的那個N出現時(即i=N時),此時sum保存了1到N-1的和,它是小於等於1000的,因此下一次循環開始時可以正常進入循環體。接下來sum加上了N(超過1000了),i被修改成了N+1,因此下一次條件判斷就失敗了,跳出循環。此時i中保存的是N+1的值,為了得到N我們需要輸出i-1。
do-while循環是while循環的一種變體:
do
{
表達式;
}while(條件判斷);
稍有不同的是,在do-while循環中,首先執行表達式,然後進行條件判斷,如果為真,就回頭繼續執行表達式,否則就跳出。我們可以把do-while循環改寫成如下的for循環:
for (表達式;條件判斷;)
{
表達式;
}
do-while循環不像前兩種循環那麼常見,讀者如果要使用do-while循環的話,請留心這一點:循環體中的表達式至少會被執行一次。
在循環結構的最後,我們介紹continue,break和return,它們可以大大擴展循環結構的表達能力。
continue,break和return的直接作用都是中斷當前的循環,不同的是,在此之後,continue會開始下一次循環;break會跳出循環,而return會直接跳出當前函數(我們目前的循環都是在main函數中,在main函數中執行return就相當於整個程序結束了)。我們以表達能力最豐富的for循環來舉例:continue的作用如下:
for (表達式1;條件判斷;表達式3)
{
表達式2-1;
continue;
表達式2-2;//不會被執行!
}
和正常的for循環一樣,我們首先從表達式1開始,接著進行條件判斷,如果為真,進入循環體執行表達式2-1,執行continue——這裡出現了變化:我們遇到了continue之後,會中斷本次循環(表達式2-2不被執行),跳去執行表達式3,然後接著進行條件判斷,等等。在上面的for循環中,由於continue的存在,表達式2-2不會被執行。
如果把continue換成了break,那麼執行到break之後就不僅僅是中止這一次循環了,而是直接跳出整個for循環,執行for循環之後的代碼;如果continue換成了return,那麼在main函數中程序直接結束。
可以看出不加限制地使用continue,break和return會讓一些代碼永遠不被執行到,這顯然是沒有意義的。因此一般continue,break和return都會和條件判斷語句結合使用——當滿足一定條件時,跳過循環中剩下的代碼,否則還繼續正常執行。
我們用while循環來舉一個例子:
int sum = 0;
while (true)
{
if (sum == 100)
break;
sum++;
printf("%d\n", sum);
}
這段代碼打印了前100個正整數(當然,你也可以用for循環100次來實現它),我們來單步跟蹤一下它的執行過程:
1. 初始化sum;
2. 條件判斷通過(這是個永真循環,條件判斷永遠為true);
3. 判斷sum是否為100,結果為否,因此不執行break;
4. sum++;
5. printf;
6. 進行下一次條件判斷(永真);
7. 判斷sum是否為100,繼續為否,不執行break;
8. sum++;
9. printf;
等等。直到sum等於100了,進入循環體內部之後我們的條件判斷為真了,因此執行break,直接跳出循環,不再執行隨後的sum++和printf。如果把break換成continue,那麼sum等於100時執行continue,跳過隨後的sum++和printf直接跳到條件判斷語句true那裡,開始下一次循環(當然你應該意識到在這裡把break改成continue是有風險的——這個循環停不下來了!)
無論是for循環,while循環還是do while循環,它們的內部都是可以嵌套新的循環的。最常見的例子就是多重for循環:
:動手體驗:兩重for循環
假設有人找你開發一個4人在線打牌的程序,需要你幫助他們實現一個非常簡單的自動發牌的功能:4個人打兩副撲克牌用108張,要求你用程序模擬正常的抽牌過程:
第1次:
玩家1抽一張
玩家2抽一張
玩家3抽一張
玩家4抽一張
第2次:
玩家1抽一張
玩家2抽一張
玩家3抽一張
玩家4抽一張
……
第27次:
玩家1抽一張
玩家2抽一張
玩家3抽一張
玩家4抽一張
他們給了你一個簡單的函數:
void dealCards(int playerId)
{
printf("發一張牌給玩家%d\n", playerId);
}
來體現給編號為playerId的玩家發一張牌的功能。比如,如果你在main函數中輸入:
dealCards(1);
那麼就會在屏幕上輸出:發一張牌給玩家1。
試著運行下面的程序,猜猜它會輸出什麼?
#include
void dealCards(int playerId)
{
printf("發一張牌給玩家%d\n", playerId);
}
int main()
{
for (int round = 1; round <= 27; round++)
{
printf("第%d輪\n", round);
for (int id = 1; id <= 4; id++)
{
dealCards(id);
}
}
return 0;
}
兩重for循環並不比簡單的一重for循環要復雜多少。它只是在外層的for循環執行的每一步中完整地又執行了一個新的for循環而已。在上面這個簡單的例子中,我們首先寫了一個循環27次的外循環,用來模擬發牌的每一輪。在這個循環裡,我們要模擬給四個玩家發牌的過程,直觀地說,這樣寫會比較容易理解:
for (int round = 1; round <= 27; round++)
{
printf("第%d輪\n", round);
dealCards(1);
dealCards(2);
dealCards(3);
dealCards(4);
}
在每一輪中,我們干了四件事:分別發牌給玩家1到4號。但是,如果你仔細觀察,你會發現這四句話也是一個天然的循環結構:我們只需要用一個循環來遍歷1到4這4個自然數就可以了!於是我們又有了第二重循環:
for (int round = 1; round <= 27; round++)
{
printf("第%d輪\n", round);
for (int id = 1; id <= 4; id++)
{
dealCards(id);
}
}
除了兩重for循環,你也可以在for循環裡嵌套while,或者在while循環裡嵌套for,等等。你還可以寫出更多重的for循環。不過,如果一個程序員的程序裡有連續四五重for循環的話,這段循環很有可能會非常費解,尤其是如果裡面還包含了條件判斷,continue,break等等語句的話。因此,如果你寫出了這樣的多重for循環,最好還是考慮把你的程序稍微修改一下。
在循環結構的最後,我們介紹一種古老的語句:goto語句。goto語句用來在程序內進行跳轉,它具有非常大的靈活性。我們以之前的發牌機為例:
#include
void dealCards(int playerId)
{
printf("發一張牌給玩家%d\n", playerId);
}
int main()
{
int round = 1;
start:
printf("第%d輪\n", round);
dealCards(1);
dealCards(2);
dealCards(3);
dealCards(4);
round++;
if (round <= 27)
goto start;
return 0;
}
這個程序和之前的發牌機程序輸出結果是一樣的。雖然程序中沒有任何循環結構的關鍵字,但是它實際上執行了一段循環。我們來逐一講解它和此前的for循環的不同之處:
首先,在進入main函數之後,我們定義了一個round變量,並初始化為1。這一步和for循環的初始化是一樣的。
隨後,我們定義了一個叫做start的標簽:
start:
這個標簽沒有任何實際含義,只是用來給隨後的goto語句定位。我們暫時先忽略它。
接下來程序打印了當前的輪次:
printf("第%d輪\n", round);
並給四個玩家發牌:
dealCards(1);
dealCards(2);
dealCards(3);
dealCards(4);
發牌結束後,我們把round的值自增1:
round++;
這一步是不是很像for循環中的更新語句?
接下來我們去執行一個條件判斷:
if (round <= 27)
goto start;
這句話的意思是:如果round還沒有超過27輪,我們就跳到到start標簽的位置繼續執行。也就是說,執行完goto之後,我們繼續回到:
printf("第%d輪\n", round);
開始執行,此時的round已經自增為2了,我們接著執行發牌,自增round,判斷的過程,知道round自增到了28,這個時候if判斷不通過,我們不用goto了,而是直接執行return跳出程序。
如果你把整個程序完整地走一遍,你會發現它其實在干和for循環一樣的事情,但是,使用循環語句比使用goto要好懂多了。在大多數情況下,我們強烈建議初學者謹慎使用goto語句。
C語言的語句以分號結尾,用花括號括起來的一系列語句被稱為一個語句塊。
變量分為局部變量和全局變量。變量必須先定義再使用。局部變量的作用域從定義開始到函數結束為止。在語句塊內的變量會使得語句塊之外的同名變量失效。
順序結構是最常見的程序結構。printf和scanf用來完成基本的輸入輸出功能。使用scanf的時候需要傳入變量的地址。
判斷結構包括if和switch兩種。if之後可以帶else if或else。如果遇到分支數特別多的情況請使用switch,記得在每個分支結束前加上break。
循環結構包括for循環,while循環和do while循環。for循環適合循環次數已知的情況,while循環適合循環次數未知的情況。如果要求循環必須執行至少一次,請使用do while循環。continue用於執行下一次循環,break用於跳出循環,return可以跳出整個函數。循環之內可以嵌套循環。goto語句用於在程序內進行跳轉,慎用goto語句。