【在主画面加入捷径】
       
【选择语系】
繁中 简中

[C 语言] 程序设计教学:如何使用运算符 (Operators)

【赞助商连结】

    运算符 (operator) 如同程序中的基本指令,可相互组合以达成更多复杂的功能。运算符会用符号来表示,和函式调用相异。主流语言会认为运算符和函式是相异的,但 LISP 系的语言则不对两者进行严格的区分。

    由于 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,这个程序代码清楚地说出我们的意图。

    【赞助商连结】