GNU C 手册 第六章

6 算术运算

C中的算术运算符试图尽可能与抽象的算术运算类似,但不可能做到完美。计算机中数字的取值范围有限,并且非整数值的精度也有局限。尽管如此,在大多数情况下,使用“+”进行加法,使用“-”进行减法,使用“*”进行乘法,你都不会遭遇意外状况。

每个C运算符都有一个优先级,这是它在各种运算符的语法顺序中的等级。具有最高优先级的运算符首先抓取相邻操作数;然后,这些表达式成为优先级较低的运算符的操作数。在本章中,我们描述了运算符,并给出了一些关于运算符优先级的信息。

算术运算符总是在对操作数进行操作之前提升操作数。这意味着将较窄的整数类型转换为更宽的类型。如果您只是在学习C语言,请不要担心这一点。

给定两个具有不同类型的操作数,大多数算术运算都将它们转换为它们的公共类型。例如,如果一个是int,另一个是double,则公共类型为double。(这是因为double可以表示nt的所有值,反之则不然。)

6.1 四则运算

C中的四则运算是用代数中常用的二元运算符完成的:加法(“+”)、减法(“-”)、乘法(“*”)和除法(“/”)。一元运算符“-”用于更改数字的符号。一元+运算符也存在;它不做改变的产出它的操作数。

“/”是除法运算符,但整除可能不会得到预期的结果。整除的值是一个整数,它不等于数学上的商,当这个商是分数的时候。必要时使用“%”获取相应的整数余数。浮点除法产生与数学上的商尽可能接近的值。

这些运算符使用代数语法和通常的代数优先法则,即乘法和除法在加法和减法之前完成,但可以使用括号明确指定运算符嵌套方式。它们是左结合的。因此

-a + b - c + d * e / f is equivalent to

(((-a) + b) - c) + ((d * e) / f)

6.2 整数运算

C中的每个算术运算都有两种整数变体:signed 和 unsigned。选择由操作数的数据类型决定。

C中的每个整数类型都是signed 或 unsigned。signed类型可以包含正数和负数,其中零接近范围的中间。unsigned类型只能包含非负数;其范围从零开始向上延伸。

最基本的整数类型是int,通常可以保存从-2,147,483,648到2,147,483,647的数字,以及unsigned int,通常可保存从0到4,294,967,295的数字。(这里假设int是32位宽,在实际计算机上对GNU C总是如此,但在嵌入式控制器上不一定。)

当一个算术运算有两个有符号操作数时,它进行有符号算术运算。给定两个无符号操作数,它执行无符号运算。

如果一个操作数是无符号整数,另一个是整数,则运算符将它们都视为无符号。更一般地,操作数的公共类型决定操作是否有符号。

用printf使用“%d”打印无符号算术运算的结果可能产生令人惊讶的结果。即使按上面的规则计算是用无符号算术完成的,但打印的结果可能看起来是有符号的!

解释是,加法、减法或乘法产生的位模式实际上对于有符号和无符号操作是相同的。不同之处仅在于结果的数据类型,这会影响结果位模式的解释,以及算术运算是否会溢出。

但“%d”不知道其参数的数据类型。它只看到值的位模式,并将其解释为有符号整数。要将其打印为无符号,需要使用“%u”而不是“%d”。

C中的算术从不直接对窄整数类型进行运算(位数小于整数的类型;Narrow Integers)。相反,它将它们“提升”为int。

6.3 整数溢出

当算术运算的值不在使用的数据类型范围内时,称为溢出。当它发生在整数算术中时,它是整数溢出。

整数溢出仅发生在算术运算中。根据定义,类型转换操作不会导致溢出,即使结果不能符合其新类型

有符号数字使用2的补码表示,其中最大负数缺少与之对应的正数。因此,有符号整数上的一元“-”运算符可能会溢出。

6.3.1 无符号整数溢出

C中的无符号算术运算忽略溢出;它生成对2的n次幂取模的结果,其中n是数据类型中位的数量。我们说它将结果“截断”到最低的n位。

负数对2的n次幂取模,结果为一个正数。例如,

unsigned int x = 1;
unsigned int y;

y = -x;

导致溢出,因为负数-1不能以无符号类型存储。实际结果是-1模2的n次方,比2的n次方小1。这是无符号数据类型可以存储的最大值。对于32位无符号整数,值为4294967295。

将该数字与自身相加,如下

unsigned int z;

z = y + y;

应产生8489934590;然而,这又太大,无法容纳,因此将值截断为4294967294。如果这是一个符号整数,则表示-2,这(并非巧合)等于-1+-1。

6.3.2 符号数溢出

对于有符号整数,C中溢出的结果原则上是未定义的,这意味着任何事情都可能发生。因此,C编译器可以进行优化,完全不考虑溢出情况。(由于溢出的结果在原则上是未定义的,因此不能断言这些优化是错误的。)

注意:这些优化可能会产生令人惊讶的效果。例如,

int i;
…
if (i < i + 1)
  x = 5;

可能被优化为无条件执行赋值,因为如果i+1不溢出,则if条件始终为true。

GCC提供了编译器选项来控制处理有符号整数溢出。这些选项按模块运行;也就是说,每个模块都根据编译时使用的选项进行操作。

这两个选项指定了处理有符号整数溢出的特定方式,而不是默认方式:

-fwrapv

-ftrapv

当发生有符号整数溢出时,生成信号SIGFPE。这将终止程序,除非程序处理信号。

另一个选项用于查找溢出发生的位置:

-fsanitize=signed-integer-overflow

发生有符号整数溢出时,在运行时输出警告消息。这将检查“+”、“*”和“-”运算符。这比ftrapv优先。

6.4 混合计算

在基本算术运算中混合整数和浮点数会自动将整数转换为浮点。在大多数情况下,这会给出理想的结果。但有时,转换发生在何处非常重要。

如果i和j是整数,(i+j)*2.0将它们相加为整数,然后将和转换为浮点进行乘法。如果加法的结果溢出,这并不等同于将两个整数转换为浮点,然后将它们相加。通过显式转换整数,可以得到后一种结果,如((double)i+(double)j)*2.0

几个数相加或相乘,包括一些整数和一些浮点,运算从左到右进行。因此,3.0+i+j会将i转换为浮点数,然后加3.0,然后再将j转换为浮点数并相加。您可以使用括号指定不同的顺序:3.0+(i+j)首先将i,j相加,然后将结果(转换为浮点数)和3.0相加。在这方面,C与其他语言(如Fortran)不同。

6.5 除法和余数

C中整数的除法将结果舍入为整数。结果总是四舍五入为零。

16 / 3 ⇒ 5 -16 / 3 ⇒ -5 16 / -3 ⇒ -5 -16 / -3 ⇒ 5

要获取相应的余数,请使用“%”运算符:

16 % 3 ⇒ 1 -16 % 3 ⇒ -1 16 % -3 ⇒ 1 -16 % -3 ⇒ -1

“%”具有与“/”和“*”相同的运算符优先级。

根据四舍五入的商和余数,您可以重新构造,如下所示:

int
original_dividend (int divisor, int quotient, int remainder)
{
  return divisor * quotient + remainder;
}

要进行无舍入除法,请使用浮点数。如果只有一个操作数是浮点操作数,“/”会将另一个操作数转换为浮点数。

16.0 / 3 ⇒ 5.333333333333333 16 / 3.0 ⇒ 5.333333333333333 16.0 / 3.0 ⇒ 5.333333333333333 16 / 3 ⇒ 5

浮点操作数不允许使用余数运算符“%”,因为不需要它。余数的概念对整数有意义,因为整数除法的结果必须是整数。对于浮点数,除法的结果是一个浮点数,换言之,是一个分数,它与精确结果相差很小。

标准C库中有一些函数用于计算浮点数整除的余数。

在一种特定情况下,整数除法溢出:将数据类型的最小负值除以-1。这是因为正确的结果(即相应的正数)不适合相同的位数。在一些正在使用的计算机上,这总是导致信号SIGFPE,与选项-ftrapv指定的行为相同。

除以零会导致不可预测的结果,这取决于计算机的类型,可能会导致信号SIGFPE,也可能会产生数字结果。

注意:确保程序不被零除。如果你不能证实除数不是零,则测试它是否为零,如果是,跳过除数。

6.6 数值比较

有两种比较运算符:相等运算符和次序运算符。相等比较测试两个表达式是否具有相同的值。结果是一个真值:1表示“真”,0表示“假”

a == b   /* Test for equal.  */
a != b   /* Test for not equal.  */

相等比较写为==因为普通=是赋值运算符。

次序比较测试哪个操作数大或小。它们的结果是真值。以下是C的次序比较:

a < b   /* Test for less-than.  */
a > b   /* Test for greater-than.  */
a <= b  /* Test for less-than-or-equal.  */
a >= b  /* Test for greater-than-or-equal.  */

对于任何整数a和b,ab中只有一个比较是正确的,就像在数学中一样。但是,如果a和b是特殊浮点值(不是普通数字),则这三个值都可能为假。

6.7 移位运算

移位仅在整数上定义。下面是写它的方法:

/* Left shift.  */
5 << 2 ⇒ 20

/* Right shift.  */
5 >> 2 ⇒ 1

左边的操作数是要移位的值,右边的操作数表示要移位多少位(移位计数)。左操作数被提升,因此移位不会对窄整数类型进行操作;它总是整数或更宽。移位运算符的值和被提升的左操作数有相同的类型。

6.7.1 移位产生新的比特

移位操作向数字的一端移位,同时必须在另一端生成新比特。

左移一位必须生成新的最低有效位。它总是在那里补零。这相当于乘以2的适当幂,

5 << 3 is equivalent to 5 * 222 -10 << 4 is equivalent to -10 * 222*2

右移的含义取决于数据类型是有符号的还是无符号的。对于有符号数据类型,它执行“算术移位”,通过复制符号位保持数字的符号不变。对于无符号数据类型,它执行“逻辑移位”,它总是在最高有效位处填充零。

在这两种情况下,右移一位都是除以2,舍入到负无穷大。例如

(unsigned) 19 >> 2 ⇒ 4 (unsigned) 20 >> 2 ⇒ 5 (unsigned) 21 >> 2 ⇒ 5

对于负的操作数a,a>>1不等于a/2。它们都除以2,但“/”向零舍入。

移位计数必须为零或更大。移位一个负的位数会产生机器相关的结果。

6.7.2 注意事项

警告:如果移位计数大于或等于第一个操作数的位宽度,则结果取决于机器。从逻辑上讲,“正确”值可能是-1(对于负数的右移)或0(在所有其他情况下),但它真正生成的是机器的移位指令在这种情况下所做的。因此,除非您可以证明第二个操作数不是太大,否则请编写代码在运行时检查它。

警告:永远不要依赖移位运算符以及相关算数运算符的优先级关系。没人记得住这些优先级列表,也没人能理解那些代码。请始终使用括号显式指定嵌套,如下所示:

a + (b << 5)   /* Shift first, then add.  */
(a + b) << 5   /* Add first, then shift.  */

注:根据C标准,当被移位的数为负数或在左移操作中变为负数时,有符号数的移位不能保证正确工作。然而,只有学究才有理由对此感到担忧;只有具有奇怪的移位指令的计算机才会貌似合理的算错。在GNU C中,操作总是按预期工作。

6.7.3 移位之不能说的核心机密

您可以使用移位运算符进行各种有用的破解。例如,给定由d月、m月和y年的日期指定的日期,可以将整个日期存储在单个整数日期中:

unsigned int d = 12;
unsigned int m = 6;
unsigned int y = 1983;
unsigned int date = ((y << 4) + m) << 5) + d;

要提取原来的日期、月份和年份,请结合使用移位和余数。

d = date % 32;
m = (date >> 5) % 16;
y = date >> 9;

-1<

6.8 按位运算

位运算符对整数进行操作,独立处理每个位。浮点类型不允许使用它们。

本节中的示例使用以“0b”开头的二进制常量。它们代表int类型的32位整数。

~a

一元运算符,按位取反;这将a的每个位从1更改为0或从0更改为1。

        ~0b10101000 ⇒ 0b11111111111111111111111101010111
        ~0 ⇒ 0b11111111111111111111111111111111
        ~0b11111111111111111111111111111111 ⇒ 0
        ~ (-1) ⇒ 0

记住这些很有用:~x+1 等于 -x,对于整数,~x 等于 -x-1。

a & b

二元运算符,按位“and”或“conjunction”。

`0b10101010 & 0b11001100 ⇒ 0b10001000`

a | b

二元运算符,按位“或”(“inclusive or”或“析取”)。

`0b10101010 | 0b11001100 ⇒ 0b11101110`

a ^ b

二元运算符,按位“xor”(“exclusive or”)。

要理解这些运算符对符号数的影响,请记住,所有现代计算机对负整数使用2的补码表示。这意味着数字的最高位指示符号;负数为1,正数为0。在负数中,其他位的值随着数字接近零而增加,因此0b111…111是-1,0b100…000是最负的可能整数。

警告:C为按位运算符定义了优先顺序,但永远不要依赖它。永远不要依赖于按位运算符与算术和移位运算符的优先级关系。其他程序员不记得这种优先顺序,所以总是使用括号来显式指命嵌套。

例如,假设偏移量是一个整数,指定某个表的共享内存中的偏移量,但其底部的几个位(低位表示有多少位)是特殊标志。下面是如何获得偏移量并将其添加到基址。

shared_mem_base + (offset & (-1 << LOWBITS))

由于外圆括号的存在,我们不需要知道“&”是否具有比“+”更高的优先级。因为内部括号,我们不需要知道“&”是否具有比“<<”更高的优先级。但是我们可以信赖所有一元运算符比任何二元运算符具有更高的优先级,因此我们不需要在“<<”的左操作数周围使用括号。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章