结构体字节的对齐
各类型占字节数
char: 1
short: 2
int: 4
long: 4
float: 4
double: 8
long long: 8
结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个对齐准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。
对于以上规则的说明如下:
第一条:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
第二条:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
第三条:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。
struct temp {#24
char a1;
double a2;
int a3;
};
假设从位置0开始 char占一字节,double占8字节根据规则2,要从位置8开始 到15,int占4字节从位置16开始到19。一共20字节,根据规则3,要为8的倍数,所以是24
struct temp{#此时为12
char a1;
int a2;
int a3;
};
struct temp{#此时也为12
char a1;
int a2[2];
};
结构体末端定义空数组
不分配内存,只记录空数组的位置!!!!
但是计算结构体内存要从开始位置到这个空数组的位置。
不占任何空间,指针需要占用4字节(32位)/8字节(64位)长度空间,空数组不占任何空间。
而结构体最后使用0长度数组的原因,主要是为了方便的管理内存缓冲区,如果你直接使用指针而不使用数组,那么,你在分配内存缓冲区时,就必须分配结构体一次,然后再分配结构体内的指针一次,(而此时分配的内存已经与结构体的内存不连续了,所以要分别管理即申请和释放)而如果使用数组,那么只需要一次就可以全部分配出来,反过来,释放时也是一样,使用数组,一次释放,使用指针,得先释放结构体内的指针,再释放结构体。还不能颠倒次序。
struct temp{#此时还为12 最后a4空数组不占内存
char a1;
int a2;
int a3;
int a4[0];
};
#include <iostream>
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
using namespace std;
//#pragma pack(4)
int main()
{
struct temp{
char a1;
char a2;
char a3;
double a4[0] ;
//double a3;
};
cout << sizeof(temp)<<endl;#8
cout<<OFFSET(temp, a1)<< endl;#0
cout<<OFFSET(temp, a2)<< endl;#1
cout<<OFFSET(temp, a3)<< endl;#2
cout<<OFFSET(temp, a4)<< endl;#8
return 0;
}
这里如果没有最后的a4,占用的内存为3,加上double型后,a4的头部位置需要从8开始,但是a4没有长度,所以从0-8一共占8个字节。
结构体中间定义空数组
按照改空数组类型进行准则2,但是不分配内存,所以下边成员从这个位置开始找自己的位置
#include <iostream>
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
using namespace std;
//#pragma pack(4)
int main()
{
struct temp{
char a1;
short a2[0] ;
double a3;
};
cout << sizeof(temp)<<endl;#16
cout<<OFFSET(temp, a1)<< endl;#0
cout<<OFFSET(temp, a2)<< endl;#2
cout<<OFFSET(temp, a3)<< endl;#8
return 0;
}
位域
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域
位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
其中位域列表的形式为: 类型说明符 位域名:位域长度
冒号后面跟着的就是位域
位域的对齐
如果结构体中含有位域(bit-field),那么VC中准则是:
-
如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小(以几字节为单位,不同类型值不一样,1,2,4,8……),则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
-
如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
-
如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;
系统会先为结构体成员按照对齐方式分配空间和填塞(padding),然后对变量进行位域操作。
struct temp{#4
unsigned a1:8;
unsigned a2:24;
};
此时满足第一种情况,紧邻存储,一共占32bit,输出为4字节
struct temp{#4
unsigned a1:8;
unsigned a2:20;
};
同样紧邻存储,28bit凑整,输出4字节
struct temp{#8
unsigned a1:24;
unsigned a2:24;
};
满足第二种情况
struct temp{
int a1:24;
char a2;
int a3;
long long a4;
};
此时temp占16个字节
因为a1占3个字节,[0,3),a2:[3,4),a3:[4,8) a4[8,16)
struct temp{
int a1:24;
double a2;
int a3;
long long a4;
占32个 a1[0,3)a2[8,16) a3[16,20) a4[24,32)
struct temp{#4
char a1:3;
int a2:4;
};
输出4字节,即使其可以连续存储,但也要满足结构体内存大小为最宽的倍数
#include <iostream>
using namespace std;
int main()
{
struct temp {
unsigned m;
unsigned n:4;
unsigned char ch:6;
};
cout << sizeof(temp) << endl;#8
return 0;
}
#include <iostream>
using namespace std;
int main()
{
struct temp {#24
int a1:24;
double a2;
int a3;
long long d[0];
};
cout << sizeof(temp) << endl;
return 0;
}
a1[0,3) a2[8,16) a3[16,20) d[24,24]
更改对齐方式
使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;
自定义对齐值后要用#pragma pack()来还原,否则会对后面的结构造成影响。
#pragma pack (n)的意思是告诉编译器字节对齐方式为n字节对齐,n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。
#include <iostream>
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
using namespace std;
#pragma pack(2)
int main()
{
struct temp{
char a1;
int a2;
short a3;
};
cout << sizeof(temp)<<endl;#8
cout<<OFFSET(temp, a1)<< endl;#0
cout<<OFFSET(temp, a2)<< endl;#2
cout<<OFFSET(temp, a3)<< endl;#6
return 0;
}
#pragma pack()
默认对齐方式下
#include <iostream>
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
using namespace std;
int main()
{
struct temp{
char a1;
short a2;
char a3;
int a4;
char a5[3];
};
cout << sizeof(temp)<<endl;#16
cout<<OFFSET(temp, a1)<< endl;#0
cout<<OFFSET(temp, a2)<< endl;#2
cout<<OFFSET(temp, a3)<< endl;#4
cout<<OFFSET(temp, a4)<< endl;#8
cout<<OFFSET(temp, a4)<< endl;#12
return 0;
}
四字节对齐
#include <iostream>
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
using namespace std;
#pragma pack(4)
int main()
{
struct temp{
char a1;
short a2;
char a3;
int a4;
char a5[3];
};
cout << sizeof(temp)<<endl;#16
cout<<OFFSET(temp, a1)<< endl;#0
cout<<OFFSET(temp, a2)<< endl;#2
cout<<OFFSET(temp, a3)<< endl;#4
cout<<OFFSET(temp, a4)<< endl;#8
cout<<OFFSET(temp, a5)<< endl;#12
return 0;
}
#pragma pack()
首先char a占用1个字节,没问题。
short b本身占用2个字节,根据上面准则2,需要在b和a之间填充1个字节。
char c占用1个字节,没问题。
int d本身占用4个字节,根据准则2,需要在d和c之间填充3个字节。
char e[3];本身占用3个字节,根据原则3,需要在其后补充1个字节。
因此,sizeof(temp) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字节。
#include <iostream>
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
using namespace std;
#pragma pack(2)
int main()
{
struct temp{
char a1;
short a2;
char a3;
int a4;
char a5[3];
};
cout << sizeof(temp)<<endl;#14 一共13,取2的倍数
cout<<OFFSET(temp, a1)<< endl;#[0,1)
cout<<OFFSET(temp, a2)<< endl;#[2,4)
cout<<OFFSET(temp, a3)<< endl;#[4,5)
cout<<OFFSET(temp, a4)<< endl;#[6,10)
cout<<OFFSET(temp, a5)<< endl;#[10,13)
return 0;
}
#pragma pack()
1字节对齐 15
2字节对齐 16
4字节对齐 16
8字节对齐 24
#include <iostream>
using namespace std;
#pragma pack(1)
int main()
{
struct temp {#15
int a1:24;
double a2;
int a3;
long long d[0];
};
cout << sizeof(temp) << endl;
return 0;
}