C 可变宽度位域

3

我在这里不提出很多问题,所以如果我的“提问技巧”有些生疏,请原谅。以下是我的问题:


我已经查找了一些资料,但似乎找不到解决方案。

这个程序的想法是打印出一个表格,其中包含具有任意位数的数字的每个可能值的十进制(有符号和无符号)、十六进制和二进制表示。它从argv接受位大小,示例输出应该是(重点关注二进制表示):

$ ./test 2
00
01
10
11
$ ./test 4
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
1010
1011
1100
1101
1110
1111

这段代码使用预处理宏可以正常工作,但这意味着我每次想要改变位数时都需要重新编译。如果使用argv,所有问题都能解决,但有一个小问题。编译器似乎不喜欢我的做法,即使这看起来非常合理。
int bit_size = atoi(argv[1]);
struct { unsigned int a : bit_size; } test;

但编译器给我一个错误,严厉地告诉我我不能这样做(“位域不是整数常量”)。

好的...所以我尝试使用 const int 代替:

const int bit_size = atoi(argv[1]);
struct { unsigned int a : bit_size; } test;

我遇到了完全相同的错误。

需要注意的是,GCC希望我执行以下操作:

struct { unsigned int a : 8; } test;

而且它可以正常运作。但我需要能够变化。

我很困惑。

请注意,我不希望或需要位域的宽度在程序中间改变,这就是我认为GCC试图防止我做的事情。这实际上是一次性操作。

还要注意,我并不像这个问题(以及许多类似的问题)那样在普通变量上尝试这样做。

这个问题也与我想要完成的任务无关。


此外,如果有帮助的话,这是一个在没有错误的情况下运行时显示的示例(位域宽度=4):

$ ./test 4
$ cat table.md
Table for a/an 4-bit integer: 

| Unsigned | Signed | Hex | Binary | Sign Bit |
|:--------:|:------:|:---:|:------:|:--------:|
|    0     |   0    |  0  |  0000  |    0     |
|    1     |   1    |  1  |  0001  |    0     |
|    2     |   2    |  2  |  0010  |    0     |
|    3     |   3    |  3  |  0011  |    0     |
|    4     |   4    |  4  |  0100  |    0     |
|    5     |   5    |  5  |  0101  |    0     |
|    6     |   6    |  6  |  0110  |    0     |
|    7     |   7    |  7  |  0111  |    0     |
|    8     |   -8   |  8  |  1000  |    1     |
|    9     |   -7   |  9  |  1001  |    1     |
|    10    |   -6   |  A  |  1010  |    1     |
|    11    |   -5   |  B  |  1011  |    1     |
|    12    |   -4   |  C  |  1100  |    1     |
|    13    |   -3   |  D  |  1101  |    1     |
|    14    |   -2   |  E  |  1110  |    1     |
|    15    |   -1   |  F  |  1111  |    1     |

@ouah 我不知道“常量”和“只读”的区别。 - Braden Best
"constant expression" 在 C 语言中有特殊的含义,只能通过定义、枚举和在编译时定义的东西来获得。 - Étienne
1
啊哈,基本上,位域不是解决方案。我使用它们的唯一原因是为了有符号整数,但我刚刚注意到有符号负数中的逻辑模式(符号位 - 数字的其余部分 = 负数的绝对值)。我可能会稍后发布自己的答案。 - Braden Best
我找到了解决方法。1010 = 10或-6。1000 - 0010 = 8 - 2 = 6。我最终做的是将符号位向左移,然后从数字中减去它-> 01010 - 10000 = 10 - 16 = -6。所以我无意中解决了它。为了防止这是4位数字的偶然事件,我将运行更多测试。 - Braden Best
1
@Étienne 啊,原来这就是二进制补码。 - Braden Best
显示剩余4条评论
3个回答

5
在C语言中,不能在运行时定义位域的大小。但是你不需要使用位域来打印二进制值,只需编写一个函数以二进制格式打印数字,就像这里的函数一样:Is there a printf converter to print in binary format? 然后编写一个简单的循环来打印你的数字:
//get n
for (int i = 0; i < n; i++) {
    print_binary(i);
}

编辑: 关于如何打印以二进制补码表示的负数(在C语言中没有原生类型,比如int8_t、int16_t、int32_t等),就像你发现的那样,对于用二进制补码表示的N位有符号字,对于负数,可以使用以下公式:

Xnegative = 2^N - Xpositive

//get n
for (uint32_t Xpos = 0; Xpos < (1<<n); Xpos++) {
    if (Xpos > 1<<(n-1))
        printf("%d\n", -(1 << n) + Xpos);
    else
        printf("%u\n", Xpos);
}

注意:问题并不在于二进制数本身。我认为使位域成为必要的真正问题是,我不确定如何打印有符号整数(请参见我添加到问题中的示例运行)。例如,我该如何告诉它8是-8,9是-7?我想我可以通过数学方法得出答案。(例如,1010 => 1000 - 0010 => 8 - 2 => 6 => -6) - Braden Best
无论如何,感谢直接的回答,现在这个问题对于将来遇到同样问题的人来说已经得到解决! - Braden Best
“我怎么告诉它8是-8,9是-7?”无论如何,没有上下文你都做不到这一点。 - Meirion Hughes

1
我已经理解了。
如其他答案所述,GCC在运行时不允许动态设置位域的宽度,因为这是在编译时完成的。
于是我开始思考为什么需要位域,那是因为我想要显示负数。
然后我研究了几个有符号整数,看能否找到一个数学模式。
我找到了这样的模式,并决定不再需要位域。
1010 = 0 - (sign_bit (1000) - raw_number (0010)) = -6

虽然我在调试原始算法时意外地发现了另一个同样有效的算法:

number - ( (number << 1) & (1 << number of bits) )

我记录了笔记以便理解并确保它不是巧合:

For a negative number:

n = 10 (binary 1010)
num_bits = 4
power = (1010 << 1 => 10100) & (1 << num_bits => 10000)
  10100
& 10000
  10000
return n (10) - power (16) => -6

For a positive number:

n = 6 (binary 0110)
num_bits = 4
power = (0110 << 1 => 01100) & (1 << num_bits => 10000)
  01100
& 10000
  00000
return n (6) - power (0) => 6

最终函数为:
signed int get_signed(long long int n, int num_bits){
  int power = (n << 1) & (1 << num_bits);
  return n - power;
}

我将num_bits设置为1、2、4、8和16进行了测试,结果令人惊喜,它完美无缺地工作。


编辑:

我刚刚让我的原始算法工作了

signed int get_tc(long long int n, int num_bits){
  int sign_bit = n >> (num_bits - 1) & 1;
  sign_bit <<= (num_bits - 1);
  int raw = n ^ sign_bit;
  return -1 * (sign_bit - raw);
}

而且逻辑:

n = 10
num_bits = 4
sign_bit = 10 >> (4-1) & 1
  1010
>>3
  0001
& 0001
  0001
sign_bit <<= (4-1)
  0001
<<3
  1000
sign_bit = 8
raw = 10 ^ 8
  1010
^ 1000
  0010
raw = 2
return -1 * (8 - 2)
  8 - 2 = 6
  6 * -1 = -6
return = -6

n = 6
num_bits = 4
sign_bit = 6 >> (4-1) & 1
  0110
>>3
  0000
& 0001
  0000
sign_bit <<= (4-1)
  0000
<<3
  0000
sign_bit = 0
raw = n ^ sign_bit
  0110
^ 0000
  0110
raw = 6
return -1 * (0 - 6)
  0 - 6 = -6
  -6 * -1 = 6

0

我知道你已经解决了这个问题,但这是我的打印表格的解决方案。特别是因为我更喜欢使用简单的异或运算。这依赖于使用负数表示,例如在4位情况下0x1001被表示为-7,并将其符号扩展为带符号的长整型然后打印出来。

首先有两件事情,一个是你的数字(比如说4位的-7,0b1001),另一个是包含所讨论的位数的掩码(比如说4),设置为1(0b1111)异或 -1 的带符号表示。所以,对于32位带符号数,掩码为:

掩码:0b1111 ^ 0b11111111111111111111111111111111 = 0b11111111111111111111111111110000

当你用这个掩码与你的数字进行异或运算时,你得到了你选择使用的分辨率中的符号扩展版本。

0b1001 ^ 0b11111111111111111111111111110000 = 0b11111111111111111111111111111001

在有符号 32 位表示中,-7 是一个容易打印的数字。 符号扩展 是一个简单的概念。

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

void makeStrbits(long n,long b,char *strbits)
{
   long i=0, m=1L<<(b-1);
   while(m){
      strbits[i++]= (n&m)?'1':'0';
      m=m>>1;
   }
}

int main(int argc, char *argv[])
{
   long i,bit=4,mask=0;
   char *strbits = NULL;
   if (argc==2) bit = strtol( argv[1],NULL,10 );

   strbits = calloc(1,bit+1);

   for(i=0;i<bit;i++) mask |= (1L<<i);
   mask ^= -1L;

   printf("Table for a %ld bit integer\n", bit);
   printf("unsgn\t sign\t hex\t sbit\t bits\n" );
   for(i=0; i<pow(2,bit); i++) {
      int sign = ((i&(1L<<(bit-1))) != 0);
      makeStrbits(i,bit,strbits);
      printf("%lu\t %ld\t %0x\t %1d\t %s\n", i, sign?i^mask:i, i, sign, strbits );
   }
   free(strbits);
   return 0;
}

非常有趣的阅读,但是算法确实需要处理任何数字,而不仅仅是0..15。这就是为什么我尝试通过获取符号位,然后获取其余数字(但不包括符号位),然后从符号位的值中减去“原始”数字,然后乘以-1来实现算法。 1010 => -1 * (1000 - 0010) = -60101 => -1 * (0000 - 0101) = +5。虽然当我试图找出如何获得“原始数字”时偶然发现了该算法,但它同样有效。 - Braden Best
它可以处理任何数字(长整型位数内的数字),但使用long long扩展也很容易。我的上面的程序将命令行中想要的位数作为第一个参数,但如果您没有提供参数,则默认为4。 - JohnH

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