在C语言中是否有一个类似于sizeof()的位等效函数?

35

当应用于位域时,Sizeof()无法正常工作:

# cat p.c
  #include<stdio.h>
  int main( int argc, char **argv )
  {
    struct { unsigned int bitfield : 3; } s;
    fprintf( stdout, "size=%d\n", sizeof(s.bitfield) );
  }
# gcc p.c -o p
  p.c: In function ‘main’:
  p.c:5: error: ‘sizeof’ applied to a bit-field

显然,由于它不能返回浮点部分大小或类似的东西。不过,这提出了一个有趣的问题:在C语言中是否有相应的函数可以告诉你变量/类型中的位数?理想情况下,它也适用于常规类型,如charint,还包括位域。

更新:

如果没有像sizeof()那样用于位域的语言等价物,最有效的计算方法是什么——在运行时!想象一下,您有依赖于此的循环,并且不希望在更改位域的大小时打破它们——而且不能通过将位域大小和循环长度设置为宏而作弊。;-)


相信结构的布局是在编译时确定的。因此,虽然原则上它可以在运行时进行检查(尽管如果我正确地阅读了下面的答案,C语言没有提供这样的方法),但一旦编译发生(使用特定编译器在特定平台上),它将保持不变(根据字边界优化等因素,当然会根据编译器和平台而异)。 - lindes
4个回答

27

在C语言中,您无法确定位域(bit-fields)的大小。然而,您可以通过使用<limits.h>中定义的 CHAR_BIT 值来找出其他类型的位数大小。位数大小简单地为CHAR_BIT * sizeof(type)

不要假设 C 字节是八进制字节(octet),它至少有8位。实际上,有些机器具有16位甚至32位字节。

关于你的编辑: 我会说一个位域int a: n;根据定义具有大小为n位。当放入结构体时,额外的填充位属于结构体而不属于位域。

我的建议:不要使用位域,而是使用(数组)unsigned char并使用位掩码(bitmasks)。这样,许多行为(溢出,没有填充)都是良好定义的。


这是不可能的(这也是人们避免使用位域的原因之一)。编译器可以将其实现为扩展,但我从未听说过这样的情况。 - schot
3
抱歉,您是错误的。C99标准规定CHAR_BIT的最小限制为8。在附录J 3.4中,它明确说明了作为实现定义行为的“字节中比特数的数量”。 - schot
1
@Dummy00001:我同意机器字节!= C字节。但是char并不总是8位。5.2.4.2.1:“它们的实现定义值应该等于或大于(绝对值)显示的值,并具有相同的符号。”然后显示:“最小对象(字节)CHAR_BIT 8的位数”。6.2.6.1:“存储在任何其他对象类型的非位域对象中的值由n×CHAR_BIT位组成,其中n是该类型对象的大小(以字节为单位)。 - schot
13
什么时候这个愚蠢的 CHAR_BIT 参数才能消失?除了 DSP 和 30 年以上的遗留主机,任何东西上的 CHAR_BIT 都是 8。POSIX 要求 CHAR_BIT==8,Windows 绑定在 CHAR_BIT==8 的 x86 上,整个互联网和网络机器之间的互操作性都是基于八位字节构建的。除非你有非常不寻常的目标(在这种情况下,你的代码可能根本无法移植),否则甚至想到 CHAR_BIT!=8 的可能性都没有任何意义。 - R.. GitHub STOP HELPING ICE
1
对于您的建议我表示赞同:不要使用位域,而是使用(数组)unsigned char 并使用位掩码进行操作。这样可以定义很多行为(溢出、无填充)。我建议使用 uint8_t 而不是 unsigned char,以增加清晰度和明确性。 - Gabriel Staples
显示剩余7条评论

5
无法使用sizeof()找到位域的大小。请参考C99:
6.5.3.4 sizeof运算符,位域显然不受sizeof()支持。
6.7.2.1 结构和联合说明符在这里澄清了位域不是独立的成员。
否则,您可以尝试将-1u(所有位设置的值)分配给位域成员,然后找到最高有效位的索引。例如(未经测试):
s.bitfield = -1u;
num_bits = ffs(s.bitfield+1)-1;

请查看man ffs了解更多相关信息。


+1 这看起来更接近于一个最优长度查找器。ffs() 是什么? - eruciform
1
@eruciform: ffs = find first set。这是一个函数(通常直接映射到CPU指令),用于在整数中查找第一个设置的位。位从1开始编号。如果输入整数为0,则返回值也为0。 - Dummy00001
太棒了!这绝对是我从未见过的函数。我还以为这些年来我已经大部分访问了C语言中布满蛛网的角落! - eruciform
1
没有(可移植的)方法可以在编译时计算 ffs,因此这通常是低效的。但是,您的循环可能不依赖于位数的计数,而只是循环遍历位,这种情况下,您可以使用 -1 初始化一个位域,并执行类似 for (counter.bf=-1; counter.bf; counter.bf>>=1) 的操作。(提示:这仅适用于您的位域为 unsigned 的情况。) - R.. GitHub STOP HELPING ICE
1
请注意,ffs是POSIX函数,并非所有平台都可用,也不适用于大于int的数据类型。当然,您可以自己实现,但速度可能会慢一些。 - Chiara Coetzee
有趣的方法,但它不适用于带符号位域或无符号位域,其位长度恰好为类型“unsigned int”的位长度。还要注意,它会处理比那更长的位域。 - chqrlie

3

我实现了这个解决方案[1]

#include <stdio.h>
#define bitoffsetof(t, f) \
    ({ union { unsigned long long raw; t typ; }; \
    raw = 0; ++typ.f; __builtin_ctzll(raw); })

#define bitsizeof(t, f) \
    ({ union { unsigned long long raw; t typ; }; \
    raw = 0; --typ.f; 8*sizeof(raw)-__builtin_clzll(raw)\
        -__builtin_ctzll(raw); })

struct RGB565 { unsigned short r:5, g:6, b:5; };
int main()
{
    printf("offset(width): r=%d(%d) g=%d(%d) b=%d(%d)\n",
        bitoffsetof(RGB565, r), bitsizeof(RGB565, r),
        bitoffsetof(RGB565, g), bitsizeof(RGB565, g),
        bitoffsetof(RGB565, b), bitsizeof(RGB565, b));
}


$ gcc bitfieldtest.cpp && ./a.out
偏移量(位宽):r=0(5) g=5(6) b=11(5)
[1] https://twitter.com/suarezvictor/status/1477697986243272706

更新: 我确认这是在编译时解决的:

void fill(int *x)
{
    x[0]=bitoffsetof(RGB565, r);
    x[1]=bitsizeof(RGB565, r);
    x[2]=bitoffsetof(RGB565, g);
    x[3]=bitsizeof(RGB565, g);
    x[4]=bitoffsetof(RGB565, b);
    x[5]=bitsizeof(RGB565, b);
}

汇编输出:

fill:
.LFB12:
    .cfi_startproc
    movl    $0, (%rdi)
    movl    $5, 4(%rdi)
    movl    $5, 8(%rdi)
    movl    $6, 12(%rdi)
    movl    $11, 16(%rdi)
    movl    $5, 20(%rdi)
    ret

1
有趣的方法,但你可能需要补充说明当位域值改变时,填充位的值也可能会改变,因此以上代码不能保证可行。 - chqrlie
我认为这个问题是不可能的,因为当前的实现没有访问任何现有结构的实例,它创建一个用于计数的结构并将其初始化为零作为第一步。然后这个结构被丢弃了。我猜编译器可以在编译时解决所有这些问题,并提供所需的常量。宏只接收结构类型和字段名称,你不能从外部触及任何东西。这是否说明了它不可能存在任何问题? - Victor Suarez
我确认编译器在编译时解决了这个问题,请查看更新。 - Victor Suarez
1
它只证明了特定编译器在编译时运行代码的表现良好。事实上,在这个特定的测试中,没有填充位,所以行为是明确定义的(除了别名问题,即从未最后写入的联合成员读取,这是另一个问题),但 OP 的示例 struct { unsigned int bitfield : 3; } s; 至少有 5 个填充位,当设置 s.bitfield 时其值可能会改变。 - chqrlie
这太棒了,我觉得如果std::popcount和std::find/find_if也是constexpr的话,甚至可以沿着同样的思路创建一个可移植的模板。谢谢你提出这个想法! - undefined

-1
使用一组#define语句来指定结构定义中的位宽,然后在打印或其他操作时使用相同的#define。这样你就可以实现“定义一次,多次使用”,尽管你会在全局命名空间中有一些比特字段大小的定义混乱。
# cat p.c
#include<stdio.h>
int main( int argc, char **argv )
{
   #define bitfield_sz 3
   struct { unsigned int bitfield : bitfield_sz; } s;
   fprintf( stdout, "size=%d\n", bitfield_sz );
}
# gcc p.c -o p
# ./p
size=3
#

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