4

继续我在 C 中的实验,我想看看位域是如何放置在内存中的。我正在使用英特尔 64 位机器。这是我的一段代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
int main(int argc, char**argv){
       struct box_props
       {
         unsigned int opaque       : 1;
         unsigned int fill_color   : 3;
         unsigned int              : 4; 
         unsigned int show_border  : 1;
         unsigned int border_color : 3;
         unsigned int border_style : 2;
         unsigned int              : 2; 
       };

       struct box_props s;
       memset(&s, 0, 32);
       s.opaque = 1;
       s.fill_color = 7;
       s.show_border = 1;
       s.border_color = 7;
       s.border_style = 3;

       int i;
       printf("sizeof box_porps: %d sizeof unsigned int: %d\n", sizeof(struct box_props), sizeof(unsigned int));
       char *ptr = (char *)&s;
       for (i=0; i < sizeof(struct box_props); i++){
          printf("%x = %x\n", ptr + i, *(ptr + i));
       }

       return 0;

这是一个输出:

sizeof box_porps: 4 sizeof unsigned int: 4
5be6e2f0 = f
5be6e2f1 = 3f
5be6e2f2 = 0
5be6e2f3 = 0

这是一个问题:为什么struct box_props有大小4- 它不能只是2字节吗?在这种情况下如何填充?我有点(名词预兆)对此感到困惑。

提前谢谢所有答案

4

4 回答 4

9

尽管在这种情况下总要求仅为 2 字节 (1+3+4+1+3+2+2),但使用的数据类型 ( unsigned int) 的大小为 4 字节。所以分配的内存也是4 Bytes。如果您只想分配 2 个字节 unsigned short,请用作您的数据类型并再次运行该程序。

于 2013-05-22T13:16:55.470 回答
6

来自 ISO C 标准:

实现可以分配任何大到足以容纳位域的可寻址存储单元。(以及以后)结构或联合的末尾可能有未命名的填充。

所以不需要总是为结构选择尽可能小的内存块。并且由于 32 位字可能是编译器的本机大小,所以它就是这样选择的。

于 2013-05-22T13:22:00.997 回答
3

位字段在内存中的放置不仅取决于编译器如何决定分配结构中的各个字段,还取决于您正在运行的机器的字节序。让我们一个接一个。编译器内的字段分配可以通过指定字段的大小(如@DDD 指出的那样)来控制,也可以通过另一种机制来控制。您可以告诉编译器pack您的结构或使其更适合编译器可能希望如何针对您正在编译的机器架构进行优化。packed 使用type 属性指定包装。因此,如果您将结构指定为:

struct __attribute__ ((__packed__)) box_props {
    ...
} 

您可能会在内存中看到不同的布局。请注意,通过检查结构组件,您不会看到布局有所不同——内存中的布局可能会发生变化。在与其他东西(例如 IO 设备)进行通信时,封装结构至关重要,该设备需要特定位置的特定位。

位域结构的第二个问题是它们的布局依赖于字节序。结构在内存中的布局(或与此相关的任何数据)取决于您是在大端(POWER)还是小端(例如,x86)机器上运行。一些系统(例如,嵌入式 PowerPC 系统是双端的)。

通常,位字段使移植代码变得非常困难,因为您正在处理内存中数据的布局。

希望这可以帮助!

于 2013-05-22T13:20:12.677 回答
2

出于某种原因,我不理解,C 标准的实现者决定指定数字类型和位域应该分配足以容纳该数字类型的空间,除非前一个字段是位域,分配自相同类型,具有剩下足够的空间来处理下一个字段。

对于您的特定示例,在具有 16 位无符号短裤的机器上,您应该将位域中的声明更改为无符号短裤。碰巧, unsigned char 也可以工作,并产生相同的结果,但情况并非总是如此。如果优化打包的位域将跨越字符边界而不是短边界,则将位域声明为unsigned char需要填充以避免这种跨越。

尽管某些处理器可以毫无问题地为跨越存储单元边界的位域生成代码,但当前的 C 标准将禁止以这种方式打包它们(再次,出于我不理解的原因)。例如,在具有典型 8/16/32/64 位数据类型的机器上,编译器不允许程序员指定包含 8 个 3 字节字段的 3 字节结构,因为这些字段必须跨越字节边界. 我可以理解规范不要求编译器处理跨越边界的字段,或者要求以某种特定方式布置位域(如果可以指定特定位域应该使用位 4-,我会认为它们无限有用) 7 个位置),但目前的标准似乎给出了两全其美的结果。

在任何情况下,有效使用位域的唯一方法是找出存储单元边界的位置,并适当地选择位域的类型。

PS——有趣的是,虽然我记得编译器曾经禁止volatile声明包含位域的结构(因为编写位域时的操作顺序可能没有很好地定义),但在新规则下,语义可以很好地定义(我不'不知道规范是否真的需要它们)。例如,给定:

typedef struct {
  uint64_t b0:8,b1:8,b2:8,b3:8, b4:8,b5:8,b6:8,b7:8;
  uint64_t b8:8,b9:8,bA:8,bB:8, bC:8,bD:8,bE:8,bF:8;
} FOO;
extern volatile FOO bar;

该语句bar.b3 = 123;将从 中读取前 64 位bar,然后将 的前 64 位写入bar更新后的值。如果bar不是易失性的,编译器可能会用简单的 8 位写入替换该序列,但bar可能类似于只能以 32 位或 64 位块写入的硬件寄存器。

如果我有我的 druthers,则可以使用以下方式定义位域:

typedef struct {
  uint32_t {
    baudRate:13=0, dataFormat:3,
    enableRxStartInt: 1=28, enableRxDoneInt: 1, enableTxReadyInt: 1, enableTxEmptyInt: 1;};
  };
} UART_CONTROL;

表示 baudRate 是从第 0 位(LSB)开始的 13 位,dataFormat 是从 baudRate 之后开始的 3 位,enableRxStartInt 是第 28 位等。这样的语法将允许以可移植方式编写许多类型的数据打包和解包,并且将允许以与编译器无关的方式完成许多 I/O 寄存器操作(尽管这样的代码显然是特定于硬件的)。

于 2013-05-22T13:25:12.653 回答