算术运算符
算术运算符对其操作数应用标准数学运算。
本节尚不完整 原因:考虑为此表以及涵盖多个主题的其他表提供更通用的目录 |
运算符 | 运算符名称 | 示例 | 结果 |
---|---|---|---|
+ | 一元加 | +a | 经过提升后 a 的值 |
- | 一元减 | -a | a 的负值 |
+ | 加法 | a + b | a 与 b 的和 |
- | 减法 | a - b | a 减去 b 的差 |
* | 乘积 | a * b | a 与 b 的积 |
/ | 除法 | a / b | a 除以 b 的商 |
% | 余数 | a % b | a 除以 b 的余数 |
~ | 按位非 | ~a | a 的按位非 |
& | 按位与 | a & b | a 与 b 的按位与 |
| | 按位或 | a | b | a 与 b 的按位或 |
^ | 按位异或 | a ^ b | a 与 b 的按位异或 |
<< | 按位左移 | a << b | a 左移 b 位 |
>> | 按位右移 | a >> b | a 右移 b 位 |
目录 |
[编辑] 溢出
无符号整数算术运算始终以 模 2n
执行,其中 n 是该特定整数中的位数。 例如,对于 unsigned int,将 1 加到 UINT_MAX 得到 0,而从 0 中减去 1 得到 UINT_MAX。
当有符号整数算术运算溢出(结果不适合结果类型)时,行为是未定义的:它可能根据表示规则(通常是补码)环绕,它可能在某些平台上或由于编译器选项(例如 GCC 和 Clang 中的 -ftrapv
)而陷入陷阱,或者可能被编译器完全优化掉。
[编辑] 浮点环境
如果 #pragma STDC FENV_ACCESS 设置为 ON
,则所有浮点算术运算符都遵循当前的浮点舍入方向,并报告 math_errhandling 中指定的浮点算术错误,除非是 静态初始化器 的一部分(在这种情况下,不会引发浮点异常,并且舍入模式为最近舍入)
[编辑] 浮点收缩
除非 #pragma STDC FP_CONTRACT 设置为 OFF
,否则所有浮点算术运算都可以像中间结果具有无限范围和精度一样执行,也就是说,优化会省略舍入误差和浮点异常,如果表达式完全按照编写的方式求值,则会观察到这些误差和异常。 例如,允许使用单个融合乘加 CPU 指令实现 (x*y) + z,或将 a = x*x*x*x; 优化为 tmp = x*x; a = tmp*tmp。
与收缩无关,浮点算术的中间结果可能具有与其类型指示的范围和精度不同的范围和精度,请参见 FLT_EVAL_METHOD
[编辑] 一元算术
一元算术运算符表达式的形式为
+ 表达式 |
(1) | ||||||||
- 表达式 |
(2) | ||||||||
表达式 | - | 任何算术类型的表达式 |
一元加和一元减都首先对其操作数应用整型提升,然后
- 一元加返回提升后的值
- 一元减返回提升后的值的负值(除非 NaN 的负值是另一个 NaN)
表达式的类型是提升后的类型,值类别是非左值。
[编辑] 注解
在一元减应用于 INT_MIN、LONG_MIN 或 LLONG_MIN 时,由于有符号整数溢出,会调用未定义行为,这在典型的(2 的补码)平台上发生。
在 C++ 中,一元运算符 +
也可以用于其他内置类型,例如数组和函数,但在 C 中则不然。
#include <stdio.h> #include <complex.h> #include <limits.h> int main(void) { char c = 'a'; printf("sizeof char: %zu sizeof int: %zu\n", sizeof c, sizeof +c); printf("-1, where 1 is signed: %d\n", -1); // Defined behavior since arithmetic is performed for unsigned integer. // Hence, the calculation is (-1) modulo (2 raised to n) = UINT_MAX, where n is // the number of bits of unsigned int. If unsigned int is 32-bit long, then this // gives (-1) modulo (2 raised to 32) = 4294967295 printf("-1, where 1 is unsigned: %u\n", -1u); // Undefined behavior because the mathematical value of -INT_MIN = INT_MAX + 1 // (i.e. 1 more than the maximum possible value for signed int) // // printf("%d\n", -INT_MIN); // Undefined behavior because the mathematical value of -LONG_MIN = LONG_MAX + 1 // (i.e. 1 more than the maximum possible value for signed long) // // printf("%ld\n", -LONG_MIN); // Undefined behavior because the mathematical value of -LLONG_MIN = LLONG_MAX + 1 // (i.e. 1 more than the maximum possible value for signed long long) // // printf("%lld\n", -LLONG_MIN); double complex z = 1 + 2*I; printf("-(1+2i) = %.1f%+.1f\n", creal(-z), cimag(-z)); }
可能的输出
sizeof char: 1 sizeof int: 4 -1, where 1 is signed: -1 -1, where 1 is unsigned: 4294967295 -(1+2i) = -1.0-2.0
[编辑] 加法运算符
二元加法算术运算符表达式的形式为
左操作数 + 右操作数 |
(1) | ||||||||
左操作数 - 右操作数 |
(2) | ||||||||
[编辑] 算术加法和减法
如果两个操作数都具有算术类型,则
- 首先,执行常用算术转换
- 然后,按照常用的数学规则,将转换后操作数的值相加或相减(对于减法,从 左操作数 中减去 右操作数),但
- 如果一个操作数是 NaN,则结果为 NaN
- 无穷减无穷是 NaN,并且引发 FE_INVALID
- 无穷加负无穷是 NaN,并且引发 FE_INVALID
复数和虚数加法和减法定义如下(请注意,如果两个操作数都是虚数,则结果类型为虚数;如果一个操作数是实数,另一个操作数是虚数,则结果类型为复数,如常用算术转换所指定)
+ 或 - | u | iv | u + iv |
---|---|---|---|
x | x ± u | x ± iv | (x ± u) ± iv |
iy | ±u + iy | i(y ± v) | ±u + i(y ± v) |
x + iy | (x ± u) + iy | x + i(y ± v) | (x ± u) + i(y ± v) |
// work in progress // note: take part of the c/language/conversion example
[编辑] 指针算术
- 如果指针
P
指向索引为I
的数组元素,则
- P+N 和 N+P 是指向同一数组中索引为
I+N
的元素的指针 - P-N 是指向同一数组中索引为
I-N
的元素的指针
- P+N 和 N+P 是指向同一数组中索引为
只有当原始指针和结果指针都指向同一数组的元素或超出该数组末尾一个位置时,行为才是定义的。 请注意,当 p 指向数组的第一个元素时执行 p-1 是未定义行为,并且可能在某些平台上失败。
- 如果指针
P1
指向索引为I
的数组元素(或超出末尾一个位置),并且P2
指向同一数组中索引为J
的元素(或超出末尾一个位置),则
- P1-P2 的值等于 I-J,类型为 ptrdiff_t(这是一个有符号整数类型,通常是可声明的最大对象大小的一半)
只有当结果适合 ptrdiff_t 时,行为才是定义的。
为了指针算术的目的,指向非任何数组元素的对象的指针被视为指向大小为 1 的数组的第一个元素的指针。
// work in progress int n = 4, m = 3; int a[n][m]; // VLA of 4 VLAs of 3 ints each int (*p)[m] = a; // p == &a[0] p = p + 1; // p == &a[1] (pointer arithmetic works with VLAs just the same) (*p)[2] = 99; // changes a[1][2]
[编辑] 乘法运算符
二元乘法算术运算符表达式的形式为
左操作数 * 右操作数 |
(1) | ||||||||
左操作数 / 右操作数 |
(2) | ||||||||
左操作数 % 右操作数 |
(3) | ||||||||
- 首先,执行常用算术转换。 然后...
[编辑] 乘法
二元运算符 * 对其操作数执行乘法运算(在常用算术转换之后),遵循常用的算术定义,但
- 如果一个操作数是 NaN,则结果为 NaN
- 无穷乘以零得到 NaN,并引发 FE_INVALID
- 无穷乘以非零数得到无穷(即使对于复数参数也是如此)
由于在 C 中,任何复数值,即使其另一部分是 NaN,只要至少有一个无限部分,它也是无穷大,因此常用算术规则不适用于复数-复数乘法。 浮点操作数的其他组合遵循下表
* | u | iv | u + iv |
---|---|---|---|
x | xu | i(xv) | (xu) + i(xv) |
iy | i(yu) | −yv | (−yv) + i(yu) |
x + iy | (xu) + i(yu) | (−yv) + i(xv) | 特殊规则 |
除了无穷大处理外,复数乘法不允许中间结果溢出,除非 #pragma STDC CX_LIMITED_RANGE 设置为 ON
,在这种情况下,该值可以像 (x+iy)×(u+iv) = (xu-yv)+i(yu+xv) 一样计算,因为程序员承担限制操作数范围和处理无穷大的责任。
尽管不允许不必要的溢出,但复数乘法可能会引发虚假的浮点异常(否则,实现非溢出版本非常困难)
#include <stdio.h> #include <stdio.h> #include <complex.h> #include <math.h> int main(void) { // TODO simpler cases, take some from C++ double complex z = (1 + 0*I) * (INFINITY + I*INFINITY); // textbook formula would give // (1+i0)(∞+i∞) ⇒ (1×∞ – 0×∞) + i(0×∞+1×∞) ⇒ NaN + I*NaN // but C gives a complex infinity printf("%f + i*%f\n", creal(z), cimag(z)); // textbook formula would give // cexp(∞+iNaN) ⇒ exp(∞)×(cis(NaN)) ⇒ NaN + I*NaN // but C gives ±∞+i*nan double complex y = cexp(INFINITY + I*NAN); printf("%f + i*%f\n", creal(y), cimag(y)); }
可能的输出
inf + i*inf inf + i*nan
[编辑] 除法
二元运算符 /
将第一个操作数除以第二个操作数(在常用算术转换之后),遵循常用的算术定义,但
- 当常用算术转换后的类型为整数类型时,结果是代数商(不是分数),在实现定义的方向上舍入(C99 之前)向零截断(自 C99 起)
- 如果一个操作数是 NaN,则结果为 NaN
- 如果第一个操作数是复数无穷大,而第二个操作数是有限的,则
/
运算符的结果是复数无穷大 - 如果第一个操作数是有限的,而第二个操作数是复数无穷大,则
/
运算符的结果为零。
由于在 C 中,任何复数值,即使其另一部分是 NaN,只要至少有一个无限部分,它也是无穷大,因此常用算术规则不适用于复数-复数除法。 浮点操作数的其他组合遵循下表
/ | u | iv |
---|---|---|
x | x/u | i(−x/v) |
iy | i(y/u) | y/v |
x + iy | (x/u) + i(y/u) | (y/v) + i(−x/v) |
除了无穷大处理外,复数除法不允许中间结果溢出,除非 #pragma STDC CX_LIMITED_RANGE 设置为 ON
,在这种情况下,该值可以像 (x+iy)/(u+iv) = [(xu+yv)+i(yu-xv)]/(u2
+v2
) 一样计算,因为程序员承担限制操作数范围和处理无穷大的责任。
尽管不允许不必要的溢出,但复数除法可能会引发虚假的浮点异常(否则,实现非溢出版本非常困难)
如果第二个操作数为零,则行为是未定义的,除非支持 IEEE 浮点算术,并且正在进行浮点除法,则
- 非零数除以 ±0.0 得到正确符号的无穷大,并引发 FE_DIVBYZERO
- 0.0 除以 0.0 得到 NaN,并引发 FE_INVALID
[编辑] 余数
二元运算符 % 产生第一个操作数除以第二个操作数的余数(在常用算术转换之后)。
余数的符号定义为:如果商 a/b
可在结果类型中表示,则 (a/b)*b + a%b == a。
如果第二个操作数为零,则行为是未定义的。
如果商 a/b
不可在结果类型中表示,则 a/b
和 a%b
的行为都是未定义的(这意味着 INT_MIN%-1 在 2 的补码系统上是未定义的)
注意:余数运算符不适用于浮点类型,库函数 fmod 提供了该功能。
[编辑] 按位逻辑
按位算术运算符表达式的形式为
~ 右操作数 |
(1) | ||||||||
左操作数 & 右操作数 |
(2) | ||||||||
左操作数 | 右操作数 |
(3) | ||||||||
左操作数 ^ 右操作数 |
(4) | ||||||||
其中
左操作数, 右操作数 | - | 整数类型的表达式 |
首先,运算符 &、^ 和 | 对两个操作数执行常用算术转换,运算符 ~ 对其唯一操作数执行整型提升。
然后,按位应用相应的二进制逻辑运算符; 也就是说,根据应用于操作数相应位的逻辑运算(非、与、或或异或),设置或清除结果的每一位。
注意:按位运算符通常用于操作位集和位掩码。
注意:对于无符号类型(提升后),表达式 ~E 等效于结果类型可表示的最大值减去 E 的原始值。
可能的输出
Promoted mask: 0x000000f0 Value: 0x12345678 Setting bits: 0x123456f8 Clearing bits: 0x12345608 Selecting bits: 0x00000070
[编辑] 移位运算符
按位移位运算符表达式的形式为
左操作数 << 右操作数 |
(1) | ||||||||
左操作数 >> 右操作数 |
(2) | ||||||||
其中
左操作数, 右操作数 | - | 整数类型的表达式 |
首先,分别对每个操作数执行整型提升(注意:这与其他所有执行常用算术转换的二元算术运算符不同)。 结果的类型是提升后 左操作数 的类型。
如果 右操作数 为负数,或者大于或等于提升后的 左操作数 中的位数,则行为是未定义的。
对于无符号 左操作数,LHS << RHS
的值是 LHS * 2RHS
的值,模返回类型的最大值加 1 减少(即,执行按位左移,并且移出目标类型的位将被丢弃)。 对于具有非负值的有符号 左操作数,如果 LHS << RHS
的值 LHS * 2RHS
可在提升后的 左操作数 类型中表示,则该值是 LHS * 2RHS
,否则行为是未定义的。
对于无符号 左操作数 和具有非负值的有符号 左操作数,LHS >> RHS
的值是 LHS / 2RHS
的整数部分。 对于负数 LHS
,LHS >> RHS
的值是实现定义的,在大多数实现中,这会执行算术右移(以便结果保持为负数)。 因此,在大多数实现中,右移有符号 LHS
会用原始符号位填充新的高阶位(即,如果它为非负数,则用 0 填充;如果它为负数,则用 1 填充)。
#include <stdio.h> enum {ONE=1, TWO=2}; int main(void) { char c = 0x10; unsigned long long ulong_num = 0x123; printf("0x123 << 1 = %#llx\n" "0x123 << 63 = %#llx\n" // overflow truncates high bits for unsigned numbers "0x10 << 10 = %#x\n", // char is promoted to int ulong_num << 1, ulong_num << 63, c << 10); long long long_num = -1000; printf("-1000 >> 1 = %lld\n", long_num >> ONE); // implementation defined }
可能的输出
0x123 << 1 = 0x246 0x123 << 63 = 0x8000000000000000 0x10 << 10 = 0x4000 -1000 >> 1 = -500
[编辑] 参考
- C17 标准 (ISO/IEC 9899:2018)
- 6.5.3.3 一元算术运算符 (p: 64)
- 6.5.5 乘法运算符 (p: 66)
- 6.5.6 加法运算符 (p: 66-68)
- 6.5.7 按位移位运算符 (p: 68)
- 6.5.10 按位与运算符 (p: 70)
- 6.5.11 按位异或运算符 (p: 70)
- 6.5.12 按位包含或运算符 (p: 70-71)
- C11 标准 (ISO/IEC 9899:2011)
- 6.5.3.3 一元算术运算符 (p: 89)
- 6.5.5 乘法运算符 (p: 92)
- 6.5.6 加法运算符 (p: 92-94)
- 6.5.7 按位移位运算符 (p: 94-95)
- 6.5.10 按位与运算符 (p: 97)
- 6.5.11 按位异或运算符 (p: 98)
- 6.5.12 按位包含或运算符 (p: 98)
- C99 标准 (ISO/IEC 9899:1999)
- 6.5.3.3 一元算术运算符 (p: 79)
- 6.5.5 乘法运算符 (p: 82)
- 6.5.6 加法运算符 (p: 82-84)
- 6.5.7 按位移位运算符 (p: 84-85)
- 6.5.10 按位与运算符 (p: 87)
- 6.5.11 按位异或运算符 (p: 88)
- 6.5.12 按位包含或运算符 (p: 88)
- C89/C90 标准 (ISO/IEC 9899:1990)
- 3.3.3.3 一元算术运算符
- 3.3.5 乘法运算符
- 3.3.6 加法运算符
- 3.3.7 按位移位运算符
- 3.3.10 按位与运算符
- 3.3.11 按位异或运算符
- 3.3.12 按位包含或运算符
[编辑] 参见
常用运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 自增 自减 |
算术 | 逻辑 | 比较 | 成员 访问 |
其他 |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
a(...) |
C++ 文档 关于 算术运算符
|