Symmetry is a complexity reducing concept (co-routines include sub-routines); seek it everywhere. 1
[TOC]
一元运算符(只需要 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语言的目标之一——高效,这常常意味着与硬件相匹配。
对于我们来说,我们要尽量避免编写这种由实现定义的行为的程序。如果不能做到,起码要仔细查阅手册。
当表达式包含多个运算符时,其含义可能不是一目了然的。我们的解决方法是:
- 用括号进行分组
- 了解运算符的优先级和结合性
(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)
在许多语言(特别是 C 语言)中,优先级和结合性规则都是十分重要的。然而 C 语言的运算符太多了(差不多 50 种)。为了自己和他人理解代码的方便,请最好加上足够多的圆括号。
求出表达式的值后往往需要将其存储在变量中,以便将来使用。C语言的 = (简单赋值 simple assignment)运算符可以用于此目的。为了更新已经存储在变量中的值,C语言还提供了一种复合赋值(compound assignment)。
表达式 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
赋值运算要求它的左操作数必须是左值(lvalue)。左值表示在计算机中的存储对象,而不是常量或计算的结果。左值是变量。
例 2-4:
12 = i;
i + j = 0;
-i = j;
以上三种表达式都是错误的。
i = i + 2;
//等同于
i += 2;
上面的例子中 += 就是一种符合运算符,表示:将自身表示的数增加 2 后再赋值给自己。
与加法相似,所有赋值运算符的工作原理大体相同。
+=
-=
*=
/=
%=
注意:
-
i *= j + k
和i = i * j + k
是不一样的。 -
使用复合赋值运算符时,注意不要交换组成运算符的两个字符的位置。如:
i += j
写成了i =+ j
后者等价于:i = (+j)
复合运算符有着和 =
运算符一样的特性。它们也是右结合的,所以:
i += j += k
等价于i += (j += k)
++
--
“自增”(加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
部分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 的情况:
- 取出第二个操作数(i 的原始值),然后 i 自增
- 取出第一个操作数(i 的新值)
- 将取除的两个操作数相乘(2 和 3),结果是 6
“取出”变量意味着从内存中获取它们的值。变量后续变化不会影响已经取出的值,因为取出的值通常存储在 CPU 中称为寄存器的一个特殊位置。
未定义行为(undefined behavior): 类似上面两个例子中的语句会导致 未定义行为,这和我们前面讲的由实现定义的行为是不同的。当程序中出现未定义行为时,后果是不可预料的。不同的编译器给出的结果可能是不同的。也就是说,程序可能无法通过编译,也可能运行时崩溃,不稳定或者产生无意义的结果。换句话说,我们应该像躲避“新冠”一样避免未定义行为。
参考资料:《C Primer Plus》《C语言程序设计:现代方法》
Footnotes
-
对称性有助于减少复杂度(协程包含例程)。对称性无处不在。Epigrams on Programming 编程警句 ↩