【问题标题】:C safely taking absolute value of integerC安全地取整数的绝对值
【发布时间】:2016-05-17 00:56:39
【问题描述】:

考虑以下程序 (C99):

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>

int main(void)
{
    printf("Enter int in range %jd .. %jd:\n > ", INTMAX_MIN, INTMAX_MAX);
    intmax_t i;
    if (scanf("%jd", &i) == 1)
        printf("Result: |%jd| = %jd\n", i, imaxabs(i));
}

现在据我了解,这包含易于触发的未定义行为,如下所示:

Enter int in range -9223372036854775808 .. 9223372036854775807:
 > -9223372036854775808
Result: |-9223372036854775808| = -9223372036854775808

问题:

  1. 当用户输入错误的数字时,这真的是未定义的行为吗,例如“允许代码触发任何代码路径,任何代码都被编译器喜欢”?还是其他的不完全定义?

  2. 一个学究气的程序员如何防范这种情况,不做任何标准不能保证的假设?

(有一些相关的问题,但我没有找到一个回答上面问题2的问题,所以如果你建议重复,请确保它回答了。)

【问题讨论】:

  • 请注意,输入超出范围的 int 也会导致未定义的行为。如果你想避免 UB,你不能使用任何风格的 %d 或其他整数或浮点 scanf 说明符。使用 strto 家族。而且只有一种未定义的行为,就是不好的。
  • @M.M 还有实现定义的行为,未指定但有效的值,也许还有一些其他更温和的未定义行为的替代方案。但是,我是否误解了,或者您是说用于有符号数或浮点数的 scanf 隐式包含用户可触发的 UB?参考?
  • 是的,用户可以通过输入一个超出被扫描整数范围的值来触发 UB。请参阅 C 标准中fscanf 的规范。在 C11 中是 7.21.6.2/10,“如果转换的结果不能在对象中表示,则行为未定义”。所以scanf 系列在大多数情况下不适合在生产中使用
  • 我记得很多年前在我的编程入门课上,第一个作业是编写一个程序来将两个数字相加,这两个数字可以是正数也可以是负数。我尽职尽责地编写了代码,然后意识到可能存在上溢和下溢,因此我编写了代码来检测并通知用户是否发生。我想可以做类似的事情来满足你的第二个问题。

标签: c undefined-behavior absolute-value


【解决方案1】:

如果imaxabs的结果无法表示,如果使用二进制补码可能会发生,则行为未定义

7.8.2.1 imaxabs 函数

  1. imaxabs 函数计算整数 j 的绝对值。如果结果不能 被表示,行为是未定义的。 221)

221) 最大负数的绝对值不能用二进制补码表示。

不做任何假设且始终定义的检查是:

intmax_t i = ... ;
if( i < -INTMAX_MAX )
{
    //handle error
}

(如果使用反码或符号大小表示,则无法采用此 if 语句,因此编译器可能会给出无法访问的代码警告。代码本身仍然是已定义且有效的。)

【讨论】:

  • 感谢您的好解决方案,这是一个艰难的选择,但经过深思熟虑后我仍然选择接受另一个答案,它显示了如何打印正确的结果。
  • @hyde 除了另一个答案不符合标准,而这个答案是。
  • 是否保证-INTMAX_MAX不溢出?
  • @nwellnhof 这是有保证的。请参阅我的其他评论:stackoverflow.com/questions/35251410/…
【解决方案2】:

一个学究气的程序员如何在不做任何标准不能保证的假设的情况下防止这种情况发生?

一种方法是使用无符号整数。无符号整数的溢出行为与从有符号整数转换为无符号整数时的行为一样定义良好。

所以我认为以下应该是安全的(事实证明它在一些非常晦涩的系统上被严重破坏,请参阅帖子后面的改进版本)

uintmax_t j = i;
if (j > (uintmax_t)INTMAX_MAX) {
  j = -j;
}
printf("Result: |%jd| = %ju\n", i, j);

那么这是如何工作的呢?

uintmax_t j = i;

这会将有符号整数转换为无符号整数。如果为正,则值保持不变,如果为负,则值增加 2n(其中 n 是位数)。这会将其转换为一个大数(大于 INTMAX_MAX)

if (j > (uintmax_t)INTMAX_MAX) {

如果原始数字是正数(因此小于或等于 INTMAX_MAX),则不会执行任何操作。如果原始数字为负,则运行 if 块的内部。

  j = -j;

数字被否定。否定的结果显然是负数,因此不能表示为无符号整数。所以它增加了2n

所以从代数上看,负 i 的结果是这样的

j = - (i + 2n) + 2n = -i


聪明,但这个解决方案做了假设。如果 INTMAX_MAX == UINTMAX_MAX,这将失败,这是 C 标准所允许的。

嗯,让我们看看这个(我正在阅读https://busybox.net/~landley/c99-draft.html,这显然是标准化之前的最后一个 C99 草案,如果最终标准有任何变化,请告诉我。

当 typedef 名称仅在初始 u 不存在或存在时有所不同时,它们应表示相应的有符号和无符号类型,如 6.2.5 中所述;一个实现不应该提供一个类型而不提供其对应的类型。

在 6.2.5 中我看到了

对于每个有符号整数类型,都有一个对应的(但不同的)无符号整数类型(用关键字 unsigned 指定),它使用相同的存储量(包括符号信息)并具有相同的对齐要求。

在 6.2.6.2 中我看到了

#1

对于 unsigned char 以外的无符号整数类型,对象表示的位应分为两组:值位和填充位(后者不需要任何一个)。如果有 N 个值位,每个位应表示 1 和 2N-1 之间的 2 的不同幂,以便 > 该类型的对象应能够表示从 0 到 2N-1 的值 > 使用纯二进制表示;这应称为值表示。未指定任何填充位的值。39)

#2

对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。不需要任何填充位;应该只有一个符号位。作为值位的每个位应与相应无符号类型的对象表示中的相同位具有相同的值(如果有符号类型中有 M 个值位,无符号类型中有 N 个值位,则 M

所以是的,您似乎是对的,虽然有符号和无符号类型必须具有相同的大小,但对于无符号类型比有符号类型多一个填充位似乎是有效的。


好的,根据上面的分析,我在第一次尝试中发现了一个缺陷,我写了一个更偏执的变体。这与我的第一个版本相比有两个变化。

我使用 i (uintmax_t)INTMAX_MAX 来检查负数。这意味着即使 INTMAX_MAX == UINTMAX_MAX,算法也会对大于或等于 -INTMAX_MAX 的数字产生正确的结果。

我添加了对错误情况的处理,其中 INTMAX_MAX == UINTMAX_MAX、INTMAX_MIN == -INTMAX_MAX -1 和 i == INTMAX_MIN。这将导致我们可以轻松测试的 if 条件中的 j=0。

从C标准的要求可以看出,INTMAX_MIN不能小于-INTMAX_MAX -1,因为符号位只有一个,值位的个数必须等于或小于对应的无符号类型。根本没有剩下的位模式来表示较小的数字。

uintmax_t j = i;
if (i < 0) {
  j = -j;
  if (j == 0) {
    printf("your platform sucks\n");
    exit(1);
  }
}
printf("Result: |%jd| = %ju\n", i, j);

@plugwash 我认为 2501 是正确的。例如,-UINTMAX_MAX 值变为 1:(-UINTMAX_MAX + (UINTMAX_MAX + 1)),并且不会被您的 if 捕获。 – hyde 58 分钟前

嗯,

假设 INTMAX_MAX == UINTMAX_MAX 并且 i = -INTMAX_MAX

uintmax_t j = i;

在这条命令之后 j = -INTMAX_MAX + (UINTMAX_MAX + 1) = 1

如果 (i

i 小于零,所以我们在 if 中运行命令

j = -j;

在这条命令之后 j = -1 + (UINTMAX_MAX + 1) = UINTMAX_MAX

这是正确的答案,因此无需在错误情况下将其捕获。

【讨论】:

  • 我选择接受这一点,因为即使对于 INTMAX_MIN 值,这实际上也会显示正确的结果。
  • 很聪明,但这个解决方案做了假设。如果 INTMAX_MAX == UINTMAX_MAX 会失败,这是 C 标准所允许的。
  • @2501 这可能吗?我的印象是,这可能是错误的,将有符号转换为相应的无符号类型不能丢失位,因此如果有符号值为负,则生成的无符号值必须大于有符号最大值。
  • @hyde 段落 C11 6.2.6.2, p2 说无符号整数中的值位数可能与相应的有符号整数中的位数相同。(注意: M
  • @hyde 1. 是的,只是错误的结果。 2. 我不知道。 :),我认为这更多是理论上的问题。您始终可以为这种不太可能的情况添加#ifdef,并根据需要使用此代码。
【解决方案3】:

在二补系统上,获得最大负值的绝对数确实是未定义的行为,因为绝对值会超出范围。由于 UB 在运行时发生,因此编译器无能为力。

防止这种情况的唯一方法是将输入与该类型的最负值(您显示的代码中的INTMAX_MIN)进行比较。

【讨论】:

  • 这涵盖了二进制补码(并且只丢失了一个有效数字作为一个补码),但我发现无论整数表示如何都能以可靠的方式检测到它是一个好问题(我认为标准不限于只有一个和两个的补码,尽管我必须承认我从未检查过)
  • @JoachimIsaksson:标准限制为以下三个选项之一:二进制补码、二进制补码和符号幅度。 (C99,6.2.6.2,第 2 段。)
  • @JoachimIsaksson if( i &lt; -INTMAX_MAX ) 适用于任何代表。尽管您可能会收到关于补码和符号大小的编译器警告,因为无法采用该语句。我不知道如何防止这种情况。
  • "因为 UB 发生在运行时,所以编译器无能为力。"编译器可以生成执行运行时检查的代码;-)
  • @skyking 根据 C,对于任何有符号类型,-MAX 必须是可表示的:C11 6.2.6.2, p2,因为有符号整数必须是这三种表示之一,保证这些范围。有符号整数的最大值不可能大于绝对最小值。
【解决方案4】:

因此,计算整数的绝对值会在一种情况下调用未定义的行为。实际上,虽然可以避免未定义的行为,但在一种情况下不可能给出正确的结果。

现在考虑一个整数乘以 3:这里有一个更严重的问题。此操作在所有情况下的 2/3 中调用了未定义的行为!对于所有 int 值 x 的三分之二,找到一个值为 3x 的 int 是不可能的。这是一个比绝对值问题严重得多的问题。

【讨论】:

    【解决方案5】:

    你可能想使用一些小技巧:

    int v;           // we want to find the absolute value of v
    unsigned int r;  // the result goes here 
    int const mask = v >> sizeof(int) * CHAR_BIT - 1;
    
    r = (v + mask) ^ mask;
    

    这在INT_MIN &lt; v &lt;= INT_MAX 时效果很好。在v == INT_MIN 的情况下,它仍然是INT_MIN不会导致未定义的行为

    您还可以使用按位运算在反码和符号幅度系统上处理此问题。

    参考:https://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs

    【讨论】:

    • 我相信右移一个有符号整数本身就是 UB。
    • @abligh 如果有符号整数为负数,则由实现定义。这个答案也假设没有填充位。
    • 根据bit hacks文件,这个无分支的方案是依赖2的补码,但是在美国也申请了专利,这也可能是个问题。
    【解决方案6】:

    据此http://linux.die.net/man/3/imaxabs

    备注

    试图取最大负整数的绝对值没有定义。

    要处理全部范围,您可以在代码中添加类似这样的内容

        if (i != INTMAX_MIN) {
            printf("Result: |%jd| = %jd\n", i, imaxabs(i));
        } else {  /* Code around undefined abs( INTMAX_MIN) /*
            printf("Result: |%jd| = %jd%jd\n", i, -(i/10), -(i%10));
        }
    

    编辑:由于 abs(INTMAX_MIN) 无法在 2 的补码机器上表示,因此可表示范围内的 2 个值在输出时连接为字符串。 使用 gcc 测试,虽然 printf 需要 %lld,因为 %jd 不是受支持的格式。

    【讨论】:

    • 什么是imax(i+1)+1,它应该实现什么?
    • 我打算写 imaxabs。,我会解决它。它应该给出 INTMAX_MIN 绝对值的正确结果。只是想在这里开箱即用
    • imaxbas(i+1)+1 不是一种解决方法,它只是将未定义的行为推入第二次添加。 imaxabs(INTMAX_MIN) 在 2 的补码机器上未定义的根本原因是无法表示正确的结果。再多的加一也不会改变这个基本事实。
    • OK,稍有变化,imaxabs(INTMAX_MIN+1) 可以用 2 的补码机来表示。对吗?现在你把它变成一个字符串并增加 '\0' 之前的最后一个字符。
    • 使用 div 和 mod 将 INTMAX_MIN 置于可否定范围内更容易
    【解决方案7】:
    1. 当用户输入错误的数字时,这真的是未定义的行为吗,例如“代码被允许触发任何代码路径,其中任何代码都被编译器喜欢”?还是其他一些未完全定义的味道?

    程序的行为只是未定义的,当错误的数字被成功输入并传递给 imaxabs() 时,它在典型的 2 的补码系统上返回 -ve 结果,如您所见。

    在这种情况下,这是未定义的行为,如果 ALU 设置状态标志,则还允许实现以溢出错误终止程序。

    C 中“未定义行为”的原因是编译器编写者不必防范溢出,因此程序可以更高效地运行。虽然每个 C 程序使用 abs() 试图杀死你的第一个出生的人都符合 C 标准,但仅仅因为你用一个太 -ve 的值调用它,将这样的代码写入目标文件将是不正当的。

    这些未定义行为的真正问题在于,优化编译器可以排除幼稚的检查,因此代码如下:

    r = (i < 0) ? -i : i;
    if (r < 0) {   // This code may be pointless
        // Do overflow recovery
        doRecoveryProcessing();
    } else {
        printf("%jd", r);
    }
    

    由于编译器优化器可以推断负值被否定,它原则上可以确定 (r 总是为假,因此捕获问题的尝试失败。

    1. 一个学究气的程序员如何在不做任何标准无法保证的假设的情况下防止这种情况发生?

    到目前为止,最好的方法是确保程序在有效范围内工作,因此在这种情况下验证输入就足够了(不允许 INTMAX_MIN)。 打印 abs() 表格的程序应该避免使用 INT*_MIN 等。

        if (i != INTMAX_MIN) {
            printf("Result: |%jd| = %jd\n", i, imaxabs(i));
        } else {  /* Code around undefined abs( INTMAX_MIN) /*
            printf("Result: |%jd| = %jd%jd\n", i, -(i/10), -(i%10));
        }
    

    似乎是通过伪造写出 abs(INTMAX_MIN),从而使程序能够兑现对用户的承诺。

    【讨论】:

      猜你喜欢
      • 2016-11-18
      • 2011-12-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-29
      相关资源
      最近更新 更多