Namespaces
Variants

Arithmetic operators

From cppreference.net

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

运算符 运算符名称 示例 结果
+ 一元加号 + 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

目录

溢出

无符号整数运算始终以 2 n
为模执行,其中 n 表示该特定整数的位数。例如对于 unsigned int 类型,将 UINT_MAX 加一得到 0 ,将 0 减一得到 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)
1) 一元加号(提升)
2) 一元减号(取负)
expression - 任意 算术类型 的表达式

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

  • 一元加号返回提升后的值
  • 一元减号返回提升后值的负值(除了NaN的负值是另一个NaN)

该表达式的类型是经过提升后的类型,且其 值类别 为非左值。

注释

一元负号在应用于 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);
    // 定义明确的行为,因为算术运算是针对无符号整数执行的。
    // 因此,计算为 (-1) 模 (2的n次方) = UINT_MAX,其中n是
    // unsigned int 的位数。如果 unsigned int 是32位长,那么
    // 结果为 (-1) 模 (2的32次方) = 4294967295
    printf("-1, where 1 is unsigned: %u\n", -1u); 
    // 未定义行为,因为 -INT_MIN 的数学值 = INT_MAX + 1
    // (即比有符号整数的最大可能值多1)
    //
    // printf("%d\n", -INT_MIN);
    // 未定义行为,因为 -LONG_MIN 的数学值 = LONG_MAX + 1
    // (即比有符号长整数的最大可能值多1)
    //
    // printf("%ld\n", -LONG_MIN);
    // 未定义行为,因为 -LLONG_MIN 的数学值 = LLONG_MAX + 1
    // (即比有符号长长整数的最大可能值多1)
    //
    // 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)
(注:根据要求,HTML标签、属性及 标签内的运算符均未翻译,C++术语lhs/rhs保留原样,仅对页面说明性文字进行了简体中文翻译)
1) 加法: lhs rhs 必须满足以下条件之一
  • 两者都具有 算术类型 ,包括复数类型和虚数类型
  • 一个是指向完整对象类型的指针,另一个具有整数类型
2) 减法: lhs rhs 必须满足以下条件之一
  • 两者均具有 算术类型 ,包括复数类型与虚数类型
  • lhs 为完整对象类型的指针, rhs 为整数类型
  • 两者均为指向 兼容 类型完整对象的指针(忽略限定符)

算术加法与减法

如果两个操作数都具有 算术类型 ,那么

  • 首先,执行 常规算术转换
  • 随后,根据数学常规规则对转换后的操作数值进行加法或减法运算(对于减法, 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指向数组首元素时执行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];     // 由4个包含3个整数的VLA组成的VLA
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) 乘法运算。 lhs rhs 必须具有 算术类型
2) 除法。 lhs rhs 必须具有 算术类型
3) 取余运算。 lhs rhs 必须具有 整数类型

乘法

二元运算符 * 对其操作数(经过常规算术转换后)执行乘法运算,遵循常规算术定义,但以下情况除外:

  • 若一个操作数为 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 更简单的案例,从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_RANGE 设置为 ON 时,此时数值可以按照 (x+iy)/(u+iv) = [(xu+yv)+i(yu-xv)]/(u 2
+v 2
)
公式计算,程序员需自行承担限定操作数范围和处理无穷大的责任。

尽管禁止不当溢出,复数除法仍可能引发虚假浮点异常(否则实现非溢出版本将极其困难)

如果第二个操作数为零,则行为未定义,除非支持 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 是未定义的)

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

位运算逻辑

按位算术运算符表达式的形式为

~ 右操作数 (1)
左操作数 & 右操作数 (2)
左操作数 | 右操作数 (3)
左操作数 ^ 右操作数 (4)
1) 按位取反
2) 按位与
3) 按位或
4) 按位异或

其中

lhs , rhs - 整数类型的表达式

首先,运算符 & ^ | 会对两个操作数执行 常规算术转换 ,而运算符 ~ 则对其唯一操作数执行 整型提升

随后,对应的二进制逻辑运算符会按位应用;也就是说,结果的每一位根据逻辑运算(NOT、AND、OR 或 XOR)设置或清除,该运算应用于操作数的对应位。

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

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

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

可能的输出:

Promoted mask:  0x000000f0
Value:          0x12345678
Setting bits:   0x123456f8
Clearing bits:  0x12345608
Selecting bits: 0x00000070

移位运算符

按位位移运算符表达式的形式为

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

其中

lhs , rhs - 整型表达式

首先,对每个操作数分别执行 整型提升 (注意:这一点与其他二元算术运算符不同,其他运算符都执行常规算术转换)。结果的类型是 lhs 经过提升后的类型。

rhs 为负数或大于等于提升后的 lhs 的位数时,其行为是未定义的。

对于无符号类型的 lhs LHS << RHS 的值为 LHS * 2 RHS
对返回值类型的最大值加 1 取模的结果(即执行按位左移操作,且移出目标类型的位将被丢弃)。对于具有非负值的有符号类型 lhs ,若 LHS * 2 RHS
可在 lhs 的提升类型中表示,则 LHS << RHS 的值为该计算结果,否则行为未定义。

对于无符号的 lhs 以及具有非负值的有符号 lhs LHS >> RHS 的值是 LHS / 2 RHS
的整数部分。对于负值的 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"   // 溢出会截断无符号数的高位
           "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

参考文献

  • C17 标准 (ISO/IEC 9899:2018):
  • 6.5.3.3 一元算术运算符 (页: 64)
  • 6.5.5 乘法运算符 (页: 66)
  • 6.5.6 加法运算符 (页: 66-68)
  • 6.5.7 位位移运算符 (页: 68)
  • 6.5.10 位与运算符 (页: 70)
  • 6.5.11 位异或运算符 (页: 70)
  • 6.5.12 位或运算符 (页: 70-71)
  • C11 标准 (ISO/IEC 9899:2011):
  • 6.5.3.3 一元算术运算符 (页: 89)
  • 6.5.5 乘法运算符 (页: 92)
  • 6.5.6 加法运算符 (页: 92-94)
  • 6.5.7 位位移运算符 (页: 94-95)
  • 6.5.10 位与运算符 (页: 97)
  • 6.5.11 位异或运算符 (页: 98)
  • 6.5.12 位或运算符 (页: 98)
  • C99标准(ISO/IEC 9899:1999):
  • 6.5.3.3 一元算术运算符(第79页)
  • 6.5.5 乘法运算符(第82页)
  • 6.5.6 加法运算符(第82-84页)
  • 6.5.7 位位移运算符(第84-85页)
  • 6.5.10 位与运算符(第87页)
  • 6.5.11 位异或运算符(第88页)
  • 6.5.12 位或运算符(第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 + = 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
a ? b : c
sizeof


_Alignof
(自C11起)
(至C23止)

alignof
(自C23起)

C++ 文档 关于 算术运算符