【问题标题】:C standard on negative zero (1's complement and signed magnitude)负零上的 C 标准(1 的补码和有符号幅度)
【发布时间】:2011-10-28 05:44:52
【问题描述】:

所有这些功能在我的机器上都给出了预期的结果。他们都在其他平台上工作吗?

更具体地说,如果 x 在 1 的补码机器上具有位表示 0xffffffff 或在有符号幅度机器上具有 0x80000000,那么标准对 (unsigned)x 的表示有何规定?

另外,我认为 v2、v2a、v3、v4 中的(无符号)强制转换是多余的。这是正确的吗?

假设 sizeof(int) = 4 和 CHAR_BIT = 8

int logicalrightshift_v1 (int x, int n) {

    return (unsigned)x >> n;
}

int logicalrightshift_v2 (int x, int n) {

    int msb = 0x4000000 << 1;
    return ((x & 0x7fffffff) >> n) | (x & msb ? (unsigned)0x80000000 >> n : 0);
}

int logicalrightshift_v2a (int x, int n) {

    return ((x & 0x7fffffff) >> n) | (x & (unsigned)0x80000000 ? (unsigned)0x80000000 >> n : 0);
}

int logicalrightshift_v3 (int x, int n) {

    return ((x & 0x7fffffff) >> n) | (x < 0 ? (unsigned)0x80000000 >> n : 0);
}

int logicalrightshift_v4 (int x, int n) {

    return ((x & 0x7fffffff) >> n) | (((unsigned)x & 0x80000000) >> n);
}

int logicalrightshift_v5 (int x, int n) {

    unsigned y;
    *(int *)&y = x;
    y >>= n;
    *(unsigned *)&x = y;
    return x;
}

int logicalrightshift_v6 (int x, int n) {

    unsigned y;
    memcpy (&y, &x, sizeof (x));
    y >>= n;
    memcpy (&x, &y, sizeof (x));
    return x;
}

【问题讨论】:

  • 为什么不直接除以 (2^n) 让编译器进行优化?
  • 假设 2 的补码:logicalrightshift (-1,1) 应该是 0x7fffffff,但 -1 / 2 = 0
  • @user1016492:如果您的机器上的UINT_MAX 等于0xffffffff,那么(unsigned)-1 &gt;&gt; 1 保证为0x7fffffff,不管有符号数字使用什么表示。
  • @Mat: 2^n 是 C 中的异或运算...
  • @Deitrich:也许这就是他不使用代码标记的原因。当印刷限制阻止上标时,使用 ^ 进行求幂是常见用法。在这种情况下,读者必须应用上下文来消除歧义。我认为 Mat 的评论在上下文中已经足够清楚了。

标签: c standards bit-shift zero negative-number


【解决方案1】:

如果 x 在 1 上具有位表示 0xffffffff 补充机器或 0x80000000 在有符号幅度机器上什么 标准是否说明了 (unsigned)x 的表示?

unsigned 的转换是根据 指定的,而不是表示形式。如果您将-1 转换为unsigned,您总是 得到UINT_MAX(所以如果您的unsigned 是32 位,您总是得到4294967295)。无论您的实现使用何种符号数字表示,都会发生这种情况。

同样,如果您将-0 转换为unsigned,那么您总是得到0-0 在数值上等于 0。

请注意,支持负零不需要一个补码或符号幅度实现;如果没有,那么访问这样的表示会导致程序具有未定义的行为。

逐一检查你的函数:

int logicalrightshift_v1(int x, int n)
{
    return (unsigned)x >> n;
}

对于x 的负值,此函数的结果将取决于UINT_MAX,如果(unsigned)x &gt;&gt; n 不在int 的范围内,则将进一步由实现定义。例如,logicalrightshift_v1(-1, 1) 将返回值 UINT_MAX / 2,而不管机器对有符号数字使用什么表示。

int logicalrightshift_v2(int x, int n)
{
    int msb = 0x4000000 << 1;
    return ((x & 0x7fffffff) >> n) | (x & msb ? (unsigned)0x80000000 >> n : 0);
}

几乎所有关于此的内容都可以由实现定义。假设您尝试在 msb 中创建一个值,符号位为 1,值位为 0,您不能通过使用移位来实现此操作 - 您可以使用 ~INT_MAX,但允许未定义在不允许负零的符号幅度机器上的行为,并允许在二进制补码机器上给出实现定义的结果。

0x7fffffff0x80000000 的类型将取决于各种类型的范围,这将影响此表达式中其他值的提升方式。

int logicalrightshift_v2a(int x, int n)
{
    return ((x & 0x7fffffff) >> n) | (x & (unsigned)0x80000000 ? (unsigned)0x80000000 >> n : 0);
}

如果您创建的unsigned 值不在int 的范围内(例如,给定32 位int,值> 0x7fffffff),则return 语句中的隐式转换会产生一个实现-定义的值。这同样适用于 v3 和 v4。

int logicalrightshift_v5(int x, int n)
{
    unsigned y;
    *(int *)&y = x;
    y >>= n;
    *(unsigned *)&x = y;
    return x;
}

这仍然是实现定义的,因为未指定int 表示中的符号位是否对应于unsigned 表示中的值位或填充位。如果它对应于一个填充位,它可能是一个陷阱表示,在这种情况下,行为是未定义的。

int logicalrightshift_v6(int x, int n)
{
    unsigned y;
    memcpy (&y, &x, sizeof (x));
    y >>= n;
    memcpy (&x, &y, sizeof (x));
    return x;
}

适用于 v5 的相同 cmets 也适用于此。

另外,我认为 v2、v2a、v3、v4 中的(无符号)强制转换是多余的。是 这个对吗?

这取决于。作为一个十六进制常量,0x80000000 将具有类型int,如果该值在int 的范围内;否则unsigned 如果该值在unsigned 的范围内;否则long 如果该值在long 的范围内;否则unsigned long(因为该值在unsigned long 的最小允许范围内)。

如果您希望确保它具有无符号类型,则在常量后面加上U,到0x80000000U


总结:

  1. 将大于 INT_MAX 的数字转换为 int 会给出实现定义的结果(或者实际上,允许引发实现定义的信号)。

  2. 将超出范围的数字转换为unsigned 是通过重复加或减UINT_MAX + 1 完成的,这意味着它取决于数学,而不是表示。

  3. 将否定的int 表示检查为unsigned 是不可移植的(不过,肯定的int 表示是可以的)。

  4. 通过使用按位运算符生成负零并尝试使用结果值是不可移植的。

如果您想要“逻辑转换”,那么您应该在任何地方都使用无符号类型。有符号类型旨在处理是重要的算法,而不是表示。

【讨论】:

  • @caf:我不明白为什么~INT_MAX 会在 2 的补码上给出实现定义结果。 INT_MAX 是一个整数常量。 ~ 运算符翻转所有位。即使有填充位,结果也不会很好地定义吗?就此而言,它是否也能在 1 的补码机器上得到很好的定义。
  • @user1016492: INT_MAX 的符号位为 0,所有的值位为 1,所以 ~INT_MAX 的符号位为 1,所有的值位为 0。这不是代表正常值所必需的2s 补码实现的值,这意味着 ~INT_MAX 可能超出范围。这实际上使它成为未定义的行为而不是定义的实现。
  • Reference: 6.2.6.2/2: "implementation-defined ... 符号位为 1 且所有值位为零的值(对于前两个)... 是陷阱表示还是正常值”。 “对于前两个”包括二进制补码,它是列出的三个中的第二个。因此,这是实现定义行为是否未定义的情况之一:实现可以做任何事情,只要它记录了~INT_MAX 是一个陷阱表示。但是,如果它记录了这是一个正常值,则定义了行为。因此,严格遵守的程序不能使用它。
  • @caf 对别名的一些澄清:v5 不属于以下类别:“对象的存储值只能由具有以下类型之一的左值表达式访问:... , 对象的有效类型对应的有符号或无符号类型, ..."
  • @user1016492:你是对的,所以 v5 的运行方式与 v6 完全相同(我已经更正了答案)。
【解决方案2】:

如果您遵循这个词的标准,那么这些都不能保证在所有平台上都是相同的。

在 v5 中,您违反了严格别名,这是未定义的行为。

在 v2 - v4 中,您已经签署了右移,这是实现定义的。 (详情见 cmets)

在 v1 中,您已签署无符号强制转换,这是在数字超出范围时定义的实现。

编辑:

考虑到以下假设,

v6 可能确实有效:

  • 'int' 是 2 或 1 的补码。
  • unsignedint 的大小完全相同(字节和位都一样,并且是密集打包的)。
  • unsigned 的字节序与int 的字节序一致。
  • 填充和位布局相同:(有关详细信息,请参阅 caf 的评论。)

【讨论】:

  • 非负值的有符号整数的右移是否定义明确?
  • 那是因为你取消了两个指向同一个内存位置的不同类型的指针。 (char* 除外)
  • 我认为您对非负右移的看法是正确的。但是,另一个问题是您假设 int 是 32 位的。并非所有系统都如此。
  • 谢谢,添加了32位平台的假设
  • 关于你在上一部分的假设,int 表示的值位需要与unsigned 的对应值位匹配。唯一的问题是符号位不需要对应unsigned 表示中的另一个值位,并且int 表示中的任何填充位都可以是unsigned 表示中的附加值位。这意味着否定的n 可能会产生陷阱表示,而肯定的n 可能会产生实现定义的结果。
猜你喜欢
  • 1970-01-01
  • 2015-07-04
  • 1970-01-01
  • 2016-05-18
  • 1970-01-01
  • 2018-04-16
  • 1970-01-01
  • 2014-09-03
  • 1970-01-01
相关资源
最近更新 更多