2011年12月16日 星期五

C 語言入門 - 運算式及其真意

進一步了解運算式

  到現在為止,我們還未對運算式有更進一步的了解,只在介紹 if 以及變數時稍有提及。運算式 (Expression) 包含了零或多個運算子 (Operator),零或多對小括號,以及至少一個運算元 (Operand) 所組成。事實上它是個相當重要的概念,在 if 篇說到它是敘述的一種,而 C 語言主要由敘述所組成,由此可見一個程式碼的主體有絕大部份都是運算式。到底什麼是運算式?讓我們一起來了解它吧。這會對你理解 C 語言帶來相當大的幫助。


何謂運算子與運算元

  運算子,簡單地說就是運算方式,例如「+」、「-」等等,而運算元則是被運算的對象,通常是數字或變數,例如「3」、「-5」等等。其中由我們在程式碼中寫出固定數值的,例如「3」或「-5」,這些是寫死的,並不像變數般可以改變,這些我們稱為「常數」(constant)。

  舉個例子,3 + 5 * 7 這個運算式,包含了兩個運算子和三個運算元。運算子分別是「+」和「*」,而運算元則是「3」、「5」以及「7」。在 C 語言中,是以「*」代表乘法,「/」代表除法。其中斜線有兩種,一種是斜線「/」,一種是反斜線「\」,小心別搞混了。記法可以用分數去記。分數記作 1/2 而不是 1\2,這相當容易記。因為分母在後面,所以用斜線在直立起來後,分母才會在底下。用反斜線就會反過來前面的數字在底下,所以不對。記住反斜線不是運算子。

  在 C 語言,運算子又分成三種,一元運算子、二元運算子、以及三元運算子。它們差在哪兒?答案是,它們能,或者說,它們需要幾個運算元來做運算。例如加法會是二元運算子,因為它需要兩個運算元來計算,只有一個運算元是無法做加法運算的。負號則是一元運算子,它將一個運算元變號,從負變正,或者從正變負。三元運算子比較特別,而且只有一個,這個留待以後補充。每個運算子在工作時,會吃掉被用來作運算的運算元,消化後轉化成一個新的運算元,消化方式與結果則視運算子的運算方法而定。

  在運算子的世界中,也有一定的規矩存在。C 語言是循序的語言,無法同時進行多件事情,包括運算。即使在同一個運算式中,運算子的運算也是有順序的。吃掉一些運算元並轉化成其它運算元,是它們的工作,所以必須按照一定的規矩。而工作先後順序,會按照它們的工作以及位置,決定應該先做還是後做。

  我們可以將一個運算式,看成是一座工廠,運算子是工作人員,而運算元則是原料。我們最後的目的是把原料按一定程序,製作成最後的成品,也就是該運算式的運算結果。優先度高的負責把小原料吃掉,轉化成小組件。優先度低的則是負責最後的大組件合成,所以必須等小組件都完成了才能開始動工。我們舉個小例子來看看。

3 + 5 * 4

  「*」先進行運算,它將原料吃掉轉成小組件,再交由「+」把整個都吃掉組合起來。整體的工作流程雖然是「*」先動作,但是實際上主宰整個工作流程的是「+」,而不是「*」。優先度低的「+」就相當於是負責全盤計劃的主管,流程上要先問過「+」,它說需要「*」來組合小組件後,才去命令「*」先進行工作。這個先後順序相當重要,也相當容易被忽略或誤會。現階段看起來一樣,因為結論總是「*」先工作。文末會提到相關例子。就因為怕造成誤會,這裡才以工廠工作先後為例,而不是進食先後。

  如果運算優先度一樣的時候怎麼辦?答案是視運算子而定。一般以數學而言,左邊優先運算,所以對於所有的「算數運算子」都是左邊優先。不過在其它類型的運算子也有右邊優先的例子,這個先放一邊不管。最常見的還是算數運算子。我們舉個例子。

3 * 4 / 2 / 3

  乘除的運算優先度相同。所以會先計算左邊的,我們加上括號會比較清楚。

(((3 * 4) / 2) / 3)

  我想應該沒有人會先將 2 除以 3,再將 4 除以 (2/3),再乘以 3 的。這個優先順序的由來是數學,所以這裡不多作說明。



括號視為絕對優先事項

  括號也是很重要的一環。它讓你可以無視運算子優先順序,確保按照你希望的順序進行。請習慣數學的人注意了,在 C 語言中只有小括號是決定運算優先順序用的,中括號與大括號各有其用途,並非是用在決定運算優先順序,這點要切記。這是程式語言,不是數學。雖然多半相同,卻是兩個不一樣的世界。即使需要多層的括號,全部都用小括號就對了。不確定哪個先運算時也可以加上小括號,確保它們的順序。


  剛剛提到了算數運算子,那麼是不是還有其它類型的運算子?沒錯。除了一元、二元等等分法之外,也可以依照其種類來分類。


算數運算子

  基本上就是算數會用到的運算子,除了基本的「+」「-」「*」「/」外,還有「%」這個特別的運算子。它也是做除法,但它的運算結果不是商,而是餘數。其它還有負號「-」也算,會吃掉一個運算元後,將其正負反轉。值得一提的只有「/」和「%」如果除數為 0 的話,會造成程式當掉。儘管它們非常想為你完成任務,但除以 0 它們不知道該怎麼辦,只好以死謝罪了。


比較運算子

  多半用於條件式中,作為比較之用。有大於「>」、小於「<」、等於「==」、大於等於「>=」、小於等於「<=」、不等於「!=」六種。比較需要注意的是,「大於等於」以及「小於等於」是不可以寫反的,例如「=<」是不行的。寫成「≦」也是不行的。被承認的只有「<=」和「>=」兩種寫法。「等於」必須是兩個等號,若只有一個的話是代表賦值的意思。它們長得很像,但是卻天差地遠。它們各有代表的符號而且不會變動,請特別注意。

  它們都是二元運算子,會吃兩個運算元來做運算。運算的結果只有「零」和「非零」兩種。C 語言中「零」代表「false」的意思,也就是錯誤。「非零」則代表「true」的意思,也就是正確。雖然 true 和 false 是以「零」和「非零」來認定,但運算子算出來的「true」與「false」的結果則多以 1 和 0 來表示,通常不會以其它非零數字表示 true。

  之前提到的 if 也是這麼認為的。嚴格來說 if 吃的並不是條件式,這只是比較好理解的說法。if 吃的是運算式,如果結果是「零」,也就是錯誤,就做 else 抓住的敘述,而保護自己抓著的敘述不被執行;如果沒有 else 就不做事。如果結果是「非零」,也就是正確,就執行自己抓著的敘述,如果有 else 就保護 else 抓住的敘述不被執行。所以以下的寫法都是正確的:

if (a)

if (3)

if (a+b*c)

  條件式其實只是運算式的一小部份,因為比較運算子也是運算子,所以也會按照上述的運算式求值的過程來計算結果。廣義的來看 if 並沒有侷限在條件式。比較運算子的運算優先度比算數運算子低,因為通常會先運算完才會進行比較,例如:

if (a+3 <= b*2)

  有些人可能還沒將思考轉換到程式上,還停留在數學上,可能就不把比較運算子當運算子,而寫出以下的條件式:

if (10 <= a < 20)

  這個我們不能說它是錯的,因為它在語法上沒有錯誤,而且可以正常執行,只是結果可能和預期不符。為什麼呢?通常這樣寫是希望 a 在 10 到 19 之間時可以執行某些敘述,但是這是數學的看法。在 C 語言中它會被作為運算式來解讀,結果會變成:

if ((10 <= a) < 20)

  如果 10 <= a 則運算結果會是「非零」,通常是 1。如果不成立則結果為「零」。也就是說不論 a 的值為何,幾乎算出來都是 0 和 1。不論怎樣都會比 20 來得小,所以可以說永遠是成立的,也就是結果永遠是 true。這顯然不太符合我們預期中的,a 要在 10 到 19 之間。不過多數人都是從小接觸數學,而幾乎沒接觸過程式,產生誤會也是相當正常的事。所以才會在此特別提及。

  你可能會想,如果我們希望 10 <= a < 20 卻又不能直接這樣寫,該怎麼辦比較好呢?沒關係,除了巢狀的 if 之外還有其它方法。


邏輯運算子

  邏輯運算子有三種,代表「否定」的「!」,代表「或者」的「||」,代表「而且」的「&&」,也就是 Not、And、Or 三種。運算結果通常也都是 0 或 1 代表 false 與 true。注意如果少寫一個符號,也就是寫成「&」或是「|」的話,編譯並不會錯,在許多情形下都會正確,但是那些是不一樣的運算子,請注意這一點。這三個運算子都是視對像的 true 與 false 來做運算。

  「!」是把原先 true 的轉變為 false,也就是非零會轉成零。原先是 false 會被轉成 true,也就是零會轉成非零,通常是轉成 1。

  「&&」和「||」比較特別。「&&」是「而且」的意思,所以必須是前者為 true「而且」後者也為 true,結果才會是 true,否則會是 false。「||」則是「或者」的意思,所以只要前者為 true「或者」後者為 true,結果就會是 true,否則會是 false。結果通常也是以 1 代表 true。這個可以用簡單的表格來表示。

And:

 | T F
-+----
T| T F
F| F F


Or:

 | T F
-+----
T| T T
F| T F

  為什麼說它們特別呢?因為它們有時可以只看一個運算元就知道結果。And 只要知道左邊運算元是 false,不論右邊是什麼,結果必然是 false。同樣的 Or 只要知道左邊是 true 則不論右邊是什麼都會是 true。這種情形下它們不會去看右邊的運算子,也就是屬於它們右邊的運算式是不會被計算的,因為沒有這個必要,所以它們會偷懶。例如:

3 || b*6-6

  這個運算式中,b*6-6 並不會真的進行運算,因為光看左邊是 3 就確定這個「||」會算出 true 的結果,它們就決定偷懶,放右邊的運算子一天假。也許你覺得是否進行運算並不那麼重要,這是錯誤的。有些運算式的進行與否是會影響變數的值的,也有些具有更大的影響,比如說除以 0。如果你總認為電腦很勤勞一定會進行運算,那麼可能會出現許多預料之外的錯誤,而且不知道這點便不知道問題會出在哪兒,抓不出哪裡有錯。以下就是個例子:

a && b/a == 7

  在這個例子,這性質就顯得非常重要。我們用 And 來防止 a 為 0 的時候,仍然進行 b/a 的運算。因為 a 如果是 0,&& 的運算結果就一定是 false,所以不會對右邊 b/a==7 做運算,也就不會發生除以 0 的慘劇。有時我們也會利用它會偷懶的特性,做一些有效利用。

  就算沒有危險性,運算與否不會影響結果,也會影響程式執行效能。不論電腦運算再快,總是需要時間運算。即使一次運算只花千分之一秒,將來執行數萬數億次時,仍會有相當大的影響。善用這點有時也能提高程式的執行效率,不過那是比較進階的課題了。現階段先知道有這樣的特性就可以了。


其它運算子

  其它還有位元運算子,或一些難以分類的運算子。前面提到的 ++ 和 -- 也是運算子的一種,這算是比較特別的,因為它們的運算元僅限於單一的變數。它們會改變對象的值,所以和賦值的左邊一樣,對象必須是變數才行。

  之前也說過它們之間有微妙的不同,便是在運算的結果上有些不一樣。a-- 的運算結果,是本來的 a,而 --a 的運算結果則是之後的 a。什麼意思呢?這麼說吧。a-- 是先照張相,然後去剪頭髮。而 --a 是先去剪頭髮,再照張相。兩種情形最後頭髮都剪了,所以在運算完之後 a 的值會相同,都是減少了 1。但是交給你的運算結果,也就是相片,卻是不一樣的。

  運算的結果,指的是這個運算子把運算元吃掉後,會吐出什麼樣的結果。與運算子對運算元本身造成的影響不一定有關係。不管把 -- 寫在前面還是後面,影響都是相同的,而運算結果卻不一樣。如同上面說的,請它剪頭髮並交一張照片給我們。運算結果是照片,它可能先照完交給你,再去剪頭髮。也可能先剪完頭髮再照給你,不管怎樣對它造成的影響就是頭髮剪掉了,這點不變。不同的是傳回來的照片,也就是我們拿到的「運算結果」的「值」,是剪完前的,還是剪完後的。

  我們可以寫個程式實際執行看看:

int a;
a = 1;
if(a-- == 1)
{
    printf("11111111\n");
}
printf("%d\n", a);

  執行下去應該會輸出一堆 1,最後輸出的 a 的值會是 0。這代表照片是還沒剪頭髮的,但最後還是剪了。如果是 --a 呢?

int a;
a = 1;
if(--a == 1)
{
    printf("11111111\n");
}
printf("%d\n", a);

  執行下去不會輸出一堆 1,因為 --a 的運算結果會是 0。謹慎起見我們多測試一下。

int a;
a = 1;
if(--a == 0)
{
    printf("00000000\n");
}
printf("%d\n", a);

  這樣就會輸出一堆 0 了,因為 --a 運算結果確實是 0。最後 a 的值也是 0。這代表照片上已經剪完頭髮了,最後也確實是剪了。

  ++ 的部份,和 -- 是完全一樣的模式,只是從減 1 變成加 1 的差別。但是請注意,同一個運算式可以有很多變數做 ++ 和 --,可是在同一個運算式對同一個變數做多次 ++ 和 -- 的動作,屬於 C 語言中未定義的行為,也就是說 C 語言的規格上並沒有對此行為的結果做出規範,編譯器不論怎麼處理都不算違反規格,所以難以預測結果,不同編譯器也可能編出不同的結果。我們應該避免讓它們做出未定義的行為。

  另一個常見卻還沒提到的運算子,就是賦值運算子「=」了。它能將右邊的運算式的結果,存放到左邊的變數之中。這是它的影響,那麼它的運算結果呢?答案是,被改變值之後的左邊變數。我們舉個例子:

int a;
a = 0;
if((a = 6) == 6)
{
    printf("666666666\n");
}
printf("%d\n", a);

  執行結果會是輸出一堆 6,第二行輸出 a 的值也會是 6。賦值會將 a 的值變為 6,而且將這個值作為運算結果。

  同時這個運算子,也是少數右邊優先運算的例子,與算數運算子的順序相反。例如以下例子:

int a, b, c;
b = 5;
c = 6;
a = b = c = 7;
printf("%d %d %d\n", a, b, c);

  最後會輸出 7 7 7。如果它是左邊優先,那麼一開始 a 的值就會變成 5,之後會變成將 c 的值賦予 a,再將 7 賦予 a,最後 a 會變成 7,但其它不變。如果是右邊優先,那麼 c 會先變成 7,然後 b 也變成 7,最後 a 變成 7,符合我們得到的結果。我們試試如果讓它左邊優先會怎麼樣。

int a, b, c;
b = 5;
c = 6;
((a = b) = c) = 7;
printf("%d %d %d\n", a, b, c);

  最後輸出 7 5 6,與我們前面推測的也完全符合。

  最後介紹一些賦值的夥伴,其它運算子暫時不會用上,之後再介紹。這些運算子分別是「+=」、「-=」、「*=」、「/=」、「%=」。它們是類似捷徑的運算子。還記得如何將 a 的值減 1 嗎?

a = a-1;

  改變變數自身的數值其實相當常用,所以誕生了這些運算子來幫助我們。要將 a 的值減 1 也就可以寫成這樣:

a -= 1;

  其它可以依此類推,比如說要將 b 的值除以 8,可以寫作:

b /= 8;

  這會相當於

b = b/8;

  不過在運算上,「/=」會稍快一些。這些運算子相當的方便,寫起來也比較直觀好懂,所以好好和它們相處吧。

  至於前面為什麼說到運算順序重要,主要就是和它們偷懶有關。「&&」的運算優先度高於「||」,不過以下的例子卻會說明一切。我們利用 ++ 或 -- 來看看變數有沒有去剪頭髮,就知道實際上有沒有偷懶了。

int a, b, c;
a = 0;
b = 0;
c = 0;
if (++a || ++b && ++c)
{
    printf("YEAH I'm working!\n");
}
printf("%d %d %d\n", a, b, c);

  輸出的結果會是 1 0 0。如果我們這樣解釋:運算優先度高的先算,那麼「&&」會先計算,這時 b 會是 1,而且 && 會需要計算右邊來判斷結果,所以 b 也會是 1。之後在 || 的時候 a 也會變成 1。這樣一來應該要輸出三個 1。

  實際上卻不是這樣,而是 || 看到 ++a 的值是 1,所以告訴 && 說你們可以偷懶不用算了,所以 ++b 和 ++c 都沒執行,b 和 c 也就自然是 0 了。事實告訴我們優先度低的會先被考慮,在需要時才會讓底下優先度較高的運算式進行計算。也就是說,階層較高的 || 先被考慮了,然後它判斷右邊不用計算,所以沒有讓 && 工作。所以並不是優先度高的 && 先進行工作,再輪到 || 進行工作,而是由於 || 需要,才讓 && 進行工作的。如果 || 不需要的話,那麼 && 根本不會也不能進行工作。

  總結來說,當不是很確定運算優先順序時,除了查詢網路資料、自行設計各種嚴謹實驗來測試以外,一律加上括號也是一個好方法。關於 && 和 || 的偷懶則必須非常小心運用,如果同一個運算式有不只一個 && 和 || 出現的話,最好避免掉偷懶與否會導致結果不同的寫法比較好。

  學到這裡應該能夠寫一些較複雜的運算,if 也能夠使用更複雜的條件了。比如說輸入一個女僕的評分,將其依分數範圍給個評價,簡單點如 S、A、B、C、D 等等,複雜點像是「辛苦了」「有你在真好」「最喜歡你了」之類的也都可以。當然還是避免中文為妙。甚至弄個海龍公式的計算或是寫數學作業啦化學啦物理作業時,覺得這計算煩死人了,數字又醜得要命,也可以寫個程式計算。

沒有留言:

張貼留言