打印一个数字的二进制表示

6

我想打印一个int的二进制表示。我的解决方案似乎在Visual Studio中对intunsigned int都有效,但有人告诉我这是错误的。有人看到错误吗?如果有,为什么我的程序对我来说似乎是有效的?

void printbin(int n)
{
    unsigned int i = 1<<31;

    for (int j=0; j<32; j++)
    {
        if ((n & i) != 0)
            printf("1");
        else
            printf("0");
        i = i>>1;
    }

    printf("\n");
}

1
你为什么认为它是错误的?你遇到了什么问题? - Rosa
1
你想要实现什么?你已经尝试了什么,期望得到什么结果? - mp3ferret
1
这个“32”是什么意思?你是指sizeof(int) * CHAR_BIT吗? - William Pursell
2个回答

2
为什么我的程序对我来说似乎是有效的?
有两种可能性:
1. 你测试了所有输入和条件,程序对于这些都能正常工作。但是,你没有测试其它输入和/或条件,导致程序在这些情况下无法运行。特殊情况下,程序可能会依赖于未定义、实现定义或未指定的行为(确实如此),即使在测试环境中以期望的方式工作,它也本质上是错误的。
2. 你关于程序的正确工作存在误解,很可能是由于对所需输出的误解造成的。
未定义/实现定义的行为:
首先,像@chux首先观察到的那样,在具有32位(或更小)int的系统上评估表达式1 << 31会产生未定义行为,例如Windows提供的编译器和Visual Studio的C编译器。两个操作数都是int类型,因此结果是int类型,但算术上正确的结果超出了该类型可以表示的值的范围。对于无符号整数结果,该情况下的行为将被定义,但对于int等带符号整数类型明确地未定义。由于您将结果分配给unsigned int类型的变量,因此您只需要将表达式更改为1u << 31即可解决该问题。
此外,任何类型的表示中的位数未指定,但是您的代码假设为32位unsigned int。这确实是Visual Studio C编译器提供的unsigned ints的大小,但您不需要依赖于它。通过计算unsigned int的表示中的位数作为CHAR_BIT * sizeof(unsigned int),您将在每个环境中获得正确的实现相关结果。
只要我们谈论实现依赖性,就不一定所有对象表示中的所有位都对其值有贡献。还可以有填充位,并且在具有少于32个值位的类型unsigned int的表示的实现上,表达式1u << 31或其等效物将评估为零。为了完全正确,unsigned int表示中value bit的数量的计算必须基于UINT_MAX的值。一个绕过此问题的可行掩码的替代表达式将是~(UINT_MAX >> 1)。
输出格式:
关于输出格式,不清楚“int”的“二进制形式”是什么,特别是考虑到你想提供负数和正数值。如果你要提供负数值的形式而不使用“-”符号,就像你的代码尝试做的那样,那么要么必须指定所需输出形式的细节或假设(例如big-endian,32位二补数),要么你打算探测输入值的机器特定表示。由于你没有指定特定的格式,如果问题的一部分在于输出格式,则我只能得出结论:要求使用机器特定表示或符号/幅度。
机器表示
如果目标是探测int值的机器表示,则您的程序至少有两个错误。
首先,评估表达式n&i涉及将i的值从类型int转换为类型unsigned int。因此,您打印的是转换值的表示形式,这不能保证与原始int值的表示形式相同。实际上,这种情况不太可能发生在具有实际差异的机器和C实现中。当然,在Windows上的Visual Studio不属于这种环境。
此外,您的代码输出一个逻辑值的表示,这不一定符合物理表示。即使假设您没有遇到转换或各种对象表示的大小等问题,您的代码也假定物理布局是从最高位到最低位的字节。也就是说,它打印了一个big-endian表示,而不管实际的物理表示如何。在x86和x86_64上,int的本机物理表示是小端字节序,我下面的代码用于打印机器表示将打印与您的代码不同的结果。
void printbin(int n)
{
    unsigned char *p = (unsigned char *) &n;

    for (int j=0; j < sizeof(n); j++)
    {
        for (unsigned char mask = 1u << (CHAR_BIT - 1); mask; mask >>= 1) {
            putchar((*p & mask) ? '1' : '0');
        }
        p += 1;
    }

    putchar('\n');
}

该标准允许不同指针类型之间的转换,并且特别规定程序中的转换将导致 p 被初始化为指向 n 表示中的第一个字节。该程序逐个遍历表示中的每个字节(其总数由 sizeof 运算符确定),并按从最高位到最低位的顺序打印每个字节中的位,类似于您的版本。如果存在填充位,则包括在内。
另一方面,如果您想要一个有符号的二进制数字字符串,从最高位的非零位到最低位的位,则可以按以下方式执行:
void printbin_digits(unsigned int n) {
    char bits[CHAR_BIT * sizeof(unsigned int)] = {0};
    int bit_count = 0;

    while (n) {
        bits[bit_count++] = n % 2;
        n >>= 1;
    }
    while (bit_count) {
        putchar(bits[--bit_count] ? '1' : 0);
    }
}

void printbin(int n)
{
    if (n == 0) {
        putchar('0');
    } else if (n == INT_MIN) {
        putchar('-');
        printbin_digits(-(n / 2));
        putchar((n % 2) ? '1' : '0');
    } else if (n < 0) {
        putchar('-');
        printbin_digits(-n);
    } else {
        printbin_digits(n);
    }

    putchar('\n');
}

该方法没有任何对于类型为int的值表示不做假设的情况,这些值并未受到C标准的支持。特别需要注意的是当n等于INT_MIN时需要进行特殊处理,虽然这样做比较麻烦,但必须这样做是因为对于表达式-INT_MIN的求值可能会(在x86上)产生未定义行为。


确实,这是“C int在内存中存储的二进制表示”,而不是“整数的二进制表示”。 - greggo
@greggo,是的,但这种解释源于问题,因为可能为负的int值必须提供表示,并且纯二进制表示是原始程序产生的(而不是有符号数字字符串)。尽管如此,我会更新我的答案并提供另一种解决方案。 - John Bollinger
小问题:在 INT_MAX < UCHAR_MAX 的平台上,使用 unsigned char mask = 1u << (CHAR_BIT - 1)(添加 u)具有优势。 - chux - Reinstate Monica
谢谢,@chux。已更新。 - John Bollinger
你的第一个 printbin() 函数对字节顺序做出了(不必要的)假设。它假定大端序,据我所知,并没有改进原始代码。 - Jonathan Leffler
@JonathanLeffler,相反,第一段代码的目的是打印本地表示。该文本解释了在小端序的情况下,这将与OP的代码不同。事实上,“二进制表示”对于OP的含义不清楚,但是原始代码声称提供负值的表示而不使用符号/幅度,机器依赖性是固有的。 - John Bollinger

2
1<<31将一个位移值移到数值位之后,可能会进入符号(或填充)位。在C语言中,这是未定义的行为。 n & i试图“与”一个unsigned int和一个signed int的符号位。
OP对32的使用假定了int的宽度为32位。
以下是一个示例,可以打印出符号和变量位数 - 适用于[INT_MIN...INT_MAX]
#include <limits.h>
void printbin_c(int n) {
  char buf[CHAR_BIT * sizeof n + 1];
  char *p = &buf[sizeof buf - 1];
  *p = '\0';

  int i = n;
  if (i > 0) {
    i = -i;
  }

  do {
    p--;
    *p = '0' - i%2;
    i /= 2;
  } while (i);

  if (n < 0) {
    p--;
    *p = '-';
  }

  puts(p);
}

[编辑] 处理1的补码 @John Bollinger

使用负绝对值与if (i > 0) i = -i;作为正绝对值,不能很好地处理带有INT_MIN 2的补码。


我喜欢先在缓冲区中构建结果然后一次性打印的想法。做得很好。但是需要注意的是,如果整数的表示采用了补码格式(虽然 C 语言支持,但大多数人都不会遇到),则似乎对于负数 i(i & 1) + '0' 不会产生正确的结果。 - John Bollinger
@John Bollinger,关于(i & 1) + '0'和1的补码是正确的。除了0之外,我没有看到任何前导零的打印。您有什么想法? - chux - Reinstate Monica
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - John Bollinger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接