算术运算符
算术运算符对其操作数执行标准数学运算。
本节不完整 原因:考虑为此表和涵盖多个主题的其他表使用更通用的目录 |
运算符 | 运算符名称 | 示例 | 结果 |
---|---|---|---|
+ | 一元加 | +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,将 UINT_MAX 加 1 得到 0,将 0 减 1 得到 UINT_MAX。
当有符号整数算术运算溢出(结果不适合结果类型)时,行为是未定义的:它可能根据表示规则(通常是 2 的补码)进行环绕,可能在某些平台或由于编译器选项(例如 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)
表达式的类型是提升后的类型,值类别是非左值。
[编辑] 注意
在典型的(2 的补码)平台上,当应用于 INT_MIN、LONG_MIN 或 LLONG_MIN 时,一元减由于有符号整数溢出而导致未定义行为。
在 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
[编辑] 加性运算符
二元加性算术运算符表达式具有以下形式
lhs + rhs |
(1) | ||||||||
lhs - rhs |
(2) | ||||||||
[编辑] 算术加法和减法
如果两个操作数都具有算术类型,则
- 首先,执行常用算术转换
- 然后,转换后的操作数的值按照通常的数学规则进行加法或减法(对于减法,将 rhs 从 lhs 中减去),除了
- 如果一个操作数是 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]
[编辑] 乘性运算符
二元乘性算术运算符表达式具有以下形式
lhs * rhs |
(1) | ||||||||
lhs / rhs |
(2) | ||||||||
lhs % rhs |
(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
的行为均未定义(这意味着在 2 的补码系统上 INT_MIN%-1 是未定义的)
注意:余数运算符不适用于浮点类型,库函数 fmod 提供该功能。
[编辑] 按位逻辑
按位算术运算符表达式具有以下形式
~ rhs |
(1) | ||||||||
lhs & rhs |
(2) | ||||||||
lhs | rhs |
(3) | ||||||||
lhs ^ rhs |
(4) | ||||||||
其中
lhs, rhs | - | 整数类型的表达式 |
首先,运算符 &、^ 和 | 对两个操作数执行常用算术转换,运算符 ~ 对其唯一操作数执行整数提升。
然后,按位应用相应的二元逻辑运算符;也就是说,结果的每个位都根据应用于操作数相应位的逻辑运算(非、与、或或异或)进行设置或清除。
注意:按位运算符通常用于操作位集和位掩码。
注意:对于无符号类型(提升后),表达式 ~E 等价于结果类型可表示的最大值减去 E 的原始值。
可能的输出
Promoted mask: 0x000000f0 Value: 0x12345678 Setting bits: 0x123456f8 Clearing bits: 0x12345608 Selecting bits: 0x00000070
[编辑] 移位运算符
按位移位运算符表达式具有以下形式
lhs << rhs |
(1) | ||||||||
lhs >> rhs |
(2) | ||||||||
其中
lhs, rhs | - | 整数类型的表达式 |
首先,对每个操作数单独执行整数提升(注意:这与其他所有执行常用算术转换的二元算术运算符不同)。结果的类型是 lhs 提升后的类型。
如果 rhs 为负数或大于或等于提升后的 lhs 中的位数,则行为未定义。
对于无符号 lhs,LHS << RHS
的值是 LHS * 2RHS
的值,对返回类型的最大值加 1 取模(即执行按位左移,并丢弃移出目标类型的位)。对于具有非负值的有符号 lhs,如果 LHS * 2RHS
在 lhs 的提升类型中可表示,则 LHS << RHS
的值为 LHS * 2RHS
,否则行为未定义。
对于无符号 lhs 和具有非负值的有符号 lhs,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++ 文档 关于 算术运算符
|