结构体的联合只有位域,sizeof函数会使字节翻倍,C语言。

3

出于某种原因,我无法完全弄清楚,我的仅包含位域的结构体联合会设置两倍于任何单个结构体所需的字节数。

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

union instructionSet {
    struct Brane{
        unsigned int opcode: 4;
        unsigned int address: 12;
    } brane;
    struct Cmp{
        unsigned int opcode: 4;
        unsigned int blank: 1;
        unsigned int rsvd: 3;
        unsigned char letter: 8;
    } cmp;
    struct {
        unsigned int rsvd: 16;
    } reserved;
};

int main() {

    union instructionSet IR;// = (union instructionSet*)calloc(1, 2);

    printf("size of union %ld\n", sizeof(union instructionSet));
    printf("size of reserved %ld\n", sizeof(IR.reserved));
    printf("size of brane %ld\n", sizeof(IR.brane));
    printf("size of brane %ld\n", sizeof(IR.cmp));


    return 0;
}

所有对sizeof的调用都返回4,但据我所知它们应该返回2。


2
sizeof 返回 size_t,必须使用 %zu 进行打印。 - phuclv
1
你打包位域的数据类型决定了包含实体的大小/对齐方式。因此,如果一个 int 是 4 字节,那么 int x:1 将被打包成一个对齐的 4 字节整数。多个位域可以被打包到其中,但包含结构的大小将是 4 的倍数。 - Tom Karzes
@mch:问题说明他们观察到所有情况下都是4。您的评论说明您在所有情况下都观察到了4。这是再现,而不是未再现。 - Eric Postpischil
@TomKarzes:根据我回答中引用的C 2018 6.7.2.1 11,C实现可以选择任何适合的存储单元;它不需要基于类型进行选择。 - Eric Postpischil
@Lundin:一个实现可以定义其他类型被允许。如果unsigned char不被允许,那么代码就违反了6.7.2.1 5中的约束,因此需要产生诊断消息。因此,如果没有诊断消息,则实现已经定义它是被允许的,我们可以知道这段代码将会做什么。 - Eric Postpischil
显示剩余8条评论
4个回答

2

C 2018 6.7.2.1 11 允许 C 实现选择用于位域的容器大小:

实现可以分配任何足够大以容纳位域的可寻址存储单元。如果有足够的空间,立即跟随结构中另一个位域的位域将被打包到同一单元的相邻位中。如果剩余空间不足,则无法适应的位域是放入下一个单元还是重叠相邻单元是由实现定义的……

您正在使用的实现显然选择使用四字节单位。这也可能是实现中 int 的大小,表明它是实现的方便大小。


2

这里有几个问题,首先,你的位域 Brane 使用了 4 字节的 unsigned int。

即使你只使用了其中一半的比特位,你仍然使用了一个完整的 32 位宽度的 unsigned int。

其次,你的 Cmp 位域使用了两种不同的字段类型,所以你使用了 32 位 unsigned int 中的 8 位用于前三个字段,然后你使用了一个完整的 8 位 unsigned char。 由于数据对齐规则,这个结构体至少会占用 6 个字节,但可能会更多。

如果你想要优化联合体的大小以只占用 16 位。你需要首先使用 unsigned short,然后你需要始终使用相同的字段类型来保持所有内容在同一空间中。

像这样做可以完全优化你的联合体:

union instructionSet {
    struct Brane{
        unsigned short opcode: 4;
        unsigned short address: 12;
    } brane;
    struct Cmp{
        unsigned short opcode: 4;
        unsigned short blank: 1;
        unsigned short rsvd: 3;
        unsigned short letter: 8;
    } cmp;
    struct {
        unsigned short rsvd: 16;
    } reserved;
};

这将在四周给你一个大小为2。最初的回答中说的是这个。

C标准允许实现使用任何适合于位域的存储单元;对于 int x : 3,实现无需使用四字节的 int,也无需使用两字节的 short 来表示一个 short x : 16。此外,实现可以压缩连续的位域; short x : 1; int y : 8; int z : 23; 可以被压缩成32位(假设实现接受将 short 用于位域,这并非必须)。虽然你说 Cmp 至少使用了6个字节,但我们知道在OP的实现中它只有4个字节。 - Eric Postpischil
然后你可以使用无符号字符来表示它的完整8位。标准没有指定无符号字符位域的大小,也没有指定它是否与int位域不同。短整型也是如此,这并没有解决任何问题。由于数据对齐规则,这个结构至少会占用6个字节。但这并不是真的,因为如果一个int位域溢出到一个char位域中,那么会发生什么是未指定的。关键在于认识到这些都没有标准化或保证。你甚至不知道哪一位是MSB,也不知道填充位的位置等等。 - Lundin

1

阅读有关内存结构填充/内存对齐的内容。默认情况下,32位处理器通过32位(4字节)从内存中读取,因为这样更快。因此,在内存中,char + uint32将被写入8个字节(1个字节-char,3个字节空格,4个字节uint32)。

在程序的开头和结尾添加这些行,结果将为2。

#pragma pack(1)

#pragma unpack

这是告诉编译器的方法:将内存对齐到1字节(默认为32位处理器上的4字节)。
PS:使用不同的#pragma pack设置尝试此示例。
struct s1 
{
    char a;
    char b;
    int c;
};

struct s2
{    
    char b;
    int c;
    char a;
};

int main() {
    printf("size of s1 %ld\n", sizeof(struct s1));
    printf("size of s2 %ld\n", sizeof(struct s2));

    return 0;
}

#pragma 指令对我很有用,非常感谢。我会尝试其他实现方式。 - M. Church

1

没有具体说明这段代码将会做什么,也没有意义去考虑它,除非有特定的系统和编译器。位域在标准中的规定太过模糊,无法可靠地用于像内存布局之类的事情。

union instructionSet {

    /* any number of padding bits may be inserted here */ 

    /* we don't know if what will follow is MSB or LSB */

    struct Brane{
        unsigned int opcode: 4; 
        unsigned int address: 12;
    } brane;
    struct Cmp{
        unsigned int opcode: 4;
        unsigned int blank: 1;
        unsigned int rsvd: 3;
        /* anything can happen here, "letter" can merge with the previous 
           storage unit or get placed in a new storage unit */
        unsigned char letter: 8; // unsigned char does not need to be supported
    } cmp;
    struct {
        unsigned int rsvd: 16;
    } reserved;

    /* any number of padding bits may be inserted here */ 
};

标准允许编译器为任何位字段类型选择“存储单元”,其大小可以是任意的。标准仅声明:
实现可以分配足够大的可寻址存储单元来容纳位字段。
我们无法知道以下事情:
- 类型为unsigned int的位字段有多大,32位可能是合理的,但不能保证。 - 是否允许使用unsigned char作为位字段。 - 类型为unsigned char的位字段有多大,可以是8到32的任何大小。 - 如果编译器选择的存储单元比预期的32位小,并且位不适合其中会发生什么。 - 如果unsigned int位字段与unsigned char位字段相遇会发生什么。 - 在联合体末尾或开头是否会有填充(对齐)。 - 结构体中各个存储单元的对齐方式。 - MSB的位置。
我们可以知道以下事情:
我们已经在内存中创建了某种二进制块。 该块的第一个字节驻留在内存中最不重要的地址上。它可能包含数据或填充。 有关更多信息,需要考虑具体的系统和编译器。

我们可以使用100%可移植和确定性的位操作来代替位域,这些操作无论如何都会产生相同的机器码。


我可能最终会这样做,因为有许多领域重叠,我可以根据需要任意进行屏蔽函数。 - M. Church

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