06-表达式
表达式
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) |
运算符的结合性
当表达式包含两个或更多相同优先级的运算符时,仅有运算符优先级规则是不够的。这种情况下,运算符的结合性(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 |
如果 v 与 e 的类型不同,那么赋值运算发生时会将 e 的值转化为 v 的类型:
例 2-2:
int i; |
在很多编程语言中,赋值是语句;然而在 C语言中,赋值就像 + 那样是运算符。
既然赋值是运算符,那么多个赋值语句可以串联在一起:
例 2-3:
i = j = k = m = 0; |
运算符 = 是右结合的,所以,上面的语句等价于:
i = (j = (k = (m = 0))); |
作用是先将 0 赋值给 m,再将 m 赋值给 k,再将 k 赋值给 j,再将 j 赋值给 i 。
! 注意
因为赋值运算符存在类型转换(本节后面会讲),串在一起赋值运算的结果可能不是预期的结果:
int i; |
2. 左值
赋值运算要求它的左操作数必须是左值(lvalue)。左值表示在计算机中的存储对象,而不是常量或计算的结果。左值是变量。
例 2-4:
12 = i; |
以上三种表达式都是错误的。
3. 复合赋值
i = i + 2; |
上面的例子中 += 就是一种符合运算符,表示:将自身表示的数增加 2 后再赋值给自己。
与加法相似,所有赋值运算符的工作原理大体相同。
+=
-=
*=
/=
%=
注意:
-
i *= j + k
和i = i * j + k
是不一样的。 -
使用复合赋值运算符时,注意不要交换组成运算符的两个字符的位置。如:
i += j
写成了i =+ j
后者等价于:i = (+j)
复合运算符有着和 =
运算符一样的特性。它们也是右结合的,所以:
i += j += k
等价于i += (j += k)
4. 自增运算符和自减运算符
++
--
“自增”(加1)和“自减”(减1)也可以通过下面的方式完成:
i = i + 1; |
复合赋值运算符可以简化上面的语句:
i += 1; |
而 C语言 允许用 ++ 和 – 运算符将这些语句缩的更短。比如:
i++; |
或者:
++i; |
这两种形式的写法的意义不同的:
-
++i
(前缀(prefix)自增),意味着“立即自增 i ”cint i = 1;
printf("%d\n", ++i);
printf("%d\n", i);
//输出
2
2 -
i++
(后缀(postfix)自增),意味着“先使用 i 的原始值,稍后再自增”。稍后是多久?C语言标准没有给出精确的时间,但是可以放心的假设 i 再下一条语句执行之前进行自增。cint i = 1;
printf("%d\n", i++);
printf("%d\n", i);
//输出
1
2
--
运算符具有相同的特性。
后缀的 ++ 和 – 比一元的正号,负号优先级高,而且都是左结合的。
前缀的 ++ 和 – 与一元的正号,负号优先级相同,并且是右结合的。
比如:
int main(void) { |
5.表达式求值
部分C语言运算符表
优先级 | 类型名称 | 符号 | 结合性 |
---|---|---|---|
1 | (后缀)自增 | ++ | 左结合 |
(后缀)自减 | – | ||
2 | (前缀)自增 | ++ | 右结合 |
(前缀)自减 | – | ||
一元正号 | + | ||
一元符号 | - | ||
3 | 乘法类 | * / % |
左结合 |
4 | 加法类 | + - |
左结合 |
5 | 赋值 | = *= /= -= += |
右结合 |
能理解下面这个表达式的意义,就算掌握了这一部分的表达式求值规则:
a = b += c++ - d + --e / -f |
等价于:
a = ( b += ( (c++) - d + (--e) / (-f) ) ) |
子表达式的求值顺序
C语言没有定义子表达式的求值顺序(除了含有 逻辑与,逻辑或 或 逗号运算符的表达式(后面会讲))。
但是不管子表达式的计算顺序如何,大多数表达式都有相同的值。但是,当子表达式改变了某个操作数的值时,产生的值就可能不一致了。思考下面的例子:
a = 5; |
第二条语句的执行结果是未定义的。对大多数编译器而言,c 的值是 6 或者 2。取决于 子表达式 b = a + 2 和 a = 1 的求值顺序。
像上例那样,在表达式中,既在某处访问变量的值,又在别处修改它的值是不可取的。
为了避免出现此类情况,我们可以将子表达式分离:
a = 5; |
执行完这些语句后,c 的值将始终是 6
除此之外,自增自减运算符也要小心使用。如下例:
i = 2; |
j 有两种可能:4 或 6
我们很自然的认为结果是 4 。但是其实该语句的执行结果是未定义的。
j 的值为 6 的情况:
- 取出第二个操作数(i 的原始值),然后 i 自增
- 取出第一个操作数(i 的新值)
- 将取除的两个操作数相乘(2 和 3),结果是 6
“取出”变量意味着从内存中获取它们的值。变量后续变化不会影响已经取出的值,因为取出的值通常存储在 CPU 中称为寄存器的一个特殊位置。
未定义行为
未定义行为(undefined behavior): 类似上面两个例子中的语句会导致 未定义行为,这和我们前面讲的由实现定义的行为是不同的。当程序中出现未定义行为时,后果是不可预料的。不同的编译器给出的结果可能是不同的。也就是说,程序可能无法通过编译,也可能运行时崩溃,不稳定或者产生无意义的结果。换句话说,我们应该像躲避“新冠”一样避免未定义行为。
参考资料:《C Primer Plus》《C语言程序设计:现代方法》
[^1]: 对称性有助于减少复杂度(协程包含例程)。对称性无处不在。Epigrams on Programming 编程警句