算术运算符

来自cppreference.com
< c‎ | language

算术运算符应用标准数学运算符到其操作数。

运算符 运算符名 示例 结果
+ 一元加 +a a 提升后的值
- 一元减 -a a 的相反数
+ 加法 a + b ab
- 减法 a - b a 减去 b
* 乘法 a * b ab 的积
/ 除法 a / b a 除以 b 的商
% a % b a 除以 b 的余数
~ 逐位非 ~a a 的逐位非
& 逐位与 a & b ab 的逐位与
| 逐位或 a | b ab 的逐位或
^ 逐位异或 a ^ b ab 的逐位异或
<< 逐位左移 a << b a 左移 b
>> 逐位右移 a >> b a 右移 b

溢出

无符号整数算术始终进行 modulo 2n
,其中 n 是该特定整数中的位数。例如对于 unsigned int ,加一到 UINT_MAX 给出 0 ,而从 0 减一给出 UINT_MAX

有符号算术运算溢出(结果不符合结果类型)时,行为未定义:可以按照表示(典型为补码)的规则回卷,可以在某些平台上或由于编译器选项(例如 gcc 和 clang 中的 -ftrapv )陷落,或完全被编译器优化掉

浮点环境

若设置 #pragma STDC FENV_ACCESSON ,则所有浮点算术运算符遵循当前浮点舍入方向并报告 math_errhandling 中指定的错误,除非它们是静态初始化器的一部分(该情况下不引发浮点异常,而舍入模式为到最接近)。

浮点缩略

除非设置 #pragma STDC FP_CONTRACTOFF ,否则可能如同中间结果拥有无限范围和精度一般进行所有浮点算术,这种优化省略若准确按写法求值表达式,则会观察到的舍入误差和浮点异常。例如,允许以单条融合乘加 CPU 指令实现 (x*y) + z ,或把 a = x*x*x*x; 优化成 tmp = x*x; a = tmp*tmp

与缩略无关,浮点算术的中间结果可以拥有异于其类型所指定的范围和精度,见 FLT_EVAL_METHOD

一元算术

一元算术运算符表达式拥有形式

+ expression (1)
- expression (2)
1) 一元加(提升)
2) 一元减(相反数)

where

expression - 任何算术类型表达式

一元加和一元减都首先应用整数提升到其操作数,然后

  • 一元加返回提升后的值
  • 一元减返回提升后值的相反数(除了 NaN 的相反数是另一 NaN )

表达式类型为提升后的类型,而值类别为非左值。

注意

在典型(补码)平台上应用到 INT_MINLONG_MINLLONG_MIN 时,一元减引起有符号整数溢出所致的未定义行为。

C++ 中一元运算符 + 亦能用于其他内建类型,例如数组和函数,但 C 中不能。

#include <stdio.h>
#include <complex.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);
    printf("-1, where 1 is unsigned: %u\n", -1u);
 
    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)
1) 加法: lhsrhs 必须为下列之一
  • 都拥有算术类型,包含复数和虚数
  • 一个是指向完整对象的指针类型,另一个拥有整数类型
2) 减法: lhsrhs 必须为下列之一
  • 都拥有算术类型,包含复数和虚数
  • lhs 拥有指向完整对象的指针类型, rhs 拥有整数类型
  • 都是指向拥有兼容类型的完整对象指针,忽略限定符

算术加法与减法

若两个操作数都拥有算术类型,则

  • 首先,进行通常算术转换
  • 然后,遵循数学规则,加或减操作数在提升后的值(对于减法,从 lhs 减去 rhs ),除了
  • 若操作数之一为 NaN ,则结果为 NaN
  • 同号无穷大减无穷大为 NaN 并引发 FE_INVALID
  • 正无穷大加负无穷大为 NaN 并引发 FE_INVALID

复数和虚数加减法定义如下(注意若两个操作数均为虚数则结果类型为虚数,而若一个操作数为实数而另一为虚数,则结果为复数,同通常算术转换所指定):

+ or - 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)


// 工作中
// 注意:采用 c/language/conversion 示例的一部分

指针算术

  • 若指针 P 指向下标为 I 的数组元素,则
  • P+NN+P 是指向同一数组中下标为 I+N 的元素的指针
  • P-N 是指向同一数组中下标为 I-N 的元素的指针

仅若原指针和结果指针都指向同一数组中的元素,或该数组的尾后一位置,行为才有定义。注意在 p 指向数组首元素时,执行 p-1 是未定义行为,并且在某些平台上可能失败。

  • 若指针 P1 指向下标为 I 的数组元素(或尾后一位置)而 P2 是指向同一数组的下标为 J 的元素(或尾后一位置),则
  • P1-P2 拥有等于 I-J 的值和 ptrdiff_t 类型(有符号整数类型,最大大小典型地为能声明的最大对象的一半)

仅若结果适合于 ptrdiff_t ,行为才有定义。

为指针算术的目的,把指向非数组元素的对象的指针当做指向大小为 1 的数组首元素的指针。

// 工作中
int n = 4, m = 3;
int a[n][m];     // 4 个 VLA 的 VLA ,每个有 3 个 int
int (*p)[m] = a; // p == &a[0] 
p = p + 1;       // p == &a[1] ( VLA 所用的指针算术一如平常)
(*p)[2] = 99;    // 更改 a[1][2]

乘法性运算符

下列乘法性运算符拥有形式

lhs * rhs (1)
lhs / rhs (2)
lhs % rhs (3)
1) 乘法。 lhsrhs 必须拥有算术类型
2) 除法。 lhsrhs 必须拥有算术类型
3) 余数。 lhsrhs 必须拥有整数类型

乘法

二元运算符 * 遵循通常算术定义,进行其操作数(在通常算术提升后)的乘法,除了

  • 若操作数之一为 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) special rules

在无穷大处理外,不允许复数乘法上溢中间结果,除了在设置 #pragma STDC CX_LIMITED_RANGEON 时,该情况下可以如同 (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 从 C++ 采取一些更简单的情况
 
 
   double complex z = (1 + 0*I) * (INFINITY + I*INFINITY);
// 教科书公式会给出
// (1+i0)(∞+i∞) ⇒ (1×∞ – 0×∞) + i(0×∞+1×∞) ⇒ NaN + I*NaN
// 但 C 给出复无穷大
   printf("%f + i*%f\n", creal(z), cimag(z));
 
// 教科书公式会给出
// cexp(∞+iNaN) ⇒ exp(∞)×(cis(NaN)) ⇒ NaN + I*NaN
// 但 C 给出 ±∞+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_RANGEON 时,该情况下可以如同 (x+iy)/(u+iv) = [(xu+yv)+i(yu-xv)]/(u2
+v2
)
一般计算值,因为假定了程序员负责限制操作数的范围并处理无穷大。

虽然不允许过度的上溢,复数除法可能引发虚假的浮点异常(否则实现不上溢版本困难到近乎不可能)。

若第二操作数为零,则行为未定义,除非支持 IEE 浮点算术,且发生浮点除法,则

  • 非零数除以 ±0.0 给出符号正确的无穷大并引发 FE_DIVBYZERO
  • 0.0 除以 0.0 给出 NaN 并引发 FE_INVALID

余数

二元运算符 % 产出第一操作数除以第二操作数(在通常算术转换后)的余数。

余数的符号定义,使得若商 a/b 能以结果类型表示,则 (a/b)*b + a%b == a

若第二操作数为零,则行为未定义。

若商 a/b 不能以结果类型表示,则 a/ba%b 的行为都未定义(这表明 INT_MIN%-1 在补码系统上未定义)。

注意:余数运算符不可用于浮点类型上,库函数 fmod 提供该功能。

逐位逻辑

逐位逻辑运算符拥有形式

~ rhs (1)
lhs & rhs (2)
lhs | rhs (3)
lhs ^ rhs (4)
1) 逐位非
2) 逐位与
3) 逐位或
4) 逐位异或

其中

lhs, rhs - 整数类型表达式

首先,运算符 & 、 ^ 和 | 在两个操作数上进行通常算术转换,而运算符 ~ 在其唯一的操作数上进行整数提升

然后,逐位应用对应的逻辑运算符;即按照应用到操作数的每位的逻辑运算(或、与、非、异或),设置或清除结果的对应位。

注意:位运算符常用于操作位集和位掩码。

注意:对于无符号类型(提升后),表达式 ~E 等价于结果类型的最大可表示值减 E 的原值。

#include <stdio.h>
#include <stdint.h>
int main(void)
{
    uint16_t mask = 0x00f0;
    uint32_t a = 0x12345678;
    printf("Value: %#x mask: %#x\n"
           "Setting bits:   %#x\n"
           "Clearing bits:  %#x\n"
           "Selecting bits: %#x\n",
           a,mask,(a|mask),(a&~mask),(a&mask));
}

可能的输出:

Value: 0x12345678 mask: 0xf0
Setting bits:   0x123456f8
Clearing bits:  0x12345608
Selecting bits: 0x70

位移运算符

位移运算符表达式拥有形式

lhs << rhs (1)
lhs >> rhs (2)
1) lhs 左移 rhs
2) lhs 右移 rhs

其中

lhs, rhs - 整数类型表达式

首先,在每个操作数上独自进行整数提升(注意:这不同于其他二元算术运算符,它们全都进行通常算术转换)。结果类型为 lhs 在提升后的类型。

对于无符号 lhsLHS << RHS 的值是 LHS * 2RHS
以返回类型最大值加 1 取模(即进行逐位左移,并舍弃移出目标类型的位)。对于有非负值的有符号 lhs ,若值能以 lhs 的提升后类型表示,则 LHS << RHS 的值是 LHS * 2RHS
,否则行为未定义。

对于无符号 lhs 和有非负值的有符号 lhsLHS >> RHS 的值是 LHS / 2RHS
的整数部分。对于负的 LHSLHS >> RHS 的值是实现定义的,大多数实现中,这进行算术右移(故结果仍为负)。从而在大多数实现中,右移有符号的 LHS 会以原符号位填充新的高位(即若原数非负则为 0 ,而若原数为负则为 1 )。

任何情况下,若 rhs 为负或大于或等于提升后的 lhs 中的位数,则行为未定义。

#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"   // 对无符号数,溢出截断高位
           "0x10  << 10 = %#x\n",    // char 被提升到 int
           ulong_num << 1, ulong_num << 63, c << 10);
    long long long_num = -1000;
    printf("-1000 >> 1 = %lld\n", long_num >> ONE);  // 实现定义
}

可能的输出:

0x123 << 1  = 0x246
0x123 << 63 = 0x8000000000000000
0x10  << 10 = 0x4000
-1000 >> 1 = -500

引用

  • C11 标准(ISO/IEC 9899:2011):
  • 6.5.3.3 Unary arithmetic operators (p: 89)
  • 6.5.5 Multiplicative operators (p: 92)
  • 6.5.6 Additive operators (p: 92-94)
  • 6.5.7 Bitwise shift operators (p: 94-95)
  • 6.5.10 Bitwise AND operator (p: 97)
  • 6.5.11 Bitwise exclusive OR operator (p: 98)
  • 6.5.12 Bitwise inclusive OR operator (p: 98)
  • C99 标准(ISO/IEC 9899:1999):
  • 6.5.3.3 Unary arithmetic operators (p: 79)
  • 6.5.5 Multiplicative operators (p: 82)
  • 6.5.6 Additive operators (p: 82-84)
  • 6.5.7 Bitwise shift operators (p: 84-85)
  • 6.5.10 Bitwise AND operator (p: 87)
  • 6.5.11 Bitwise exclusive OR operator (p: 88)
  • 6.5.12 Bitwise inclusive OR operator (p: 88)
  • C89/C90 标准(ISO/IEC 9899:1990):
  • 3.3.3.3 Unary arithmetic operators
  • 3.3.5 Multiplicative operators
  • 3.3.6 Additive operators
  • 3.3.7 Bitwise shift operators
  • 3.3.10 Bitwise AND operator
  • 3.3.11 Bitwise exclusive OR operator
  • 3.3.12 Bitwise inclusive OR operator

参阅

运算符优先级

常用运算符
赋值 自增
自减
算术 逻辑 比较 成员
访问
其他

a = b
a += b
a -= b
a *= b
a /= b
a %= b
a &= b
a |= b
a ^= b
a <<= b
a >>= b

++a
--a
a++
a--

+a
-a
a + b
a - b
a * b
a / b
a % b
~a
a & b
a | b
a ^ b
a << b
a >> b

!a
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(...)
a, b
(type) a
? :
sizeof
_Alignof
(C11 起)