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

PUBLISHED ON JUN 21, 2018 — PROGRAMMING

運算子 (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;
}

或是一些以此想法所出的某些更奇怪的變化題;這種無法直觀閱讀的程式碼應盡量避免。

二元運算子 (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,這個程式碼清楚地說出我們的意圖。

comments powered by Disqus