继续我在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
个字节?在这种情况下如何填充?我有点(nomen omen)与它混淆。
提前获得所有答案
答案 0 :(得分:9)
即使在这种情况下总需求仅为2字节(1 + 3 + 4 + 1 + 3 + 2 + 2),所使用的数据类型(unsigned int
)的大小为4字节。所以分配的内存也是4字节。如果只想分配2个字节,请使用unsigned short
作为数据类型并再次运行程序。
答案 1 :(得分:6)
来自ISO C标准:
实现可以分配任何足够大的可寻址存储单元来保存位 - 领域。 (以及之后)结构或联合的末尾可能有未命名的填充。
因此,不需要总是为结构选择尽可能小的内存块。由于32位字可能是编译器的原生大小,因此它就是它所选择的。
答案 2 :(得分:2)
位域在内存中的位置不仅取决于编译器如何决定分配结构中的各个字段,还取决于运行所在机器的字节序。让我们一个接一个。可以通过指定字段的大小(如@DDD)指出,也可以通过另一种机制来控制编译器中字段的分配。您可以告诉编译器pack
您的结构,或者让它更适合编译器如何针对您正在编译的机器体系结构进行优化。使用packed
type attribute指定打包。因此,如果将结构指定为:
struct __attribute__ ((__packed__)) box_props {
...
}
你可能会在内存中看到不同的布局。请注意,通过检查结构组件,您将看不到布局的不同 - 它可能会更改内存中的布局。在与其他东西进行通信时,打包结构至关重要,例如IO设备需要特定位置的特定位。
位字段结构的第二个问题是它们的布局依赖于字节序。内存中结构的布局(或任何相关数据)取决于您是否在big-endian (POWER) or little-endian (e.g., x86)机器上运行。一些系统(例如,嵌入式PowerPC系统是双端的)。
一般来说,位字段使端口代码变得非常困难,因为你对内存中的数据布局充满了希望。
希望这有帮助!
答案 3 :(得分:2)
由于某些原因我无法理解,C标准的实现者决定指定一个数字类型以及一个位域应该分配足以容纳该数字类型的空间,除非前一个字段是一个位域,由同一类型分配,它有足够的空间来处理下一个场。
对于您的特定示例,在具有16位无符号短路的计算机上,您应该将位域中的声明更改为unsigned short。碰巧的是,unsigned char也可以工作,并产生相同的结果,但情况并非总是如此。如果最佳打包的位域跨越字符边界但不跨越短边界,则将位字段声明为unsigned char
将需要填充以避免这种跨越。
虽然有些处理器可以毫无困难地为跨越存储单元边界的位域生成代码,但是现在的C标准将禁止以这种方式打包它们(再次,由于我无法理解的原因)。例如,在具有典型8/16/32/64位数据类型的机器上,编译器不允许程序员指定包含8个三字节字段的3字节结构,因为字段必须跨越字节边界。我可以理解规范不是要求编译器来处理跨越边界的字段,或者要求以某种特定方式布置位域(如果可以指定某个特定方式,我会认为它们更有用) bitfield应该例如使用某个位置的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;
将从bar
读取前64位,然后使用更新的值写入bar
的前64位。如果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寄存器操作以与编译器无关的方式完成(尽管这些代码显然是特定于硬件的。)