【问题标题】:Bit manipulation, return 0 if x != 0, or nonzero otherwise位操作,如果 x != 0 则返回 0,否则返回非零
【发布时间】:2016-04-26 05:36:02
【问题描述】:

我正在编写一个函数is_zero,如果x != 0 应该返回0,否则返回非零。我不允许使用任何常量。例如,x == 0 是不允许的。 (也不允许使用 == 运算符)

我被允许使用的唯一运算符是=~^*(取消引用)、&|<<>>+ .

我现在编写函数的方式是,如果x != 0,它将返回0,但即使在x == 0 时,它仍然返回0,这是不应该的。我尝试了各种组合,但考虑到限制,这个家庭作业问题似乎是不可能的。我在这里发帖是最后的努力。

谁能告诉我如何让我的代码在x == 0 时返回0 以外的东西,而在x != 0 时仍返回0

int is_zero(int x) {
    return (x ^ x);
}

【问题讨论】:

  • 写下一些简单的非零值的位模式,并进行试验。例如,如果你有一个非零值,如果你使用按位和本身会发生什么?如果你有按位和相反(使用~ 运算符)本身会发生什么?当您在非零值上使用 ~ 时会发生什么?值为零?
  • return (x ^ ~x); 会起作用。
  • @DavidC.Rankin,该解决方案似乎不起作用。
  • 如果不想使用==,不如直接使用if (x) { return 0;} else { return 1;}这样的条件
  • 非常厌倦“不使用 A、B、C 怎么办?” “(使用 D 发布的答案)”“哦,也不能使用 D”“(使用 E 发布的答案)”“哦,也不能使用 E”等等......

标签: c bitwise-operators


【解决方案1】:

如果您希望代码在不假设int 和表示的特定大小的情况下工作,我认为不可能解决。但这适用于 32 位整数和二进制补码表示:

int is_zero(int x) {
    int zero = (x^x);
    int one = ~(~zero + ~zero);
    int five = one + one + one + one + one;
    int thirtyone = (one << five) + ~zero;
    return ~((x >> thirtyone) | ((~x + one) >> thirtyone));
}

它使用多个赋值来构造常量,但如果需要,代码可以折叠成一个表达式。

工作原理

如果x 为负数,(x &gt;&gt; thirtyone) 为 -1,否则为 0。 同样,如果x 为正,(~x + one) &gt;&gt; thirtyone 为 -1,否则为 0。

如果x 为零,则这两个表达式的按位or0,否则为-1。如果x 为零,则按位~ 给出-1,否则为0。

(几乎)字长独立解决方案

它不是完全独立于字长的,但可以扩展上述解决方案以适用于 16、32 和 64 位整数(尽管仍取决于二进制补码表示)。代码注意一次不要移动超过 15 位(否则如果 int 是 16 位,结果是未定义的行为):

int is_zero(int x) {
    int zero = (x^x);
    int one = ~(~zero + ~zero);
    int four = one + one + one + one;
    int k15 = (one << four) + ~zero;
    return ~((x >> k15 >> k15 >> k15 >> k15 >> k15) |
             ((~x + one) >> k15 >> k15 >> k15 >> k15 >> k15));
}

【讨论】:

  • 查看 cmets。我问是否可以假设特定的字长,答案是否定的,它必须适用于任何字长。否则很容易,正如您发现的那样。
  • @TomKarzes 我认为这将是尽可能好的答案——我相信如果你不假设一个固定的字长,这是不可能的。我将修改我的答案以包含此内容。
  • 你可能是对的——我还不确定。生成任何固定常数都是微不足道的,如果您知道字长,那么它是一个简单的解决方案。在不知道字长的情况下,如果事实上它是可能的,那就更难了。
  • 我知道如何用固定字长解决它,正如我所说的。我没有写出来是因为我不确定是否可以假设一个固定的字长。你的解决方案对我来说很明显。我仍在研究一个通用的解决方案,我现在认为这可能是可能的。这才是真正的挑战。到目前为止,还没有人发布符合 OP 既定目标的解决方案。
【解决方案2】:

请注意,这建立在@Paul Hankin 关于如何将 0 和 1 作为常量的回答之上。 (如果允许您使用“/”运算符,则 (x/x) 将更容易获得 1。因此,一种理论上的方法可能是

  • 在@PaulHankin 的回答中获取常量 0 和 1(很好,顺便说一句)
  • 使用这些“常数”,将 x 向左移动所有可能的位位置,对结果进行或运算(将原始中的任何“1”移动到任何可能的位位置,最后给出 -1 (0xffff)。因为我们没有t 似乎知道整数的大小,做 64 或 128(或 1000)次或更频繁,以确保安全并涵盖所有合理的int 大小。(那行可能会变得有点长...... ) 添加太多的班次不会有坏处。

x = x | x &lt;&lt; one | x &lt;&lt; one+one | x &lt;&lt; one + one + one ....

这实际上可能会在 UB 中结束,如果移位大于实际字宽,但通过写入来修复(感谢 @PaulHankin 的评论)

x = x | x &lt;&lt; one | x &lt;&lt; one &lt;&lt; one | x &lt;&lt; one &lt;&lt; one &lt;&lt; one ....

  • 如果 x 最初是 0,那么现在 x 中就会有 0。如果它设置了任何位(即与 0 不同),则您有任何位高于最初设置的一组。
  • 做同样的事情向右移动,你将得到 0xffff(或任何你的字长)或 0,然后
  • 将上面的结果与 -1 进行异或(作为常数从 0 - 1 获得) - 这将导致原始 0 为 -1,其他任何东西为 0,最后返回。

我很确定我不想编写那个程序。它可能很容易超出编译器的任何合理行长。

【讨论】:

  • 这仍然不是很笼统。它只是说它适用于任何“合理”的字长限制。
  • 在某些时候x &lt;&lt; one + one + ... + one 会导致未定义的行为(向左移动整数宽度或更多是未定义的行为)。但你可以改写x &lt;&lt; one &lt;&lt; one &lt;&lt; ... &lt;&lt; one 来解决这个问题。
  • @TomKarzes 它还使用了不合理的班次。你插入的越多,它就越能涵盖“不合理”的字长。但同意,我更喜欢你的解决方案。
  • 但无论如何,整个事情都是不合理的尝试;)
【解决方案3】:

事实证明,有一个通用的解决方案,它不依赖于字长,甚至不依赖于二进制补码算法。这里是:

int is_zero(int x)
{
    unsigned    y   = x;
    unsigned    c0  = y^y;
    unsigned    c1  = ~(~c0 + ~c0);
    unsigned    c1h = ~c0 ^ (~c0 >> c1);
    unsigned    y2  = y + ~c0;
    unsigned    t1  = y ^ y2;
    unsigned    t2  = t1 & y2;
    unsigned    s   = t2 & c1h;
    int         r   = s >> c1;

    return r;
}

请注意,一切都是以无符号形式完成的,这对于避免溢出问题以及强制右移到零填充是必要的。我将参数和返回值保留为有符号整数,第一个和最后一个赋值只是改变有符号/无符号行为(最后的移位仅用于避免溢出 - 请参阅下面的评论)。

解决方案实际上非常简单。如前所述,恒定生成是微不足道的。零是通过与自身进行异或运算来获得的。全 1 是它的按位补码。一个是所有 1 的异或,所有 1 都向左移动一个。

到目前为止,这一切都是微不足道的。棘手的部分是让它不管字长如何都能正常工作。为此,它构造一个值,如果 x 非零,则其高位为 0,如果 x 为零,则其高位为 1。然后它用一个常量来屏蔽它,该常量是高位位置中的单个 1,它是由所有 1 的 xor'd 和所有 1 右移 1 构成的。移位保证为零填充(而不是符号扩展) 因为该值是无符号的。

请注意,s 在高位位置上要么为零要么为一。最终返回值rs 右移一位,以避免在分配回有符号整数时溢出。此修复由 Paul Hankin 提出。这使得最终的返回值要么为零,要么为零,然后是一,然后是全零。

如果你想避免函数开头和结尾的有符号和无符号之间的隐式转换,你可以使用联合来给值取别名:

int is_zero(int x)
{
    union {int s; unsigned u;} su;
    su.s = x;
    unsigned    y   = su.u;
    unsigned    c0  = y^y;
    unsigned    c1  = ~(~c0 + ~c0);
    unsigned    c1h = ~c0 ^ (~c0 >> c1);
    unsigned    y2  = y + ~c0;
    unsigned    t1  = y ^ y2;
    unsigned    t2  = t1 & y2;
    unsigned    s   = t2 & c1h;
    su.u = s;
    int         r = su.s;

    return r;
}

在这种情况下,s 的最后移位是不必要的,返回值要么是零,要么是一个后跟全零。请注意,C90 标准不允许混合代码和声明,因此如果这是一个问题,您必须将声明与赋值分开,但最终结果将是相同的。

【讨论】:

  • 如果允许转换为其他类型,那真的很简单:return ~(bool)x;
  • @Paul 这不是表示更改。它只是使用无符号运算符。它可以使用指针来获取无符号变量。这个问题似乎允许这样做。如果允许,它也可以使用联合。可以说,该函数无论如何都应该采用并返回一个无符号的。不清楚是否实际需要签名参数和返回值。
  • 最后一行应该是:int r = s &gt;&gt; c1; -- 写成它是未定义的行为。
  • @Paul Hm,我可能错了。我没有可以检查的参考。不过你是对的,右移一位可以保证它是安全的。
  • s 不能表示为int,所以它绝对是 UB 铸造它。你应该只更新你的代码——你不需要在修复的答案中给出信用。
【解决方案4】:

@TomKarzes 回答的小优化:

int is_zero2(int x)
{
    unsigned y   = x;             // use (unsigned) x
    unsigned c0  = y ^ y;         // = 0
    unsigned c1  = ~(~c0 + ~c0);  // = 1
    unsigned c1h = ~(~c0 >> c1);  // = mask for sign bit
    unsigned t2  = ~(y | -y);     // mask for bits below (not including) lowest bit set
    unsigned s   = t2 & c1h;      // sign bit: 0 = (x != 0), 1 = (x == 0)
    int      r   = s >> c1;       // prevent overflow
    return r;
}

【讨论】:

    猜你喜欢
    • 2019-03-18
    • 2015-01-18
    • 2020-07-06
    • 1970-01-01
    • 2015-07-11
    • 2023-04-11
    • 2020-01-07
    • 2018-07-30
    • 1970-01-01
    相关资源
    最近更新 更多