算术运算符
算术运算符对其操作数应用标准数学运算。
本节尚未完善 原因:考虑为本表和其他涵盖多个主题的表格提供更通用的 ToC |
运算符 | 运算符名称 | 示例 | 结果 |
---|---|---|---|
+ | 一元加 | +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 位 |
内容 |
[编辑] 溢出
无符号整数算术始终以 modulo 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
,所有浮点算术都可以像中间结果具有无限范围和精度一样执行,即省略舍入误差和浮点异常的优化,这些误差和异常将在表达式按原样精确计算时观察到。例如,允许 (x*y) + z 用单个融合乘加 CPU 指令实现,或优化 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
[编辑] 加法运算符
二元加法算术运算符表达式具有以下形式
lhs + rhs |
(1) | ||||||||
lhs - rhs |
(2) | ||||||||
[编辑] 算术加法和减法
如果两个操作数都具有 算术类型,那么
- 首先,执行 通常的算术转换
- 然后,按照数学中的通常规则,对转换后的操作数的值进行加法或减法(对于减法,从 lhs 中减去 rhs),但以下情况除外:
- 如果一个操作数为 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
[edit] 指针算术
- 如果指针
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]
[edit] 乘法运算符
二元乘法算术运算符表达式具有以下形式
lhs * rhs |
(1) | ||||||||
lhs / rhs |
(2) | ||||||||
lhs % rhs |
(3) | ||||||||
- 首先,执行 通常的算术转换。然后…
[edit] 乘法
二元运算符 * 执行其操作数的乘法(在通常的算术转换之后),遵循通常的算术定义,但以下情况除外:
- 如果一个操作数是 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
[edit] 除法
二元运算符 /
用第二个操作数除第一个操作数(在通常的算术转换之后),遵循通常的算术定义,但以下情况除外:
- 当通常算术转换后的类型是整数类型时,结果是代数商(而不是分数),以实现定义的方向四舍五入(直到 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
[edit] 余数
二元运算符 % 生成第一个操作数除以第二个操作数(在通常的算术转换之后)的余数。
余数的符号定义如下:如果商 a/b
在结果类型中可表示,则 (a/b)*b + a%b == a.
如果第二个操作数为零,则行为未定义。
如果商 a/b
在结果类型中不可表示,则 a/b
和 a%b
的行为均未定义(这意味着 INT_MIN%-1 在 2 的补码系统中未定义)
注意:余数运算符不适用于浮点类型,库函数 fmod 提供了该功能。
[edit] 按位逻辑
按位算术运算符表达式具有以下形式
~ rhs |
(1) | ||||||||
lhs & rhs |
(2) | ||||||||
lhs | rhs |
(3) | ||||||||
lhs ^ rhs |
(4) | ||||||||
其中
lhs, rhs | - | 整数类型的表达式 |
首先,运算符 &、^ 和 | 对两个操作数执行 通常的算术转换,而运算符 ~ 对其唯一的操作数执行 整数提升。
然后,按位应用相应的二进制逻辑运算符;也就是说,结果的每个位根据应用于操作数对应位的逻辑运算(NOT、AND、OR 或 XOR)设置或清除。
注意:按位运算符通常用于操作位集和位掩码。
注意:对于无符号类型(提升后),表达式 ~E 等效于结果类型可表示的最大值减去 E 的原始值。
可能的输出
Promoted mask: 0x000000f0 Value: 0x12345678 Setting bits: 0x123456f8 Clearing bits: 0x12345608 Selecting bits: 0x00000070
[edit] 移位运算符
按位移位运算符表达式具有以下形式
lhs << rhs |
(1) | ||||||||
lhs >> rhs |
(2) | ||||||||
其中
lhs, rhs | - | 整数类型的表达式 |
首先,对每个操作数分别执行 整数提升(注意:这与其他二元算术运算符不同,其他二元算术运算符都执行通常的算术转换)。结果的类型是在提升后 lhs 的类型。
如果 rhs 为负数或大于或等于提升后的 lhs 中的位数,则行为未定义。
对于无符号的 lhs,LHS << RHS
的值为 LHS * 2RHS
,模返回类型加 1 的最大值(即,执行按位左移,并且移出目标类型的位被丢弃)。对于具有非负值的带符号的 lhs,LHS << RHS
的值为 LHS * 2RHS
,如果它在 lhs 的提升类型中可表示,否则行为未定义。
对于无符号 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++ 文档 关于 算术运算符
|