5. 表達式

常量和變數都可以參與加減乘除運算,例如1+1hour-1hour * 60 + minuteminute/60等。這裡的+ - * /稱為運算符(Operator),而參與運算的常量和變數稱為操作數(Operand),上面四個由運算符和操作數所組成的算式稱為表達式(Expression)

和數學上規定的一樣,hour * 60 + minute這個表達式應該先算乘再算加,也就是說運算符是有優先順序(Precedence)的,*和/是同一優先順序,+和-是同一優先順序,*和/的優先順序高於+和-。對於同一優先順序的運算從左到右計算,如果不希望按預設的優先順序計算則要加()括號(Parenthesis)。例如(3+4)*5/6應先算3+4,再算*5,再算/6。

前面講過打印語句和賦值語句,現在我們定義:在任意表達式後面加個;號也是一種語句,稱為表達式語句。例如:

hour * 60 + minute;

這是個合法的語句,但這個語句在程序中起不到任何作用,把hour的值和minute的值取出來加乘,得到的計算結果卻沒有保存,白算了一通。再比如:

int total_minute;
total_minute = hour * 60 + minute;

這個語句就很有意義,把計算結果保存在另一個變數total_minute裡。事實上等號也是一種運算符,稱為賦值運算符,賦值語句就是一種表達式語句,等號的優先順序比+和*都低,所以先算出等號右邊的結果然後才做賦值操作,整個表達式total_minute = hour * 60 + minute加個;號構成一個語句。

任何表達式都有值和類型兩個基本屬性hour * 60 + minute的值是由三個int型的操作數計算出來的,所以這個表達式的類型也是int型。同理,表達式total_minute = hour * 60 + minute的類型也是int,它的值是多少呢?C語言規定等號運算符的計算結果就是等號左邊被賦予的那個值,所以這個表達式的值和hour * 60 + minute的值相同,也和total_minute的值相同。

等號運算符還有一個和+ - * /不同的特性,如果一個表達式中出現多個等號,不是從左到右計算而是從右到左計算,例如:

int total_minute, total;
total = total_minute = hour * 60 + minute;

計算順序是先算hour * 60 + minute得到一個結果,然後算右邊的等號,就是把hour * 60 + minute的結果賦給變數total_minute,這個結果同時也是整個表達式total_minute = hour * 60 + minute的值,再算左邊的等號,即把這個值再賦給變數total。同樣優先順序的運算符是從左到右計算還是從右到左計算稱為運算符的結合性(Associativity)。+ - * /是左結合的,等號是右結合的。

現在我們總結一下到目前為止學過的語法規則:

表達式 → 標識符
表達式 → 常量
表達式 → 字元串字面值
表達式 → (表達式)
表達式 → 表達式 + 表達式
表達式 → 表達式 - 表達式
表達式 → 表達式 * 表達式
表達式 → 表達式 / 表達式
表達式 → 表達式 = 表達式
語句 → 表達式;
語句 → printf(表達式, 表達式, 表達式, ...);
變數聲明 → 類型 標識符 = Initializer, 標識符 = Initializer, ...;
(= Initializer的部分可以不寫)

注意,本書所列的語法規則都是簡化過的,是不准確的,目的是為了便于初學者理解,比如上面所列的語法規則並沒有描述運算符的優先順序和結合性。完整的C語法規則請參考[C99]的Annex A。

表達式可以是單個的常量或變數,也可以是根據以上規則組合而成的更複雜的表達式。以前我們用printf打印常量或變數的值,現在可以用printf打印更複雜的表達式的值,例如:

printf("%d:%d is %d minutes after 00:00\n", hour, minute, hour * 60 + minute);

編譯器在翻譯這條語句時,首先根據上述語法規則把這個語句解析成下圖所示的語法樹,然後再根據語法樹生成相應的指令。語法樹的末端的是一個個Token,每一步展開利用一條語法規則。

圖 2.2. 語法樹

語法樹

根據這些語法規則進一步組合可以寫出更複雜的語句,比如在一條語句中完成計算、賦值和打印功能:

printf("%d:%d is %d minutes after 00:00\n", hour, minute, total_minute = hour * 60 + minute);

理解組合(Composition)規則是理解語法規則的關鍵所在,正因為可以根據語法規則任意組合,我們才可以用簡單的常量、變數、表達式、語句搭建出任意複雜的程序,以後我們學習新的語法規則時會進一步體會到這一點。從上面的例子可以看出,表達式不宜過度組合,否則會給閲讀和調試帶來困難。

根據語法規則組合出來的表達式在語義上並不總是正確的,例如:

minute + 1 = hour;

等號左邊的表達式要求表示一個存儲位置而不是一個值,這是等號運算符和+ - * /運算符的又一個顯著不同。有的表達式既可以表示一個存儲位置也可以表示一個值,而有的表達式只能表示值,不能表示存儲位置,例如minute + 1這個表達式就不能表示存儲位置,放在等號左邊是語義錯誤。表達式所表示的存儲位置稱為左值(lvalue)(允許放在等號左邊),而以前我們所說的表達式的值也稱為右值(rvalue)(只能放在等號右邊)。上面的話換一種說法就是:有的表達式既可以做左值也可以做右值,而有的表達式只能做右值。目前我們學過的表達式中只有變數可以做左值,可以做左值的表達式還有幾種,以後會講到。

我們看一個有意思的例子,如果定義三個變數int a, b, c;,表達式a = b = c是合法的,先求b = c的值,再把這個值賦給a,而表達式(a = b) = c是不合法的,先求(a = b)的值沒問題,但(a = b)這個表達式不能再做左值了,因此放在= c的等號左邊是錯的。

關於整數除法運算有一點特殊之處:

hour = 11;
minute = 59;
printf("%d and %d hours\n", hour, minute / 60);

執行結果是11 and 0 hours,也就是說59/60得0,這是因為兩個int型操作數相除的表達式仍為int型,只能保存計算結果的整數部分,即使小數部分是0.98也要捨去。

向下取整的運算稱為Floor,用數學符號⌊⌋表示;向上取整的運算稱為Ceiling,用數學符號⌈⌉表示。例如:

⌊59/60⌋=0
⌈59/60⌉=1
⌊-59/60⌋=-1
⌈-59/60⌉=0

在C語言中整數除法取的既不是Floor也不是Ceiling,無論操作數是正是負總是把小數部分截掉,在數軸上向零的方向取整(Truncate toward Zero),或者說當操作數為正的時候相當於Floor,當操作符為負的時候相當於Ceiling。回到先前的例子,要得到更精確的結果可以這樣:

printf("%d hours and %d percent of an hour\n", hour, minute * 100 / 60);
printf("%d and %f hours\n", hour, minute / 60.0);

在第二個printf中,表達式是minute / 60.0,60.0是double型的,/運算符要求左右兩邊的操作數類型一致,而現在並不一致。C語言規定了一套隱式類型轉換規則,在這裡編譯器自動把左邊的minute也轉成double型來計算,整個表達式的值也是double型的,在格式化字元串中應該用%f轉換說明與之對應。本來編程語言作為一種形式語言要求有簡單而嚴格的規則,自動類型轉換規則不僅很複雜,而且使C語言的形式看起來也不那麼嚴格了,C語言這麼設計是為了書寫程序簡便而做的折衷,有些事情編譯器可以自動做好,程序員就不必每次都寫一堆繁瑣的轉換代碼。然而C語言的類型轉換規則非常難掌握,本書的前幾章會儘量避免類型轉換,到第 3 節 “類型轉換”再集中解決這個問題。

習題

1、假設變數xn是兩個正整數,我們知道x/n這個表達式的結果要取Floor,例如x是17,n是4,則結果是4。如果希望結果取Ceiling應該怎麼寫表達式呢?例如x是17,n是4,則結果是5;x是16,n是4,則結果是4。