C 語言程式設計教學:運算子 (Operators)

PUBLISHED ON JUN 21, 2018 — PROGRAMMING
FacebookTwitter LinkedIn LINE Skype EverNote GMail Email Email

    運算子 (operator) 如同程式中的基本指令,可相互組合以達成更多複雜的功能。C 語言有以下的運算子:

    • 代數運算子 (Arithmetic Operators)
    • 二元運算子 (Bitwise Operators)
    • 關係運算子 (Relational Operators)
    • 邏輯運算子 (Logical Operators)
    • 指派運算子 (Assignment Operators)
    • 其他運算子

    由於 C 語言沒有運算子重載 (operator overloading),我們無法對自訂型別使用內建的運算子。以下 C 虛擬碼實際上無法運作:

    // It won't work.
    
    // t, u, v are pointer to Vector.
    t = u + v;
    

    對於自訂型別,需將運算子轉成函式呼叫,如下例:

    // t, u, v are pointer to Vector.
    t = vector_add(u, v);
    

    代數運算子 (Arithmetic Operators)

    代數運算子用於在電腦程式中進行代數運算。包括以下二元 (binary) 運算子:

    • a + b:相加
    • a - b:相減
    • a * b:相乘
    • a / b:相除
    • a % b:取餘數

    實例如下:

    #include <assert.h>
    
    int main()
    {
        assert(4 + 3 == 7);
        assert(4 - 3 == 1);
        assert(4 * 3 == 12);
        assert(4 / 3 == 1);
        assert(4 % 3 == 1);
    
        return 0;
    }
    

    還有以下一元運算子:

    • +a:取正數
    • -a:取負數
    • ++:遞增
    • --:遞減

    取正/負數的例子如下:

    #include <assert.h>
    
    int main(void)
    {
        int a = 3;
        assert(+a == 3);
        assert(-a == -3);
    
        return 0;
    }
    

    遞增/減運算子常用在迴圈中,有分前綴 (prefix) 和後綴 (postfix) 兩種,兩者效果略有不同。如以下實例:

    #include <assert.h>
    
    int main(void) {
        int a = 3;
        assert(++a == 4);
        
        a = 3;
        assert(a++ == 3);
    
        return 0;
    }
    

    以本例來說,++a 會將 a 遞增 1 後取值,故 a 為 4;但 a++ 會先取值後再遞增 1,故在 assert(a++ == 3) 當下 a 為 3,但之後 a 為 4。

    遞增/減運算子的豆知識也會成為某些考試的項目,所以就會看到一些很沒營養的代碼:

    #include <assert.h>
    
    int main(void)
    {
        int a = 3;
        int b = 4;
        
        // DON'T DO THIS IN PRODUCTION CODE.
        int c = a++ + ++b;
        
        assert(a == 4);
        assert(b == 5);
        assert(c == 8);
    
        return 0;
    }
    

    為什麼這種題目會沒營養呢?因為這是未定義行為 (undefined behavior),在不同編譯器上跑出來的結果可能不一樣。也有可能會出現一些基於此想法所出的變化題;這種無法直觀閱讀的程式碼應盡量避免。

    二元運算子 (Bitwise Operators)

    二元運算子是指兩數間進行二進位運算,包括以下運算子:

    • a & b:bitwise and
    • a | b:bitwise or
    • a ^ b:bitwise xor
    • ~a:complement number
    • a << n:left shift
    • a >> n:right shift

    以下是實例:

    #include <assert.h>
    
    int main(void) {
        int a = 60;  // 60 == 0011 1100
        int b = 13;  // 13 == 0000 1101
        
        assert((a & b) == 12);    //  12 == 0000 1100
        assert((a | b) == 61);    //  61 == 0011 1101
        assert((a ^ b) == 49);    //  49 == 0011 0001
        assert((~a) == -61);      // -61 == 1100 0011
        assert((a << 2) == 240);  // 240 == 1111 0000
        assert((a >> 2) == 15);   //  15 == 0000 1111
    
        return 0;
    }
    

    一般來說,二元運算多用在低階程式設計 (low-level programming),初學時不太會用到。

    關係運算子 (Relational Operators)

    關係運算子用來表示兩個資料間的大小,C 語言有以下關係運算子:

    • a == b:相等
    • a != b:不等
    • a > b:大於
    • a >= b:大於等於
    • a < b:小於
    • a <= b:小於等於

    以下是實例:

    #include <assert.h>
    
    int main(void) {
        assert(3 + 4 == 7);
        assert(3 + 4 != 5);
        assert(3 + 4 > 5);
        assert(3 + 4 >= 5);
        assert(3 + 4 < 10);
        assert(3 + 4 <= 10);
    
        return 0;
    }
    

    邏輯運算子 (Logical Operators)

    邏輯運算子可進行邏輯運算,實際上用來結合複合的條件:

    • a && b:且 (and)
    • a || b:或 (or)
    • !a:否 (not)

    有些教材會提供真值表,像是維基的相關條目。一般程式設計用到的沒那麼複雜,倒不用刻意去背誦。只要記得:

    • 且 (and):所有條件皆為真時為真
    • 或 (or):其中一項條件為真即為真
    • 否 (not):真變偽,偽變真

    以下為實例:

    #include <assert.h>
    #include <stdbool.h>
    
    int main(void) {
        // AND
        assert((true && true) == true);
        assert((true && false) == false);
        assert((false && true) == false);
        assert((false && false) == false);
        
        // OR
        assert((true || true) == true);
        assert((true || false) == true);
        assert((false || true) == true);
        assert((false || false) == false);
        
        // NOT
        assert((!true) == false);
        assert((!false) == true);
    
        return 0;
    }
    

    指派運算子 (Assignment Operators)

    指派運算子算是小小的語法糖,像是把 x = x + 1; 簡寫成 x += 1。以下是 C 可用的指派運算子:

    • n = a:直接取代
    • n += a:即 n = n + a
    • n -= a:即 n = n - a
    • n *= a:即 n = n * a
    • n /= a:即 n = n / a
    • n %= a:即 n = n % a
    • n <<= a:即 n = n << a
    • n >>= a:即 n = n >> a
    • n &= a:即 n = n & a
    • n |= a:即 n = n | a
    • n ^= a:即 n = n ^ a

    其他

    三元運算子 ... ? ... : ... 算是 if ... else ... 敘述的縮小版,好處是可寫在同一行內。以下是實例:

    #include <assert.h>
    
    int main(void) {
        int a = 5;
        int b = 3;
        int min = a < b ? a : b;
        
        assert(min == 3);
    
        return 0;
    }
    

    很多人以為 sizeof 是函式呼叫,但 sizeof 其實是運算子,可記算某個型別的大小。可用於配置記憶體時計算所需的記憶體大小:

    // Allocate a chunk of memory for i_p.
    int *i_p = malloc(sizeof(int));
    

    或是用來計算陣列的長度:

    #include <assert.h>
    #include <stddef.h>
    
    int main(void) {
        int arr[] = {1, 2, 3, 4, 5};
        
        size_t sz = sizeof(arr) / sizeof(int);
        
        assert(sz == 5);
        
        return 0;
    }
    

    註:這個方式僅對靜態配置的陣列能用。

    有關結構及指標的運算子將於後文介紹。

    運算子優先順序 (Operator Precedence)

    大部分 C 語言教材都會列出運算子的優先順序,如這個表格。但筆者不太會去刻意背誦這個表格,頂多偶爾查詢一下,因為我們可以透過以下方式來處理優先順序:

    • 簡化同一行內的敘述
    • 若無法簡化時,使用括號更動優先順序

    像以下這個有時會在網站上看到的例子:

    void StackPush(stackT *stackP, stackElementT element)
    {
      if (StackIsFull(stackP)) {
        fprintf(stderr, "Can't push element on stack: stack is full.\n");
        exit(1);
      }
    
      /* A complex statement. */
      stackP->contents[++stackP->top] = element;
    }
    

    stackP->contents[++stackP->top] = element; 表面上看起來是一行,其實隱藏著兩行敘述:我們先將 stackP->top 遞增 1 後再對 stackP->contents[stackP->top] 賦值,這樣的程式碼其實不太直覺。我們可以將其改寫:

    stackP->top += 1;
    stackP->contents[stackP->top] = element;
    

    雖然看起來有點 verbose,這個程式碼清楚地說出我們的意圖。