【问题标题】:Positive integers that multiply to a negative value乘以负值的正整数
【发布时间】:2015-12-31 11:01:05
【问题描述】:

我正在通过阅读 Stroustrup 的“使用 C++ 的原理和实践”来学习 C++。

在关于前置条件和后置条件的部分中,有以下函数示例:

int area(int length, int width)
// calculate area of a rectangle;
// pre-conditions: length and width are positive
// post-condition: returns a positive value that is the area
{
    if (length<=0 || width <=0) 
        error("area() pre-condition");

    int a = length*width;

    if (a<=0) 
        error("area() post-condition");

    return a;
}

让我困惑的是这段代码的任务:

找到一对值使得这个版本的前提条件 面积成立,但后置条件不成立。

是否存在前置条件可以但后置条件不行的整数值?

【问题讨论】:

标签: c++ integer


【解决方案1】:

是否存在前置条件可以但后置条件不行的整数值?

是的,有许多输入值可能导致后置条件失败。如果例如

int a = length*width;

溢出正的int 范围(std::numeric_limits&lt;int&gt;::max()),编译器实现在这种情况下产生一个负值。


正如其他人在他们的回答中指出的那样,length*width 超出]0-std::numeric_limits&lt;int&gt;::max()[ 的范围的情况实际上是未定义的行为,并且后置条件只是无用,因为可能需要为a 预期任何值。

解决这个问题的关键点,在@Deduplicatoranswer中给出,前置条件需要改进。


作为 Bjarne Stroustrup 的推理举个例子:

我假设他想指出这种未定义的行为可能会导致后置条件中出现意外的负值,并且对于与前置条件检查的幼稚假设会导致令人惊讶的结果。

【讨论】:

  • 需要更正。 even 如果溢出产生正值,则后置条件不成立。
  • @JoseAntonioDuraOlmos:正值如何&lt;=0,请告诉我?
  • @LightnessRacesinOrbit 实际上正值不是 是区域 的正值”。如果乘法溢出为正整数,则结果是 NOT 该区域。
  • @LightnessRacesinOrbit 例如,46000*46000 是 2116000000,这是在界限内。将它提高到 47000*47000,你会得到 -2085967296,超出范围。继续上升到 66000,你会得到 61032704,这是正数,但不是 4356000000,即真正的区域。
  • @JoseAntonioDuraOlmos THX 指出。后置条件可能无法涵盖所有​​情况,但这主要不是问题。
【解决方案2】:

不,在标准 C++ 定义的行为范围内,没有任何值会违反后置条件。但是,有些值仍然可以使函数行为不正确,即值太大以至于它们的乘积不适合整数。尝试传递 200'000 和 15'000。

由于大多数编译器实现 C++ 的方式,您可能会看到违反了后置条件,但您实际观察到的是由于整数溢出导致的未定义行为。

【讨论】:

    【解决方案3】:

    答案是他的前置条件检查不完整。尽管它太严格了。
    他没有包括检查产品可以被代表而不是导致 UB:

    int area(int length, int width) {
        // calculate area of a rectangle
        assert(length >= 0 && width >= 0 && (!width
            || std::numeric_limits<int>::max() / width >= length));
        int a = length * width;
        assert(a >= 0); // Not strictly neccessary - the math is easy enough
        return a;
    }
    

    【讨论】:

      【解决方案4】:

      我想到的是签名溢出。这是未定义的行为,但可能会产生负值。
      试试std::numeric_limits&lt;int&gt;::max()2

      【讨论】:

        【解决方案5】:

        是的,如果假设您使用的是 16 位计算机,那么 int = 2B 最大值 +32767 如下

        {
            length = 500, width = 100;
            if (length<=0 || width <=0) error("area() pre-condition");
            int a = length*width;   // a = 500 * 100 = 50000
            if (a<=0) error("area() post-condition");
            return a;
        }
        

        现在最终值将是a = -17233,因为它进入了 -ve 值。 所以第二个条件为假。

        这一切都取决于范围。

        【讨论】:

        • 如果这是一台 32 位计算机并且您使用的是int,则最大值不应为32,767,而是2,147,483,647
        • 我在 32 位 comp 和 Turbo C3 编译器上试过这个。正如我所提到的,它取决于环境
        • 有趣的是,DOSBox 中模拟的 16 位编译器在印度仍然用于教授 C++。自 1990 年代初以来,这种环境到底有多重要? 25 年前??
        • @JameyD:对于什么是“32 位”或“64 位”计算机没有统一的定义,计算机的“位数”与计算机大小之间没有一致的关系输入int。 32 位计算机通常具有 32 位 int,但它可能是 16 位,尽管这不太可能。 64 位计算机通常具有 32 位 int
        • @kamaldeepsinghbhatia:哪种数据类型? int 的大小取决于编译器,并且通常由 ABI 规定。 int32_t 的大小始终是 32 位,通常是 4 个字节。 sizeof (char) == 1 根据定义。
        【解决方案6】:

        INT_MAX 在用于所有符合标准的编译器的长度和宽度时将无法满足后置条件。

        有人可能会说,由于标准保证INT_MAX>=32767,那么INT_MAX*INT_MAX 将始终大于INT_MAX,因此无法在int 中表示,而int 被定义为能够保持INT_MAX的最大值。
        这是一个很好的论点,而且它实际上是最常发生的事情,大多数编译器都会溢出。

        但要涵盖所有基础,我们需要注意C++ standard 声明:

        3.4.3
        1 未定义的行为
        行为,在使用不可移植或错误程序结构或错误数据时,本国际标准对此没有要求

        2 注意 可能的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现 (发布或不发布诊断消息),终止翻译或执行(发布诊断消息)。

        3 示例未定义行为的一个示例是整数溢出行为。

        因此,这比没有为该区域获得正确的价值要严重一些。当INT_MAX 同时用于长度和宽度时(或任何其他具有不可表示结果的组合),无法保证编译后的程序会做什么。任何事情都有可能发生;从可能的溢出或崩溃到不太可能的磁盘格式。

        【讨论】:

        【解决方案7】:

        溢出值类型的位表示的值的乘法是未定义的,因为溢出的位数可能大于 1。因此,您最终可能会得到一个正或负符号位,并且丢失的位数是可变的。

        示例 1:INT_MAX * 2:结果是正确的,但由于高位表示符号位,因此未更正其类型。

        示例 2:INT_MAX * 4:溢出丢失 1 位,符号位不正确,如上例。

        示例 3:(INT_MAX + 1) * 2 = 0:由于所有设置位溢出但符号正确。

        我使用 8 位二进制表示以使其更易于阅读,以说明为什么会发生这种情况。

        0111 1111              // Max positive signed value
        +1
        1000 0000              // Sign bit set but binary value is correct
        *2
        0000 0000              // Upper bit is lost due to overflow
        

        在这种情况下,既有软溢出,也没有丢失信息,但表示不正确。结果中不再存在该位的硬溢出。

        溢出的区别在于如何检测溢出。通常硬溢出将被硬件检测到,并且只需要很少的工作就可以让软件处理。然而,软件溢出可能需要软件显式测试溢出条件,因为硬件通常无法识别整数数学运算中的符号位。

        运行时库如何处理溢出取决于库。大多数人会忽略它,因为这样做更快,而其他人可能会抛出错误。 未定义的行为并不意味着它可能会格式化您的磁盘。数学运算的结果不会以任何方式改变代码流,除非代码逻辑指示。它可以忽略溢出或尝试以某种方式处理它。如果代码或硬件试图处理问题,该标准并未规定采用何种方法。

        基本上三个有 3 种可能发生的事情。
        1.溢出被忽略,返回值无效。
        2.溢出被运行时库忽略,但硬件抛出错误也被忽略,导致运行代码硬故障。在这种情况下,完全由操作系统决定接下来会发生什么。发疯和破坏数据会导致糟糕的设计决策。
        3. 溢出由运行时库处理,它必须确定最佳的处理方式。通常这意味着让代码有机会捕捉错误并处理它,或者尽可能优雅地关闭代码。

        【讨论】:

          【解决方案8】:

          从 C++11 开始,您可以测试一个布尔值:

          std::numeric_limits<int>::is_modulo
          

          如果此值为true,则有符号算术以环绕方式运行,并且在原始代码中没有未定义的行为。确实可以产生负值,所以原代码中的测试是有意义的。

          进一步讨论is_modulosee here

          【讨论】:

            【解决方案9】:

            所以基本上,乘法中的正值......导致正值,但这些可能实际上不适合结果类型

            你的前置条件不完整,你的后置条件也无效。您不仅可以获得负值,还可以获得小于输入值的正值,您所需要的只是足够大的值作为输入,以使环绕超过零,即 long-wrap-around em> .

            你可以使用this

            bool multiplication_is_safe(uint32_t a, uint32_t b) {
                size_t a_bits=highestOneBitPosition(a), b_bits=highestOneBitPosition(b);
                return (a_bits+b_bits<=32);
            }
            

            为了防止溢出,但是你会想要对 FALSE-Positives 进行额外的检查。

            或者,如果性能不是什么大问题,您可以使用MPZ 库。如果性能是一个问题,并且您想为具有溢出标志的 CPU 编写程序集,那么您可以这样做。您的编译器也有可能为您进行检查,例如G++ 有fno-strict-overflow 或者可能在前置条件检查后转换为unsigned int

            无论如何,这些解决方案中的大多数实际上并不能真正解决您的问题,即结果将是 foo,即您可能会得到比实际结果更小的面积。

            因此,您唯一安全的选择是只允许此处显示的安全乘法,这样做您会错过一些东西,但不会太多。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-03-20
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-10-01
              相关资源
              最近更新 更多