表达式

Symmetry is a complexity reducing concept (co-routines include sub-routines); seek it everywhere. [^1]

目录


[TOC]

一 算术运算符

1.概念

一元运算符(只需要 1 个操作数)
+ 一元正号运算符
- 一元负号运算符

二元运算符

加法类 乘法类
+ 加法运算符 * 乘法运算符
- 减法运算符 / 除法运算符
% 求余运算符

注意:

  • int 型与 float 型混合在一起时,运算结果是 float 型。

    比如,9 + 2.5f 的值为 11.5;6.7f / 2 的值为 3.35。

  • 运算符 /:当两个操作数都是整型时,结果会向下取整。如,1 / 2 的值是 0,而不是 0.5 。

  • 运算符 %要求两个操作数都是整型

  • 把 0 作为 /% 的右操作数会导致未定义行为。

  • 当运算符 /% 用于负操作数时,其结果难以确定。

    根据 C89 的标准,如果两个操作数中有一个是负数,那么除法结果既可以向上取整也可以向下取整(例如,-9 / 7 的结果既可以是 -1 也可以是 -2);i % j 的符号与具体实现有关(例如,-9 % 7 可以是 -2 也可以是 5)。

    在 C99 中,除法的结果总是向零取整(因此,-9 / 7 的结果是 -1);i % j 的符号与 i 相同(因此,-9 % 7 的结果是 -2;我特意测试了以下,9 % -7 的值是 2,-9 % -7 的值还是 2)。

**“由实现定义”**的行为:

术语由实现定义(implementation-defined)指的是 C标准对 C语言的部分内容未加指定,并认为其细节可有“实现”来具体定义。所谓实现是指程序在特定平台上编译,链接和执行所需要的软件。因此,根据实现的不同,程序的行为可能稍微有差异。

这样做的可能很奇怪甚至危险。但是这正是 C语言的目标之一——高效,这常常意味着与硬件相匹配。

对于我们来说,我们要尽量避免编写这种由实现定义的行为的程序。如果不能做到,起码要仔细查阅手册。

2. 运算符的优先级和结合性

当表达式包含多个运算符时,其含义可能不是一目了然的。我们的解决方法是:

  • 用括号进行分组
  • 了解运算符的优先级和结合性
运算符优先级

(operator precedence)

最高优先级 + - (一元运算符)
* / %
最低优先级 + - (二元运算符)

例 1-1:

i + j * k 等价于 i + (j * k)
-i + -j 等价于 (-i) + (-j)

运算符的结合性

当表达式包含两个或更多相同优先级的运算符时,仅有运算符优先级规则是不够的。这种情况下,运算符的结合性(associativity)开始发挥作用。

如果运算符是从左向右开始结合的,那么称这种运算符是左结合的。

二元运算符即:*,/,%,+,-都是左结合的。所以:

例 1-2:

i - j - k 等价于 (i - j) - k

运算符是右结合的,如一元运算符:+,-

例 1-3:

- + i 等价于 -(+i)

3.总结

在许多语言(特别是 C 语言)中,优先级和结合性规则都是十分重要的。然而 C 语言的运算符太多了(差不多 50 种)。为了自己和他人理解代码的方便,请最好加上足够多的圆括号。

二 赋值运算符

求出表达式的值后往往需要将其存储在变量中,以便将来使用。C语言的 = (简单赋值 simple assignment)运算符可以用于此目的。为了更新已经存储在变量中的值,C语言还提供了一种复合赋值(compound assignment)。

1. 简单赋值

表达式 v = e的赋值效果是求出表达式 e 的值,然后将此值赋值给 v。

例 2-1:

i = 5;// i is now 5
j = i;// j is now 5
k = 10 * i + j;// k is now 55

如果 v 与 e 的类型不同,那么赋值运算发生时会将 e 的值转化为 v 的类型:

例 2-2:

int i;
double j;
i = 72.99f;// i is now 72
f = 136;// f is now 136.0

在很多编程语言中,赋值是语句;然而在 C语言中,赋值就像 + 那样是运算符

既然赋值是运算符,那么多个赋值语句可以串联在一起:

例 2-3:

i = j = k = m = 0;

运算符 = 是右结合的,所以,上面的语句等价于:

i = (j = (k = (m = 0)));

作用是先将 0 赋值给 m,再将 m 赋值给 k,再将 k 赋值给 j,再将 j 赋值给 i 。


! 注意

因为赋值运算符存在类型转换(本节后面会讲),串在一起赋值运算的结果可能不是预期的结果:

int i;
float j;

j = i = 33.3f;
//先将 33 赋值给 i,然后将 33.0 赋值给 j

2. 左值

赋值运算要求它的左操作数必须是左值(lvalue)。左值表示在计算机中的存储对象,而不是常量或计算的结果。左值是变量。

例 2-4:

12 = i;
i + j = 0;
-i = j;

以上三种表达式都是错误的。

3. 复合赋值

i = i + 2;
//等同于
i += 2;

上面的例子中 += 就是一种符合运算符,表示:将自身表示的数增加 2 后再赋值给自己。


与加法相似,所有赋值运算符的工作原理大体相同。

+=

-=

*=

/=

%=

注意:

  1. i *= j + ki = i * j + k 是不一样的。

  2. 使用复合赋值运算符时,注意不要交换组成运算符的两个字符的位置。如:

    i += j写成了i =+ j 后者等价于:i = (+j)

复合运算符有着和 =运算符一样的特性。它们也是右结合的,所以:

i += j += k等价于i += (j += k)

4. 自增运算符和自减运算符

++

--

“自增”(加1)和“自减”(减1)也可以通过下面的方式完成:

i = i + 1;
j = j - 1;

复合赋值运算符可以简化上面的语句:

i += 1;
j -= 1;

而 C语言 允许用 ++ 和 – 运算符将这些语句缩的更短。比如:

i++;
j--;

或者:

++i;
--j;

这两种形式的写法的意义不同的:

  • ++i (前缀(prefix)自增),意味着“立即自增 i ”

    int i = 1;
    printf("%d\n", ++i);
    printf("%d\n", i);
    //输出
    2
    2
  • i++(后缀(postfix)自增),意味着“先使用 i 的原始值,稍后再自增”。稍后是多久?C语言标准没有给出精确的时间,但是可以放心的假设 i 再下一条语句执行之前进行自增。

    int i = 1;
    printf("%d\n", i++);
    printf("%d\n", i);
    //输出
    1
    2

--运算符具有相同的特性。

后缀的 ++ 和 – 比一元的正号,负号优先级高,而且都是左结合的。

前缀的 ++ 和 – 与一元的正号,负号优先级相同,并且是右结合的。

比如:

int main(void) {

int i = 1;

printf("%d", -i++);
printf("%d", i);
}
//输出:
-1
2

5.表达式求值

部分C语言运算符表

优先级 类型名称 符号 结合性
1 (后缀)自增 ++ 左结合
(后缀)自减
2 (前缀)自增 ++ 右结合
(前缀)自减
一元正号 +
一元符号 -
3 乘法类 * / % 左结合
4 加法类 + - 左结合
5 赋值 = *= /= -= += 右结合

能理解下面这个表达式的意义,就算掌握了这一部分的表达式求值规则:

a = b += c++ - d + --e / -f

等价于:

a = ( b += ( (c++) - d + (--e) / (-f) ) )
子表达式的求值顺序

C语言没有定义子表达式的求值顺序(除了含有 逻辑与,逻辑或 或 逗号运算符的表达式(后面会讲))。

但是不管子表达式的计算顺序如何,大多数表达式都有相同的值。但是,当子表达式改变了某个操作数的值时,产生的值就可能不一致了。思考下面的例子:

a = 5;
c = (b = a + 2) + (a = 1);

第二条语句的执行结果是未定义的。对大多数编译器而言,c 的值是 6 或者 2。取决于 子表达式 b = a + 2 和 a = 1 的求值顺序。

像上例那样,在表达式中,既在某处访问变量的值,又在别处修改它的值是不可取的。

为了避免出现此类情况,我们可以将子表达式分离:

a = 5;
b = a + 2;
a = 1;
c = b - a;

执行完这些语句后,c 的值将始终是 6

除此之外,自增自减运算符也要小心使用。如下例:

i = 2;
j = i * i++;

j 有两种可能:4 或 6

我们很自然的认为结果是 4 。但是其实该语句的执行结果是未定义的。

j 的值为 6 的情况:

  1. 取出第二个操作数(i 的原始值),然后 i 自增
  2. 取出第一个操作数(i 的新值)
  3. 将取除的两个操作数相乘(2 和 3),结果是 6

“取出”变量意味着从内存中获取它们的值。变量后续变化不会影响已经取出的值,因为取出的值通常存储在 CPU 中称为寄存器的一个特殊位置。

未定义行为

未定义行为(undefined behavior): 类似上面两个例子中的语句会导致 未定义行为,这和我们前面讲的由实现定义的行为是不同的。当程序中出现未定义行为时,后果是不可预料的。不同的编译器给出的结果可能是不同的。也就是说,程序可能无法通过编译,也可能运行时崩溃,不稳定或者产生无意义的结果。换句话说,我们应该像躲避“新冠”一样避免未定义行为

参考资料:《C Primer Plus》《C语言程序设计:现代方法》

[^1]: 对称性有助于减少复杂度(协程包含例程)。对称性无处不在。Epigrams on Programming 编程警句