一:结构体
1.结构体对齐规则:
规则4: 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
例子4:
//结构体嵌套结构体
struct S3
{
double d;// 8 8 8
char c; // 1 8 1
int i; // 4 8 4
};
int main()
{
struct S4
{
char c1; //1 8 1
struct S3 s3;//
double d; //8 8 8
};
printf("%zd\n", sizeof(struct S4));
return 0;
}
解析:c1大小为1个字节,vs默认对齐数为8,所以c1的对齐数为1,占用一个字节大小;
由规则4(规则4: 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍)可知:结构体S4中嵌套定义了一个结构体S3并且定义了结构体变量s3 ,s3包括double d,char c,int i三个成员,所以s3的对齐数为该结构体中成员的最大对齐数的整数倍,s3会跳过1,2,3,4,5,6,7的内存空间开始存放在第8个偏移量的位置,共占用16个字节大小;
d大小为8个字节,vs默认对齐数为8,所以d的对齐数为8,共占用8个字节大小;
此时结构体S4的占用内存大小为32,32是包括嵌套结构体成员对齐数中所以结构体S4的最大对齐数8的整数倍,所以32就是结构体S4的大小。(结构体S3成员的对齐数分别是8 ,1, 4;结构体S4成员的对齐数分别为1, 8;所以成员的最大对齐数为8)
如下图:
2.为什么存在内存对齐
(1). 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2). 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
例子:
int main()
{
struct S1
{
char c1;//1 8 1
int i; //4 8 4
char c2;//1 8 1
};
struct S2
{
char c1;//1 8 1
char c2;//1 8 1
int i; //4 8 4
};
printf("%zd\n", sizeof(struct S1));
printf("%zd\n", sizeof(struct S2));
}
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的大小有了一些区别
S1如图:
S2如图:
3.修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
例子:
//vs默认对齐数8的情况
struct S
{
char c1;//1 8 1
int i; //4 8 4
char c2;//1 8 1
};
int main()
{
printf("%zd\n", sizeof(struct S));
}
其中:#pragma pack(1) 的意思是设置默认对齐数为1,#pragma pack()的意思是取消设置的对齐数,还远为默认
//修改对齐数为1
#pragma pack(1)
struct S
{
char c1;//1 1 1
int i; //4 1 1
char c2;//1 1 1
};
#pragma pack()
int main()
{
printf("%zd\n", sizeof(struct S));
return 0;
}
如下图:
4.结构体实现位段
1). 什么是位段
位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字
S就是位段
struct S
{
int _a ;//4个字节---32个bit位
int _b ;//4个字节---32个bit位
int _c ;//4个字节---32个bit位
int _d ;//4个字节---32个bit位
};
//位段式结构体(位代表二进制位)
struct S
{
int _a : 2;//只占2个bit位
int _b : 5;//只占5个bit位
int _c : 10;//只占10个bit位
int _d : 30;//只占30个bit位
};
int main()
{
printf("%zd\n", sizeof(struct S));
return 0;
}
位段的大小:
解析:结构体成员_a变量冒号后面加上一个数字代表该结构体成员是位段 ,_a后面的数字2代表着其会在内存中占用2个bit位;由于_a为int类型,所以内存会一次性开辟4个字节(32个bit位)大小;结构体成员_b变量冒号后面加上一个数字代表该结构体成员是位段 ,_b后面的数字5代表着其会在内存中占用5个bit位;结构体成员_c变量冒号后面加上一个数字代表该结构体成员是位段 ,_c后面的数字10代表着其会在内存中占用10个bit位;但是由于此时d会占用30个bit位此时一个int类型的剩余内存只有15个bit位不足d继续存放在这个int内存中,根据vs默认编译器当内存不足时会浪费掉剩余的空间重新开辟新的内存空间,此时会浪费剩余的15个空间重新开辟1个int类型的内存空间用来存放d的内存;结构体成员_d变量冒号后面加上一个数字代表该结构体成员是位段 ,_d后面的数字30代表着其会在内存中占用30个bit位;因此共开辟了(4 + 4 = 8)8个字节内存的空间,位段的大小就是8个字节。
提醒:位段在进行内存分配时会产生很多分歧,例如内存是从左向右存放还是从右向左存放,当内存不足于存放下一个位段时是浪费掉剩余的内存还是重新开辟内存空间不同的编译器是不同的。
在vs编译器上内存是从右向左存放,内存不足于存放下一个位段时会浪费掉剩余的内存。
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;//10的二进制:0000 1010
s.b = 12;//12的二进制:0000 1100
s.c = 3; //3的二进制: 0000 0011
s.d = 4; //4的二进制: 0000 0100
printf("%zd\n", sizeof(struct S));
return 0;
}
解析: :结构体成员a变量冒号后面加上一个数字代表该结构体成员是位段 ,a后面的数字3代表着其会在内存中占用3个bit位;由于a为char类型,所以内存会一次性开辟1个字节(8个bit位)大小;此时a从左向右占用内存3个bit位大小,因为我们为a初始化为10,10的二进制表示为:
0000 1010,因此会将010存放在内存中;
b后面的数字4代表着其会在内存中占用4个bit位,此时存放好a的内存后内存还剩余5个bit位足以存放b 4个bit位大小的内存,所以b继续存放在这个char类型开辟的空间中;因为我们为b初始化为12,12的二进制表示为:0000 1100,因此会将1100存放在内存中;
存放好b后此时内存空间只剩余1个bit位大小不足以存放c的内存,所以这1个bit位会被浪费,重新开辟1个字节(8个bit位)大小空间用来存放c,c后面的数字5代表着其会在内存中占5个bit位,因为我们为c初始化为3,3的二进制表示为:0000 0011,因此会将0 0011存放在内存中;
存放好c后此时内存空间只剩余2个bit位大小不足以存放d的内存,所以这2个bit位会被浪费,重新开辟1个字节(8个bit位)大小空间用来存放d,d后面的数字4代表着其会在内存中占4个bit位,因为我们为d初始化为4,4的二进制表示为:0000 0100,因此会将0100存放在内存中;
所以我们共向内存申请开辟了3个字节大小内存,所以位段大小为3个字节。
2).位段的内存分配
1. 位段的成员可以是 int,unsigned int , signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。