1. 程式人生 > 實用技巧 >C語言有大約40個運算子,最常用的有這些

C語言有大約40個運算子,最常用的有這些

C語言有大約40個運算子,最常用的有這些

5.2 基本運算子

C用運算子(operator)表示算術運算。例如,+運算子使在它兩側的值加在一起。如果你覺得術語“運算子”很奇怪,那麼請記住東西總得有個名稱。與其叫“那些東西”或“運算處理符”,還不如叫“運算子”。現在,我們介紹一下用於基本算術運算的運算子:=、+、-、*和/(C沒有指數運算子。不過,C的標準數學庫提供了一個pow()函式用於指數運算。例如,pow(3.5, 2.2)返回3.5的2.2次冪)。

5.2.1 賦值運算子:=

在C語言中,=並不意味著“相等”,而是一個賦值運算子。下面的賦值表示式語句:

bmw = 2002;

把值2002賦給變數bmw。也就是說,=號左側是一個變數名,右側是賦給該變數的值。符號=被稱為賦值運算子。另外,上面的語句不讀作“bmw等於2002”,而讀作“把值2002賦給變數bmw”。賦值行為從右往左進行。

也許變數名和變數值的區別看上去微乎其微,但是,考慮下面這條常用的語句:

i = i + 1;

對數學而言,這完全行不通。如果給一個有限的數加上1,它不可能“等於”原來的數。但是,在計算機賦值表示式語句中,這很合理。該語句的意思是:找出變數i的值,把該值加1,然後把新值賦值變數i(見圖5.1)。

圖5.1 語句i = i + 1;

在C語言中,類似這樣的語句沒有意義(實際上是無效的):

2002 = bmw;

因為在這種情況下,2002被稱為右值(rvalue),只能是字面常量,不能給常量賦值,常量本身就是它的值。因此,在編寫程式碼時要記住,=號左側的項必須是一個變數名。實際上,賦值運算子左側必須引用一個儲存位置。最簡單的方法就是使用變數名。不過,後面章節還會介紹“指標”,可用於指向一個儲存位置。概括地說,C使用可修改的左值(modifiable lvalue)標記那些可賦值的實體。也許“可修改的左值”不太好懂,我們再來看一些定義。

幾個術語:資料物件、左值、右值和運算子

賦值表示式語句的目的是把值儲存到記憶體位置上。用於儲存值的資料儲存區域統稱為資料物件(data object)。C標準只有在提到這個概念時才會用到物件這個術語。使用變數名是標識物件的一種方法。除此之外,還有其他方法,但是要在後面的章節中才學到。例如,可以指定陣列的元素、結構的成員,或者使用指標表示式(指標中儲存的是它所指向物件的地址)。左值(lvalue)是C語言的術語,用於標識特定資料物件的名稱或表示式。因此,物件指的是實際的資料儲存,而左值是用於標識或定位儲存位置的標籤。

對於早期的C語言,提到左值意味著:

1.它指定一個物件,可以引用記憶體中的地址;

2.它可用在賦值運算子的左側,左值(lvalue)中的l源自left。

但是後來,標準中新增了const限定符。用const建立的變數不可修改。因此,const識別符號滿足上面的第1項,但是不滿足第2項。一方面C繼續把標識物件的表示式定義為左值,一方面某些左值卻不能放在賦值運算子的左側。

為此,C標準新增了一個術語:可修改的左值(modifiable lvalue),用於標識可修改的物件。所以,賦值運算子的左側應該是可修改的左值。當前標準建議,使用術語物件定位值(object locator value)更好。

右值(rvalue)指的是能賦值給可修改左值的量,且本身不是左值。例如,考慮下面的語句:

1 bmw = 2002;

這裡,bmw是可修改的左值,2002是右值。讀者也許猜到了,右值中的r源自right。右值可以是常量、變數或其他可求值的表示式(如,函式呼叫)。實際上,當前標準在描述這一概念時使用的是表示式的值(value of an expression),而不是右值。

我們看幾個簡單的示例:

1 int ex;
2 int why;
3 int zee;
4 const int TWO = 2;
5 why = 42;
6 zee = why;
7 ex = TWO * (why + zee);

這裡,ex、why和zee都是可修改的左值(或物件定位值),它們可用於賦值運算子的左側和右側。TWO是不可改變的左值,它只能用於賦值運算子的右側(在該例中,TWO被初始化為2,這裡的=運算子表示初始化而不是賦值,因此並未違反規則)。同時,42是右值,它不能引用某指定記憶體位置。另外,why和zee是可修改的左值,表示式(why + zee)是右值,該表示式不能表示特定記憶體位置,而且也不能給它賦值。它只是程式計算的一個臨時值,在計算完畢後便會被丟棄。

在學習名稱時,被稱為“項”(如,賦值運算子左側的項)的就是運算物件(operand)。運算物件是運算子操作的物件。例如,可以把“吃漢堡”描述為:“吃”(運算子)操作“漢堡”(運算物件)。類似地可以說,=運算子的左側運算物件應該是可修改的左值。

C的基本賦值運算子有些與眾不同,請看程式清單5.3。

程式清單5.3 golf.c程式

 1 /* golf.c -- 高爾夫錦標賽記分卡 */
 2 #include <stdio.h>
 3 int main(void)
 4 {
 5      int jane, tarzan, cheeta;
 6 
 7      cheeta = tarzan = jane = 68;
 8      printf("                  cheeta   tarzan    jane\n");
 9      printf("First round score %4d %8d %8d\n", cheeta, tarzan, jane);
10 
11      return 0;
12 }

許多其他語言都會迴避該程式中的三重賦值,但是C完全沒問題。賦值的順序是從右往左:首先把68賦給jane,然後再賦給tarzan,最後賦給cheeta。因此,程式的輸出如下:

1                   cheeta    tarzan      jane
2 First round score   68         68         68

5.2.2 加法運算子:+

加法運算子(addition operator)用於加法運算,使其兩側的值相加。例如,語句:

printf("%d", 4 + 20);

列印的是24,而不是表示式

4 + 20

相加的值(運算物件)可以是變數,也可以是常量。因此,執行下面的語句:

income = salary + bribes;

計算機會檢視加法運算子右側的兩個變數,把它們相加,然後把和賦給變數income。

在此提醒讀者注意,income、salary和bribes都是可修改的左值。因為每個變數都標識了一個可被賦值的資料物件。但是,表示式salary + bribes是一個右值。

5.2.3 減法運算子:-

減法運算子(subtraction operator)用於減法運算,使其左側的數減去右側的數。例如,下面的語句把200.0賦給takehome:

takehome = 224.0024.00;

+和-運算子都被稱為二元運算子(binary operator),即這些運算子需要兩個運算物件才能完成操作。

5.2.4 符號運算子:-和+

減號還可用於標明或改變一個值的代數符號。例如,執行下面的語句後,smokey的值為12:

1 rocky = –12;
2 smokey = –rocky;

以這種方式使用的負號被稱為一元運算子(unary operator)。一元運算子只需要一個運算物件(見圖5.2)。

圖5.2 一元和二元運算子

C90標準新增了一元+運算子,它不會改變運算物件的值或符號,只能這樣使用:

dozen = +12;

編譯器不會報錯。但是在以前,這樣做是不允許的。

5.2.5 乘法運算子:*

符號*表示乘法。下面的語句用2.54乘以inch,並將結果賦給cm:

cm = 2.54 * inch;

C沒有平方函式,如果要列印一個平方表,怎麼辦?如程式清單5.4所示,可以使用乘法來計算平方。

程式清單5.4 squares.c程式

/* squares.c -- 計算1~20的平方 */
#include <stdio.h>
int main(void)
{
     int num = 1;

     while (num < 21)
     {
          printf("%4d %6d\n", num, num * num);
          num = num + 1;
     }

     return 0;
}

該程式列印數字1~20及其平方。接下來,我們再看一個更有趣的例子。

1.指數增長

讀者可能聽過這樣一個故事,一位強大的統治者想獎勵做出突出貢獻的學者。他問這位學者想要什麼,學者指著棋盤說,在第1個方格里放1粒小麥、第2個方格里放2粒小麥、第3個方格里放4粒小麥,第4個方格里放8粒小麥,以此類推。這位統治者不熟悉數學,很驚訝學者竟然提出如此謙虛的要求。因為他原本準備獎勵給學者一大筆財產。如果程式清單5.5執行的結果正確,這顯然是跟統治者開了一個玩笑。程式計算出每個方格應放多少小麥,並計算了總數。可能大多數人對小麥的產量不熟悉,該程式以穀粒數為單位,把計算的小麥總數與粗略估計的世界小麥年產量進行了比較。

程式清單5.5 wheat.c程式

 1 /* wheat.c -- 指數增長 */
 2 #include <stdio.h>
 3 #define SQUARES 64             // 棋盤中的方格數
 4 int main(void)
 5 {
 6      const double CROP = 2E16;  // 世界小麥年產穀粒數
 7      double current, total;
 8      int count = 1;
 9 
10      printf("square     grains       total     ");
11      printf("fraction of \n");
12      printf("           added        grains    ");
13      printf("world total\n");
14      total = current = 1.0;        /* 從1顆穀粒開始   */
15      printf("%4d %13.2e %12.2e %12.2e\n", count, current,
16                total, total / CROP);
17      while (count < SQUARES)
18      {
19           count = count + 1;
20           current = 2.0 * current;    /* 下一個方格穀粒翻倍 */
21           total = total + current;    /* 更新總數 */
22           printf("%4d %13.2e %12.2e %12.2e\n", count, current,
23                     total, total / CROP);
24      }
25      printf("That's all.\n");
26 
27      return 0;
28 }

程式的輸出結果如下:

square        grains       total        fraction of
              added        grains       world total
    1         1.00e+00     1.00e+00     5.00e-17
    2         2.00e+00     3.00e+00     1.50e-16
    3         4.00e+00     7.00e+00     3.50e-16
    4         8.00e+00     1.50e+01     7.50e-16
    5         1.60e+01     3.10e+01     1.55e-15
    6         3.20e+01     6.30e+01     3.15e-15
    7         6.40e+01     1.27e+02     6.35e-15
    8         1.28e+02     2.55e+02     1.27e-14
    9         2.56e+02     5.11e+02     2.55e-14
   10         5.12e+02     1.02e+03     5.12e-14

10個方格以後,該學者得到的小麥僅超過了1000粒。但是,看看55個方格的小麥數是多少:

55         1.80e+16     3.60e+16     1.80e+00

總量已超過了世界年產量!不妨自己動手執行該程式,看看第64個方格有多少小麥。

這個程式示例演示了指數增長的現象。世界人口增長和我們使用的能源都遵循相同的模式。

5.2.6 除法運算子:/

C使用符號/來表示除法。/左側的值是被除數,右側的值是除數。例如,下面four的值是4.0:

four = 12.0/3.0;

整數除法和浮點數除法不同。浮點數除法的結果是浮點數,而整數除法的結果是整數。整數是沒有小數部分的數。這使得5除以3很讓人頭痛,因為實際結果有小數部分。在C語言中,整數除法結果的小數部分被丟棄,這一過程被稱為截斷(truncation)。

執行程式清單5.6中的程式,看看截斷的情況,體會整數除法和浮點數除法的區別。

程式清單5.6 divide.c程式

 1 /* divide.c -- 演示除法 */
 2 #include <stdio.h>
 3 int main(void)
 4 {
 5      printf("integer division:  5/4   is %d \n", 5 / 4);
 6      printf("integer division:  6/3   is %d \n", 6 / 3);
 7      printf("integer division:  7/4   is %d \n", 7 / 4);
 8      printf("floating division: 7./4. is %1.2f \n", 7. / 4.);
 9      printf("mixed division:    7./4  is %1.2f \n", 7. / 4);
10 
11      return 0;
12 }

程式清單5.6中包含一個“混合型別”的示例,即浮點值除以整型值。C相對其他一些語言而言,在型別管理上比較寬容。儘管如此,一般情況下還是要避免使用混合型別。該程式的輸出如下:

1 integer division:  5/4   is 1
2 integer division:  6/3   is 2
3 integer division:  7/4   is 1
4 floating division: 7./4. is 1.75
5 mixed division:    7./4  is 1.75

注意,整數除法會截斷計算結果的小數部分(丟棄整個小數部分),不會四捨五入結果。混合整數和浮點數計算的結果是浮點數。實際上,計算機不能真正用浮點數除以整數,編譯器會把兩個運算物件轉換成相同的型別。本例中,在進行除法運算前,整數會被轉換成浮點數。

C99標準以前,C語言給語言的實現者留有一些空間,讓他們來決定如何進行負數的整數除法。一種方法是,舍入過程採用小於或等於浮點數的最大整數。當然,對於3.8而言,處理後的3符合這一描述。但是-3.8會怎樣?該方法建議四捨五入為-4,因為-4小於-3.8。但是,另一種舍入方法是直接丟棄小數部分。這種方法被稱為“趨零截斷”,即把-3.8轉換成-3。在C99以前,不同的實現採用不同的方法。但是C99規定使用趨零截斷。所以,應把-3.8轉換成-3。

5.2.7 運算子優先順序

考慮下面的程式碼:

butter = 25.0 + 60.0 * n / SCALE;

這條語句中有加法、乘法和除法運算。先算哪一個?是25.0加上60.0,然後把計算的和85.0乘以n,再把結果除以SCALE?還是60.0乘以n,然後把計算的結果加上25.0,最後再把結果除以SCALE?還是其他運算順序?假設n是6.0,SCALE是2.0,帶入語句中計算會發現,第1種順序得到的結果是255,第2種順序得到的結果是192.5。C程式一定是採用了其他的運算順序,因為程式執行該語句後,butter的值是205.0。

顯然,執行各種操作的順序很重要。C語言對此有明確的規定,通過運算子優先順序來解決操作順序的問題。每個運算子都有自己的優先順序。正如普通的算術運算那樣,乘法和除法的優先順序比加法和減法高,所以先執行乘法和除法。如果兩個運算子的優先順序相同怎麼辦?如果它們處理同一個運算物件,則根據它們在語句中出現的順序來執行。對大多數運算子而言,這種情況都是按從左到右的順序進行(=運算子除外)。因此,語句:

butter = 25.0 + 60.0 * n / SCALE;

的運算順序是:

1 60.0 * n            首先計算表示式中的*或/(假設n的值是6,所以60.0*n得360.02 360.0 / SCALE       然後計算表示式中第2個*或/
3 25.0 + 180          最後計算表示式裡第1個+或-,結果為205.0(假設SCALE的值是2.0

許多人喜歡用表示式樹(expression tree)來表示求值的順序,如圖5.3所示。該圖演示瞭如何從最初的表示式逐步簡化為一個值。

圖5.3 用表示式樹演示運算子、運算物件和求值順序

如何讓加法運算在除法運算之前執行?可以這樣做:

flour = (25.0 + 60.0 * n) / SCALE;

最先執行圓括號中的部分。圓括號內部按正常的規則執行。該例中,先執行乘法運算,再執行加法運算。執行完圓括號內的表示式後,用運算結果除以SCALE。

表5.1總結了到目前為止學過的運算子優先順序。

表5.1 運算子優先順序(從高至低)

注意正號(加號)和負號(減號)的兩種不同用法。結合律欄列出了運算子如何與運算物件結合。例如,一元負號與它右側的量相結合,在除法中用除號左側的運算物件除以右側的運算物件。

5.2.8 優先順序和求值順序

運算子優先順序為表示式中的求值順序提供重要的依據,但是並沒有規定所有的順序。C給語言的實現者留出選擇的餘地。考慮下面的語句:

y = 6 * 12 + 5 * 20;

當運算子共享一個運算物件時,優先順序決定了求值順序。例如上面的語句中,12是*和+運算子的運算物件。根據運算子的優先順序,乘法的優先順序比加法高,所以先進行乘法運算。類似地,先對5進行乘法運算而不是加法運算。簡而言之,先進行兩個乘法運算6*12和5*20,再進行加法運算。但是,優先順序並未規定到底先進行哪一個乘法。C語言把主動權留給語言的實現者,根據不同的硬體來決定先計算前者還是後者。可能在一種硬體上採用某種方案效率更高,而在另一種硬體上採用另一種方案效率更高。無論採用哪種方案,表示式都會簡化為72 + 100,所以這並不影響最終的結果。但是,讀者可能會根據乘法從左往右的結合律,認為應該先執行+運算子左邊的乘法。結合律只適用於共享同一運算物件的運算子。例如,在表示式12 / 3*2中,/和*運算子的優先順序相同,共享運算物件3。因此,從左往右的結合律在這種情況起作用。表示式簡化為4*2,即8(如果從右往左計算,會得到12/6,即2,這種情況下計算的先後順序會影響最終的計算結果)。在該例中,兩個*運算子並沒有共享同一個運算物件,因此從左往右的結合律不適用於這種情況。

學以致用

接下來,我們在更復雜的示例中使用以上規則,請看程式清單5.7。

程式清單5.7 rules.c程式

 1 /* rules.c -- 優先順序測試 */
 2 #include <stdio.h>
 3 int main(void)
 4 {
 5      int top, score;
 6 
 7      top = score = -(2 + 5) * 6 + (4 + 3 * (2 + 3));
 8      printf("top = %d, score = %d\n", top, score);
 9 
10      return 0;
11 }

該程式會列印什麼值?先根據程式碼推測一下,再執行程式或閱讀下面的分析來檢查你的答案。

首先,圓括號的優先順序最高。先計算-(2 + 5)*6中的圓括號部分,還是先計算(4 + 3*(2 + 3))中的圓括號部分取決於具體的實現。圓括號的最高優先順序意味著,在子表示式-(2 + 5)*6中,先計算(2 + 5)的值,得7。然後,把一元負號應用在7上,得-7。現在,表示式是:

top = score = -7 * 6 + (4 + 3 * (2 + 3))

下一步,計算2 + 3的值。表示式變成:

top = score = -7 * 6 + (4 + 3 * 5)

接下來,因為圓括號中的*比+優先順序高,所以表示式變成:

top = score = -7 * 6 + (4 + 15)

然後,表示式為:

top = score = -7 * 6 + 19

-7乘以6後,得到下面的表示式:

top = score = -42 + 19

然後進行加法運算,得到:

top = score = -23

現在,-23被賦值給score,最終top的值也是-23。記住,=運算子的結合律是從右往左。

本文摘自《C Primer Plus(第6版)中文版》

[美] 史蒂芬·普拉達(Stephen Prata) 著,姜佑 譯