结构体内存默认分配
问大家一个问题:
struct STUDENT
{
char a;
int b;
}data;
如上结构体变量 data 占多少字节?char 占 1 字节,int 占 4 字节,所以总共占 5 字节吗?我们写一个程序验证一下:
# include <stdio.h>
struct STUDENT
{
char a;
int b;
}data;
int main(void)
{
printf("%p, %p\n", &data.a, &data.b); //%p是取地址输出控制符
printf("%d\n", sizeof(data));
return 0;
}
输出结果是:
00427E68, 00427E6C
8
我们看到 data 不是占 5 字节,而是占 8 字节。变量 a 的地址是从 00427E68 到 00427E6B,占 4字 节;变量 b 的地址是从 00427E6C 到 00427E6F,也占 4 字节。b 占 4 字节我们能理解,但 a 是 char 型,char 型不是占 1 字节吗,这里为什么占 4 字节?其实不是它占了 4 字节,它占的还是 1 字节,只不过结构体中有一个字节对齐的概念。
什么叫字节对齐?我们知道结构体是一种构造数据类型,里面可以有不同数据类型的成员。在这些成员中,不同的数据类型所占的内存空间是不同的。那么系统是怎么给结构体变量的成员分配内存的呢?或者说这些成员在内存中是如何存储的呢?通过上面这个例子我们知道肯定不是顺序存储的。
那么到底是怎么存储的呢?就是按字节对齐的方式存储的!即以结构体成员中占内存最多的数据类型所占的字节数为标准,所有的成员在分配内存时都要与这个长度对齐。我们举一个例子:我们以上面这个程序为例,结构体变量 data 的成员中占内存最多的数据类型是 int 型,其占 4 字节的内存空间,那么所有成员在分配内存时都要与 4 字节的长度对齐。也就是说,虽然 char 只占 1 字节,但是为了与 4 字节的长度对齐,它后面的 3 字节都会空着,即:
a | 空 | 空 | 空 |
---|---|---|---|
b | b | b | b |
所谓空着其实也不是里面真的什么都没有,它就同定义了一个变量但没有初始化一样,里面是一个很小的、负的填充字。为了便于表达,我们就暂且称之为空好了。
如果结构体成员为:
struct STUDENT
{
char a;
char b;
int c;
}data;
那么这三个成员是怎么对齐的?a 和 b 后面都是空 3 字节吗?不是!如果没有 b,那么 a 后面就空 3 字节,有了 b 则 b 就接着 a 后面填充。即:
a | b | 空 | 空 |
---|---|---|---|
c | c | c | c |
所以这时候结构体变量 data 仍占 8 字节。我们写一个程序验证一下:
# include <stdio.h>
struct STUDENT
{
char a;
char b;
int c;
}data;
int main(void)
{
printf("%p, %p, %p\n", &data.a, &data.b, &data.c); //%p是取地址输出控制符
printf("%d\n", sizeof(data));
return 0;
}
输出结果是:
00427E68, 00427E69, 00427E6C
8
这时我们发现一个问题:所有成员在分配内存的时候都与 4 字节的长度对齐,多个 char 类型时是依次往后填充,但是 char 型后面的 int 型为什么不紧接着后面填充?为什么要另起一行?也就是说,到底什么时候是接在后面填充,什么时候是另起一行填充?
我们说,所有的成员在分配内存时都要与所有成员中占内存最多的数据类型所占内存空间的字节数对齐。假如这个字节数为 N,那么对齐的原则是:理论上所有成员在分配内存时都是紧接在前一个变量后面依次填充的,但是如果是“以 N 对齐”为原则,那么,如果一行中剩下的空间不足以填充某成员变量,即剩下的空间小于某成员变量的数据类型所占的字节数,则该成员变量在分配内存时另起一行分配。
下面再来举一个例子,大家觉得下面这个结构体变量data占多少字节?
struct STUDENT
{
char a;
char b;
char c;
char d;
char e;
int f;
}data;
首先最长的数据类型占 4 字节,所以是以 4 对齐。然后 a 占 1 字节,b 接在 a 后面占 1 字节,c 接在 b 后面占 1 字节,d 接在 c 后面占 1 字节,此时满 4 字节了,e 再来就要另起一行。f 想紧接着 e 后面分配,但 e 后面还剩 3 字节,小于 int 类型的 4 字节,所以 f 另起一行。即该结构体变量分配内存时如下:
a | b | c | d |
---|---|---|---|
e | 空 | 空 | 空 |
f | f | f | f |
即总共占 12 字节。我们写一个程序验证一下:
# include <stdio.h>
struct STUDENT
{
char a;
char b;
char c;
char d;
char e;
int f;
}data;
int main(void)
{
printf("%p, %p, %p, %p, %p, %p\n", &data.a, &data.b, &data.c, &data.d, &data.e, &data.f); //%p是取地址输出控制符
printf("%d\n", sizeof(data));
return 0;
}
输出结果是:
00427E68, 00427E69, 00427E6A, 00427E6B, 00427E6C, 00427E70
12
现在大家应该能掌握字节对齐的精髓了吧!下面给大家出一个题目试试掌握情况。我们将前面的结构体改一下:
struct STUDENT
{
char a;
int b;
char c;
}data;
即将原来第二个和第三个声明交换了位置,大家看看现在 data 变量占多少字节?没错,是 12 字节。首先最长类型所占字节数为 4,所以是以 4 对齐。分配内存的时候 a 占 1 字节,然后 b 想紧接着 a 后面存储,但 a 后面还剩 3 字节,小于 b 的 4 字节,所以 b 另起一行分配。然后 c 想紧接着 b 后面分配,但是 b 后面没空了,所以 c 另起一行分配。所以总共 12 字节。内存分配图如下所示:
a | 空 | 空 | 空 |
---|---|---|---|
b | b | b | b |
c | 空 | 空 | 空 |
下面写一个程序验证一下:
# include <stdio.h>
struct STUDENT
{
char a;
int b;
char c;
}data;
int main(void)
{
printf("%p, %p, %p\n", &data.a, &data.b, &data.c); //%p是取地址输出控制符
printf("%d\n", sizeof(data));
return 0;
}
输出结果是:
00427E68, 00427E6C, 00427E70
12
我们看到,同样三个数据类型,只不过交换了一下位置,结构体变量data所占的内存空间就由8字节变成12字节,多了4字节。这就告诉我们,在声明结构体类型时,各类型成员的前后位置会对该结构体类型定义的结构体变量所占的字节数产生影响。没有规律的定义会增加系统给结构体变量分配的字节数,降低内存分配的效率。但这种影响对操作系统来说几乎是可以忽略不计的!所以我们在写程序的时候,如果有心的话,声明结构体类型时就按成员类型所占字节数从小到大写,或从大到小写。但是如果没有按规律书写的话也不要紧,声明结构体类型时并非一定要从小到大声明,只是为了说明“字节对齐”这个概念!而且有时候为了增强程序的可读性我们就需要没有规律地写,比如存储一个人的信息:
struct STUDENT
{
char name[10];
int age;
char sex;
float score;
}data;
正常的思维是将“性别”放在“年龄”后面,但如果为了内存对齐而交换它们的位置,总让人觉得有点别扭。所以我说“尽量”有规律地写!
这时又有人会提出一个问题:“上面这个结构体变量 data 中有成员 char name[10],长度最长,是 10,那是不是要以 10 对齐?”不是,char a[10] 的本质是 10 个 char 变量,所以就把它当成 10 个 char 变量看就行了。所以结构体变量 data 中成员最长类型占 4 字节,还是以 4 对齐。该结构体变量分配内存时情况如下:
name[0] | name[1] | name[2] | name[3] |
---|---|---|---|
name[4] | name[5] | name[6] | name[7] |
name[8] | name[9] | 空 | 空 |
age | age | age | age |
sex | 空 | 空 | 空 |
score | score | score | score |
总共 24 字节,我们写一个程序验证一下:
# include <stdio.h>
struct STUDENT
{
char name[10];
int age;
char sex;
float score;
}data;
int main(void)
{
printf("%p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p, %p\n", &data.name[0], &data.name[1], &data.name[2], &data.name[3], &data.name[4], &data.name[5], &data.name[6], &data.name[7], &data.name[8], &data.name[9], &data.age, &data.sex, &data.score);
printf("%d\n", sizeof(data));
return 0;
}
输出结果是:
00427E68, 00427E69, 00427E6A, 00427E6B, 00427E6C, 00427E6D, 00427E6E,
00427E6F, 00427E70, 00427E71, 00427E74, 00427E78, 00427E7C
24
字节对齐
数据对齐
1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2)硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。
1. 对齐原则:
【原则1】数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
【原则2】结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
【原则3】结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
备注:数组成员按长度按数组类型长度计算,如char t[9],在第1步中数据自身长度按1算,累加结构体时长度为9;第2步中,找最大数据长度时,如果结构体T有复杂类型成员A,该A成员的长度为该复杂类型成员A的最大成员长度。
小结:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
【注意】(对齐位数跟处理器位数和编译器都有关)VS, VC等编译器默认是#pragma pack(8),所以测试我们的规则会正常;注意gcc默认是#pragma pack(4),并且gcc只支持1,2,4对齐。套用三原则里计算的对齐值是不能大于#pragma pack指定的n值。
2. 自然对齐:
存放变量的地址要是该变量数据类型大小的整数倍。如:存放int型数据的地址一定要是4的倍数,存放short型数据的地址一定要是2的倍数。
3. 改变缺省的对界条件(指定对界):
- 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
- 使用伪指令#pragma pack(),取消自定义字节对齐方式。
3.1 举例一
例1:
#pragma pack(1)
struct AA
{
int a; //长度4 < 1 按1对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 = 1 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 > 1 按1对齐;偏移量为5;存放位置区间[5,6]
char d; //长度1 = 1 按1对齐;偏移量为6;存放位置区间[7]
//整体存放在[0~7]位置区间中,共八个字节。
};
#pragma pack()
结果:8个字节
例2:
pragma pack(2)
struct AA
{
int a; //长度4 > 2 按2对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 2 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 = 2 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
char d; //长度1 < 2 按1对齐;偏移量为7;存放位置区间[8];共九个字节
};
#pragma pack()
结果:10个字节
例3:
#pragma pack(4)
struct AA
{
int a; //长度4 = 4 按4对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 4 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 < 4 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
char d; //长度1 < 4 按1对齐;偏移量为7;存放位置区间[8];总大小为9
};
#pragma pack()
结果:12个字节
例4:
#pragma pack(8)
struct AA
{
int a; //长度4 < 8 按4对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 8 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 < 8 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
char d; //长度1 < 8 按1对齐;偏移量为7;存放位置区间[8],总大小为9
};
#pragma pack()
结果:12个字节
例5:
struct EE //8个字节对齐
{
int a; //长度4 < 8 按4对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 8 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 < 8 按2对齐;偏移量由5提升到6;存放位置区间[6,7]
struct FF //结构体内部最大元素为int,由于偏移量为8刚好是4的整数倍,所以从8开始存放接下来的struct FF
{
int a1; //长度4 < 8 按4对齐;偏移量为8;存放位置区间[8,11]
char b1; //长度1 < 8 按1对齐;偏移量为12;存放位置区间[12]
short c1; //长度2 < 8 按2对齐;偏移量为13,提升到2的倍数14;存放位置区间[14,15]
char d1; //长度1 < 8 按1对齐;偏移量为16;存放位置区间[16]
}; //整体对齐系数 = min((max(int,short,char), 8) = 4,将内存大小由17补齐到4的整数倍20
char d; //长度1 < 8 按1对齐;偏移量为21;存放位置区间[21]
//整体对齐系数 = min((max(int,short,char), 8) = 4,将内存大小由21补齐到4的整数倍24
};
struct B
{
char e[2]; //长度1 < 8 按2对齐;偏移量为0;存放位置区间[0,1]
short h; //长度2 < 8 按2对齐;偏移量为2;存放位置区间[2,3]
//结构体内部最大元素为double,偏移量为4,提升到8,所以从8开始存放接下来的struct A
struct A
{
int a; //长度4 < 8 按4对齐;偏移量为8;存放位置区间[8,11]
double b; //长度8 = 8 按8对齐;偏移量为12,提升到16;存放位置区间16,23]
float c; //长度4 < 8,按4对齐;偏移量为24,存放位置区间[24,27]
};
//整体对齐系数 = min((max(int,double,float), 8) = 8,将内存大小由28补齐到8的整数倍32
};
3.2举例二
代码1:
#include <stdio.h>
typedef struct
{
int aa1; //4个字节对齐 1111
char bb1; //1个字节对齐 1
short cc1; //2个字节对齐 011
char dd1; //1个字节对齐 1
}testlength1;
int length1 = sizeof(testlength1); //4个字节对齐,占用字节1111 1011 1000,length = 12
typedef struct
{
char bb2; //1个字节对齐 1
int aa2; //4个字节对齐 01111
short cc2; //2个字节对齐 11
char dd2; //1个字节对齐 1
}
testlength2;
int length2 = sizeof(testlength2); //4个字节对齐,占用字节1011 1111 1000,length = 12
typedef struct
{
char bb3; //1个字节对齐 1
char dd3; //1个字节对齐 1
int aa3; //4个字节对齐 001111
short cc23; //2个字节对齐 11
}testlength3;
int length3 = sizeof(testlength3); //4个字节对齐,占用字节1100 1111 1100,length = 12
typedef struct
{
char bb4; //1个字节对齐 1
char dd4; //1个字节对齐 1
short cc4; //2个字节对齐 11
int aa4; //4个字节对齐 1111
} testlength4;
int length4 = sizeof(testlength4); //4个字节对齐,占用字节1111 1111,length = 8
int main(void)
{
printf("length1 = %d.\n",length1);
printf("length2 = %d.\n", length2);
printf("length3 = %d.\n", length3);
printf("length4 = %d.\n", length4);
return 0;
}
VS2017输出结果:
代码2:
#include <stdio.h>
#pragma pack(2)
typedef struct
{
int aa1; //2个字节对齐 1111
char bb1; //1个字节对齐 1
short cc1; //2个字节对齐 011
char dd1; //1个字节对齐 1
} testlength1;
int length1 = sizeof(testlength1); //2个字节对齐,占用字节11 11 10 11 10,length = 10
typedef struct
{
char bb2; //1个字节对齐 1
int aa2; //2个字节对齐 01111
short cc2; //2个字节对齐 11
char dd2; //1个字节对齐 1
} testlength2;
int length2 = sizeof(testlength2); //2个字节对齐,占用字节10 11 11 11 10,length = 10
typedef struct
{
char bb3; //1个字节对齐 1
char dd3; //1个字节对齐 1
int aa3; //2个字节对齐 11 11
short cc23; //2个字节对齐 11
}testlength3;
int length3 = sizeof(testlength3); //2个字节对齐,占用字节11 11 11 11,length = 8
typedef struct
{
char bb4; //1个字节对齐 1
char dd4; //1个字节对齐 1
short cc4; //2个字节对齐 11
int aa4; //2个字节对齐 11 11
}testlength4;
int length4 = sizeof(testlength4); //2个字节对齐,占用字节11 11 11 11,length = 8
int main(void)
{
printf("length1 = %d.\n", length1);
printf("length2 = %d.\n", length2);
printf("length3 = %d.\n", length3);
printf("length4 = %d.\n", length4);
return 0;
}
VS2017输出结果:
代码3:
#include<iostream>
using namespace std;
typedef struct bb
{
int id; //[0]....[3]
double weight; //[8].....[15] 原则1
float height; //[16]..[19],总长要为8的整数倍,补齐[20]...[23] 原则3
}BB;
typedef struct aa
{
char name[2]; //[0],[1]
int id; //[4]...[7] 原则1
double score; //[8]....[15]
short grade; //[16],[17]
BB b; //[24]......[47] 原则2
}AA;
int main()
{
AA a;
cout << sizeof(a) << " " << sizeof(BB) << endl;
return 0;
}
VS2017输出结果: 48 24
代码4:
#include<iostream>
using namespace std;
#pragma pack(2)
typedef struct bb
{
int id;
double weight;
float height;
}BB;
typedef struct aa
{
char name[2];
int id;
double score;
short grade;
BB b;
}AA;
int main()
{
AA a;
cout << sizeof(a) << " " << sizeof(BB) << endl;
return 0;
}
VS2017输出结果:32 16
转载:http://c.biancheng.net/view/243.html
以及:https://www.cnblogs.com/sunbines/p/9257981.html