C语言 - 无法获取位域的地址

23

为什么不能取位域的地址?

我如何创建一个指向位域的指针?

以下是代码...

struct bitfield {
    unsigned int a: 1;
    unsigned int b: 1;
    unsigned int c: 1;
    unsigned int d: 1;
};

int main(void)
{
    struct bitfield pipe = {
        .a = 1, .b = 0,
        .c = 0, .d = 0
    };
    printf("%d %d %d %d\n", pipe.a,
            pipe.b, pipe.c, pipe.d);
    printf("%p\n", &pipe.a); /* OPPS HERE */
    // error: cannot take address of bit-field ...
    return 0;
}

6
不能对位域使用指针。它的类型会是什么? - melpomene
@melpomene:嗯,那真正的问题是什么?如果我们说它将具有指向无符号整数的指针类型呢? - effeffe
CPU应该如何访问它?你有了解过对齐吗? - dhein
你是对的 - 它不能。 - Martin James
如果您想要访问位域所在的整个字节,您可以将位域结构联合为一个单独的int。 - Larry
显示剩余2条评论
5个回答

41

位域成员的大小(通常)小于指针允许的粒度,这是 char 的粒度(根据 char 的定义,它至少应该有 8 个比特长)。因此,普通指针不能满足需要。

此外,指向位域成员的指针的类型不清楚,因为编译器必须确切地知道位域中位域成员的位置才能存储/检索此类成员(而没有“常规”指针类型可以携带这些信息)。

最后,这几乎不是一个被请求的功能(位域一开始就不经常出现);位域用于紧凑地存储信息或构建标志的紧密表示(例如写入硬件端口),很少需要指针指向它们的单个字段 - 如果需要,您可以始终使用常规的 struct 并在最后一刻转换为位域。

由于所有这些原因,标准规定位域成员不可寻址,到此为止。虽然可能可以克服这些障碍(例如通过定义特殊的指针类型来存储访问位域成员所需的所有信息),但这将是语言中又一个过于复杂的黑暗角落,没有人使用。


10
最后有人指出了一些东西,并不仅仅是“标准说……”,没有“常规”的指针类型可以携带这样的信息。+1. - effeffe
4
实际上在64位系统中,指针可以引用特定的位。截至2017年,指针仅使用了64位中的48位,英特尔计划在不久的将来推出一种使用56位的硬件。因此,您可以使用最高的3位来编码字节内部的位偏移量,并通过一些比特操作在(void *)内部编码所有内容或定义一个特定的指针类型。您可以使用C风格的预处理器宏或C ++模板来完成此操作。但是,在标准中支持这样奇怪的功能是没有必要的。需要该功能的人可以实现它。 - DanielHsH
2
在64位架构中,滥用指针内的免费位是很常见的。Linux使用其中一些来编码指针+错误代码,作为结构体构造函数的返回值。此外,内核slab内存分配器使用指针的最高有效8位来存储附加数据。但再次强调,这是对类型的滥用而不是标准。 - DanielHsH
effeffe,这听起来非常像“无真正的苏格兰人”谬误。指针可以很容易地实现在遵循所有当前规则的同时携带位位置,您只需要在不使用位域时取消关联位位置即可。而且,“标准规定”是唯一重要的规则,因为它定义了语言。 - paxdiablo
@PBS 位域从通用编程角度来看完全是有问题的,它们是一种特定于结构体的hack,与C++类型系统中的任何其他东西都不兼容,主要是为了向后兼容C而保留的; unsigned int:1 不存在作为“独立”的类型,并且位域只有一堆非常特定的操作被定义得很好。除非空间紧张,否则最好避免使用它们(即使在这种情况下,您通常也最好使用普通的无符号整数和枚举值)。 - Matteo Italia
显示剩余2条评论

10

你实际上不能拥有位域的地址,因为在C语言中最小可寻址单元是一个字节(需要记住,在C语言中字节的宽度不一定是8个比特)。

你能期望的最好情况是包含字节集的地址。

标准的相关部分(在这种情况下是C11)是第6.5.3.2节“地址和间接运算符”(我强调):

一元&运算符的操作数必须是函数指示器、[]或一元*运算符的结果,或者是指代一个对象的不是位域且未声明为寄存器存储类说明符的lvalue

考虑到寻址的单位是一个字节,你可能会发现你的位域被存储为(例如):

            Bit 7   6   5   4   3   2   1   0
              +---+---+---+---+---+---+---+---+
Address: 1234 | a | b | c | d | ? | ? | ? | ? |
              +---+---+---+---+---+---+---+---+
         1235 |   |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+---+

你可以看到所有这些位域的地址实际上是相同的(1234),因此它对于区分它们并不是真正有用的。

为了操作位域,你应该直接访问它们,让编译器来处理。

即使使用位运算符也不能保证有效,除非你知道编译器在内存中如何排列它们。


6
地址必须是整数个字节,但位域不必如此,因此C标准规定地址运算符&不能与它们一起使用。当然,如果你真的想要处理位域地址,你可以使用包含结构体的地址,并进行一些位运算。

0

您无法打印位域的地址,但可以将其分配给所需大小类型的某些局部变量(从一位内存进行类型转换到2个字节(对于整数类型大小将取决于编译器)内存),可用于打印地址。

unsigned int x = pipe.a; printf("x=%d", &x);


-1

类似的,如果您只想处理单个字节,可以这样做:

union PAIR  {
    struct { uint8_t l, h; } b;
    uint16_t w;
};

PAIR ax;

现在您可以访问指向像 &ax.w、&ax.b.l 或 &ax.b.h 这样的片段的指针。请注意,这仍然不允许您指向单个位,请参阅先前的解释。

编辑:修正示例


1
这只是关于类型转换的未定义行为。联合体技巧可能会让编译器不警告你,但无论如何,访问位的地址是不可能的,因为按标准字节是最小可寻址单元! - dhein
也许。然而,这个技巧在许多模拟器中被使用,它们的功能似乎表明了另一种情况。例如MAME,你可以在这里看到它:https://github.com/mamedev/mame/blob/master/src/emu/emucore.h#L93 - Martin Olika
1
你知道你的链接并不支持你的回答,对吗?未定义行为只会在你以那种方式解引用它时出现,而不是结构本身。在你的链接中,它被用于条件编译以实现字节序安全,这是完全合法的。但你建议将其用于解引用,这是非法的,而且结果也没有用处。 - dhein
1
仅供参考,我发现了这个信息:ISO/IEC:9899(C99 TC3)在关于位域的章节中第102页的脚注106中指出:“一元&(取地址)运算符不能应用于位域对象;因此,不存在指向位域对象的指针或数组。” - dhein

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