很久之前在书上看过位段这个词,用在结构体中,我在实际编程中从来没用过。今天在做编程练习的时候定义结构体突然想到了位段,然后具体怎么用的实在是想不起来了,好奇心使我越来越坐不住了,然后我就翻开了之前看的书。废话不多说,本来写博客的目的就是为了让自己忘记的时候回来看一看。。。。。
对于什么是结构体我就不多说了,直接写我要记住的内容:
结构体的自引用
如何在一个结构体内部包含一个类型为该结构体本身的成员呢?先举个例子看看:
struct SELF_REF1{
int a;
struct SELF_REF1 b;
int c;
};
以上这种类型的自引用是非法的,因为成员b是另一个完整的结构体,其内部还将包含它自己的成员b。这第2个成员又是另一个完整的结构,它还将包含它自己的成员b。这样重复下去永无止境,就像是没有退出条件的递归。但是下边这个声明确是合法的:
struct SELF_REF1{
int a;
struct SELF_REF1 *b;
int c;
};
什么区别?就是多了一个星号(*),为什么这样就合法了?原因是b现在是一个指针,而不是一个结构体。其指针的长度在结构体确定之前就已经明确了,所以这种类型的自引用是合法的。
如果你觉得一个结构体内部包含一个指向该结构体本身的指针有些奇怪,请记住,它实际上所指的不是该结构体,而是同一类型的不同结构体。这种应用在链表和树中是最常用的。
注意:注意下边这个结构声明:
typedef struct {
int a;
struct SELF_REF1 *b;
int c;
}SELF_REF1;
这个声明的目的是为这个结构体创建类型名SELF_REF1。但是它失败了。类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。
解决方案就是定义一个结构体标签来解决,如下所示:
typedef struct SELF_REF1_TAG{
int a;
struct SELF_REF1_TAG *b;
int c;
}SELF_REF1;
不完整的声明
偶尔,我们可能会声明一些相互之间有依赖关系的结构体。也就是说,其中的一个结构包含了另一个结构的一个或多个成员。和自引用结构一样,至少有一个结构必须在另一个结构内部以指针的形式存在。问题在于声明部分:如果每个结构都引用了其他结构的标签,那么,因果循环,总得有一个标签首先被声明,所以,怎么做才能使其不相互矛盾?
解决方案就是使用不完整声明,它声明一个作为结构标签的标识符。然后,我们可以把这个标签用在不需要知道结构长度的声明中,如声明指向这个结构的指针,那么指针的长度是确定的,指针指向的类型也确定了。这样就不会发生类型未定义的情况,如下边的声明:
struct B;
struct A{
int a;
struct B *b;
};
struct B{
int b;
struct A *a;
};
在A的成员列表中需要B结构的不完整声明。一旦A被声明了,那在B中成员列表声明A也不会出问题了。
结构的存储分配
结构体声明之后在内存中存储并不是严格按顺序存储下去的,而是由一个边界对齐的要求,至于什么是边界对齐,下边我就要开始引用了。。。
结构体边界对齐规则:
1.第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 为编译器默认的一个对齐数与该成员大小中的较小值。vs中默认值是8 Linux默认值为4(当然可以通过#pragma pack()修改),但修改只能设置成1,2,4,8,16。
3.结构体总大小为最大对齐数的整数倍。(每个成员变量都有自己的对齐数)
4.如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。
可能上边4条规则不是太好理解,那我再引用几个例子来说明吧。。。
结果是8,我们来分析一下为什么结果是 8。c1是char型,占一个字节,第一个成员即 c1 在结构体变量偏移量为0 的地址处。
c2是char型,占一个字节,要对齐到对齐数的整数倍的位置。对齐数 = 编译器默认的一个对齐数与该成员大小中的较小值,vs中默认值是8,取较小值1,char类型的对齐数是1,所以对齐到1 的整数倍,那就是偏移量为1开始的地址空间。
i是int类型,占四个字节,要对齐到对齐数的整数倍的位置。int类型的对齐数就是 4,所以对齐到4 的整数倍。
再来一个例子:
结果是12,来看一下过程。c1是char型,占一个字节,对应到结构体变量偏移量为0 的地址处。i是int型,占四个字节,对齐数就是4,对齐到4的整数倍位置处,即偏移量为4开始的地址空间。
c2是char型,占一个字节,对齐到1 的整数倍,那就是下一个地址空间,对齐到偏移量为8的地址空间。结构体总大小为最大对齐数的整数倍,所以为对齐数4的整数倍,现在已经用了9个字节的空间,那么总大小就是12个字节空间。所以输出结果是12。
例3:
结果是32,我们来看一下分析:根据上面讲解的容易得出struct S3占16个字节。那我们来看一下struct S4的大小,struct S4中有三个成员变量,第一个char型,占一个字节,对齐到偏移量为0的地址处。
第二个成员是结构体嵌套使用,结构体S3变量s3,刚才已经得出占16个字节,所以第二个成员对齐数是16,又因为对齐数是编译器默认数与成员对齐数中的较小值,vs默认对齐数是8,取较小值8,所以对齐到偏移量为8的地址空间处。
第三个成员是double型,占8个字节,对应到8的整数倍即偏移量24的地址处。结构体总大小是最大对齐数8的整数倍,所以是32。
提示:
有时,我们有充分的理由,决定不对结构体的成员进行重排以减少因对齐带来的空间损失。例如,我们可能想把相关的结构成员存储在一起,提高程序的可维护性和可读性。但是,如果不存在这样的理由,结构体的成员应该根据它们的边界需要进行重排,减少因边界对齐而造成的内存损失。
sizeof操作符能够得出一个结构体的整体长度,包括因边界对齐而跳过的那些字节。
位段
前边说过我在实际编程中没有用过位段,但是很多时候是可以用位段的。例如在定义一套数据传输协议时,肯定会有某个字节其中的哪一位或几位表示传输方向、传输数据标识之类的。也就是为了使传输协议的标识符更加精简,会用一个字节的各个位表示各种含义,这里是我自己的理解,感觉说的越来越乱了,直接引用例子吧:
struct CHAR{
unsigned ch : 7;
unsigned font : 6;
unsigned size : 19;
};
struct CHAR ch1;
这个声明取自一个文本格式化程序,它可以处理多达128个不同的字符值(需要7个位)、64种不同的字体(需要6个位)以及0到524287个单位的长度。这个size位段过于庞大,无法容纳于一个短整型,但其余的位段都比一个字符还短。位段使程序员能够利用存储ch和font所剩余的位来增加size的位数,这样就避免了声明一个32位的整数来存储size位段。
其在使用的时候可以这样:
赋值:
ch1.ch = new_ch;
ch1.font = new_font;
ch1.size = new_size;
或者
ch1 = (uint32_t)new_ch1;
读取:
new_ch = ch1.ch;
new_font = ch1.font;
new_size = ch1.size;
或者
uint32_t new_ch1 = ch1;
联合
和结构体相比,联合可以说是另一种动物了。联合的声明和结构体类似,但它的行为方式却和结构体不同。联合的所有成员引用的是内存种的相同位置。当你想在不同的时刻把不同的东西存储于通一个位置时,就可以使用联合。
举例说明:
struct VARIABLE{
enum {INT,FLOAT,STRING} type;
union{
int i;
float f;
char *s;
}value;
}x_t;
x_t x;//定义结构体
x.type = X.INT;
x.value.i = 1;
现在,对于整型变量,你将在type字段设置位INT,并把整型值存储于value.i字段。对于浮点值,你将使用value.f。当以后得到这个变量的值时,对type字段进行检查来决定使用哪个值字段。
如果联合的各个成员具有不同的长度,联合的长度就是它最长成员的长度。
还有一种情况可以不用分配最长成员的长度,就是在联合中定义指针,这样,不用纠结哪个成员的长度为多少。不过在实际使用时还是需要分配给指针指向的存储空间,不过那时你已经知道数据的长度了,不是吗?
以上是我对之前看过内容的再一次记忆,希望对各位有帮助,但主要还是为了我自己忘记时方便查看,哈哈。。。。