LV025-结构体大小
一、字节对齐
1. 字节对齐是什么?

前面介绍结构体在内存中分布的时候有上图的情况, 出现了一些 "空" 的地址空间,所谓 "空" 其实也不是里面真的什么都没有,它就同定义了一个变量但没有初始化一样,里面是一个很小的、负的填充字。为了便于表达,我们就暂且称之为 "空" 好了。
字节对齐也可以叫内存对齐,计算机中内存大小的基本单位是字节( byte ),理论上 来讲,可以从 任意地址访问 某种基本数据类型,但是 实际上,计算机 并非逐字节大小读写内存,而是以 2 , 4 , 或 8 的倍数的字节块来读写内存,如此一来就会对基本数据类型的合法地址作出一些限制,即它的地址必须是 2 , 4 或 8 的倍数。
这种对齐限制简化了形成处理器和存储器系统之间的接口的硬件设计。对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做 自然对齐。例如在 32 位 CPU 下,假设一个整型变量的地址为 0x00000004 ,那它就是自然对齐的。再比如存放 int 型数据的地址一定要是 4 的倍数,存放 short 型数据的地址一定要是 2 的倍数。
2. 为什么要字节对齐?
根本在于为了让 CPU 更有效率地访问内存空间。例如,假设一个处理器总是从存储器中取出 8 个字节,则地址必须为 8 的倍数。如果我们能保证将所有的 double 类型数据的地址对齐成 8 的倍数,那么就可以用一个存储器操作来读或者写值了。如果对象被分放在两个 8 字节存储块中,横跨两个区域,我们就需要执行两次存储器访问操作。
举个例子,假设一个 int 整型变量的地址不是自然对齐:
如果变量在 0x00000002 地址,则 CPU 如果取它的值的话需要访问两次内存,第一次取从 0x00000002-0x00000003 的一个 short 长度的数据,第二次取从 0x00000004-0x00000005 的一个 short 长度的数据,然后组合得到所要的数据;
如果变量在 0x00000003 地址上的话则要访问三次内存,第一次为 char 长度,第二次为 short 长度,第三次为 char 长度,然后组合得到整型数据。
如果变量在自然对齐位置上,则只要一次就可以取出数据。
3. 对齐原则?
3.1 #pragma pack
3.1.1 基本格式
#pragma pack 是一个预处理指令,主要用作改变编译器的默认对齐方式。基本使用方法如下:
#pragma pack(n) /* 设置编辑器按照 n 个字节对齐,n 一定是 2 的幂次方,如 1,2,4,8,16 */
#pragma pack() /* 取消自定义字节对齐方式。 */如果没有通过宏那么在 32 位 Linux 主机上 默认指定对齐值为 4, 64 位的默认对齐值为 8, AMR CPU 默认指定对齐值为 8。
还有其他的用法,暂时用到的不过,后边有机会再专门总结一下吧。
3.1.2 使用实例
#include <stdio.h>
#pragma pack(1) /* 字节对齐改成了 1 个字节 */
/* 定义结构体数据类型 */
struct Student
{
char *name; /* 姓名 */
char gender; /* 性别 */
int age; /* 年龄 */
char id[3]; /* 学号 */
float score; /* 成绩 */
}; /* ; 不可缺少 */
#pragma pack() /* 取消自定义字节对齐 */
struct Test
{
char *name; /* 姓名 */
char gender; /* 性别 */
int age; /* 年龄 */
char id[3]; /* 学号 */
float score; /* 成绩 */
}; /* ; 不可缺少 */
int main(int argc, char *argv[])
{
struct Student stu1 = {"qidaink", 'm', 18, "01", 95.8}; /* 定义结构体变量 */
printf("sizeof(stu1) = %ld\n", sizeof(stu1));
printf("sizeof( struct Student) = %ld\n", sizeof(struct Student));
printf("sizeof(stu1.name) = %ld\n", sizeof(stu1.name));
printf("sizeof(stu1.gender) = %ld\n", sizeof(stu1.gender));
printf("sizeof(stu1.age) = %ld\n", sizeof(stu1.age));
printf("sizeof(stu1.id) = %ld\n", sizeof(stu1.id));
printf("sizeof(stu1.score) = %.ld\n\n", sizeof(stu1.score));
struct Test stu2 = {"qidaink", 'm', 18, "01", 95.8}; /* 定义结构体变量 */
printf("sizeof(stu2) = %ld\n", sizeof(stu2));
printf("sizeof( struct Student) = %ld\n", sizeof(struct Student));
printf("sizeof(stu2.name) = %ld\n", sizeof(stu2.name));
printf("sizeof(stu2.gender) = %ld\n", sizeof(stu2.gender));
printf("sizeof(stu2.age) = %ld\n", sizeof(stu2.age));
printf("sizeof(stu2.id) = %ld\n", sizeof(stu2.id));
printf("sizeof(stu2.score) = %.ld\n\n", sizeof(stu2.score));
return 0;
}在终端执行以下命令:
gcc test.c -Wall # 编译链接程序
./a.out # 执行可执行文件会看到有如下信息输出:
sizeof(stu1) = 20
sizeof( struct Student) = 20
sizeof(stu1.name) = 8
sizeof(stu1.gender) = 1
sizeof(stu1.age) = 4
sizeof(stu1.id) = 3
sizeof(stu1.score) = 4
sizeof(stu2) = 24
sizeof( struct Student) = 20
sizeof(stu2.name) = 8
sizeof(stu2.gender) = 1
sizeof(stu2.age) = 4
sizeof(stu2.id) = 3
sizeof(stu2.score) = 4我们会发现,相同的结构体属性,但是对齐方式不同,占用的空间也就不一样了。
3.2 几个原则
【原则 1】 数据成员对齐规则:结构( struct )或联合( union )的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员的对齐按照 #pragma pack 指定的数值和这个数据成员自身长度中,比较小的那个进行。
【原则 2】 结构或联合的整体对齐规则:在数据成员完成各自对齐之后,结构或联合本身也要进行对齐,对齐将按照 #pragma pack 指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
【原则 3】 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
备注:数组成员按长度按数组类型长度计算,如 char t [9], 在第 1 步中数据自身长度按 1 算,累加结构体时长度为 9;第 2 步中,找最大数据长度时,如果结构体 T 有复杂类型成员 A,该 A 成员的长度为该复杂类型成员 A 的最大成员长度。
【注意】
(1)当 #pragma pack 的 n 值等于或超过所有数据成员长度的时候,这个 n 值的大小将不产生任何效果。
(2)对齐位数跟处理器位数和编译器都有关。 VS , VC 等编译器默认是 #pragma pack(8) ,所以测试我们的规则会正常;注意 gcc 默认是 #pragma pack(4) ,并且 gcc 只支持 1 , 2 , 4 对齐。套用三原则里计算的对齐值是不能大于 #pragma pack 指定的 n 值。
这样上边结构体的成员分布也就很清楚明白了。
二、结构体大小计算规则
1. 成员的数据类型都相同
结构体占用空间=成员个数 × 成员数据类型的长度。
2. 成员的数据类型不相同
2.1 计算方法
类型不相同的时候,按结构体默认的字节对齐一般满足三个准则:
- (1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
- (2) 结构体每个成员相对于结构体首地址的偏移量( offset )都是成员大小的整数倍( 0 被认为是任何数的整数倍),如有需要编译器会在成员之间加上填充字节( internal adding );
- (3)当没有定义 #pragma pack(value) 这种指定 value 字节进行对齐时,整体的大小在满足为最大数据类型所占字节的倍数下要达到所占内存最小(最大数据类型所占字节数的最小整数倍)。当定义了 #pragma pack(value) ,以 value 字节进行对齐时,它整体的大小必须为 value 的最小整数倍。如有需要编译器会在最末一个成员之后加上填充字节( trailing padding )。
【方法】后来发现,其实可以按照下边的方法来计算:
(1)首先找出成员变量中最大数据类型占用的字节数 Lmax ;
(2)所有成员分析时都按照 Lmax 为准,先画一个含有 Lmax 个字节空位,然后各个成员按照大小开始往上填;
(3)成员相对于首地址偏移量需要是成员类型大小的整数倍,不是整数倍的可以在前边补空字节,若这个成员大小加上偏移量超过 Lmax ,则移到下一行(再画一个含有 Lmax 字节的空位);
2.2 使用实例
struct
{
char a;
char b;
int c;
double d;
} S1;
3. 特殊的结构体
对于一个没有任何成员的结构体,在 C 中占用空间为 0 ,而在 C++ 中它占用的空间为 1 字节,这是因为 struct 在 C++ 中相当于类。
三、实例分析
1. 相同成员结构体
#include <stdio.h>
struct TEST1
{
int a;
int b;
int c[2];
};
struct TEST2
{
char a;
char b;
char c[2];
};
int main(int argc, char *argv[])
{
struct TEST1 A;
struct TEST2 B;
printf("sizeof(A)=%ld,sizeof(A.a)=%ld,sizeof(A.b)=%ld,sizeof(A.c)=%ld\n", sizeof(A), sizeof(A.a), sizeof(A.b), sizeof(A.c));
printf("&A =%p\n&A.a =%p\n&A.b =%p\n&A.c[0]=%p\n&A.c[1]=%p\n", &A, &A.a, &A.b, &A.c[0], &A.c[1]);
printf("sizeof(B)=%ld,sizeof(B.a)=%ld,sizeof(B.b)=%ld,sizeof(B.c)=%ld\n", sizeof(B), sizeof(B.a), sizeof(B.b), sizeof(B.c));
printf("&B =%p\n&B.a =%p\n&B.b =%p\n&B.c[0]=%p\n&B.c[1]=%p\n", &B, &B.a, &B.b, &B.c[0], &B.c[1]);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sizeof(A)=16,sizeof(A.a)=4,sizeof(A.b)=4,sizeof(A.c)=8
&A =0x7ffe9c0871e0
&A.a =0x7ffe9c0871e0
&A.b =0x7ffe9c0871e4
&A.c[0]=0x7ffe9c0871e8
&A.c[1]=0x7ffe9c0871ec
sizeof(B)=4,sizeof(B.a)=1,sizeof(B.b)=1,sizeof(B.c)=2
&B =0x7ffe9c0871dc
&B.a =0x7ffe9c0871dc
&B.b =0x7ffe9c0871dd
&B.c[0]=0x7ffe9c0871de
&B.c[1]=0x7ffe9c0871df2. 特殊结构体
#include <stdio.h>
struct TEST1
{
};
int main(int argc, char *argv[])
{
struct TEST1 A;
printf("sizeof(A)=%ld\n", sizeof(A));
printf("&A=%p\n", &A);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sizeof(A)=0
&A=0x7fff18b054573. 成员不同的结构体
3.1 实例1
3.1.1 测试示例
#include <stdio.h>
struct
{
char a;
short b;
char c;
} S1;
struct
{
char a;
char b;
short c;
} S2;
int main(int argc, char *argv[])
{
printf("sizeof(S1)=%ld,sizeof(S1.a)=%ld,sizeof(S1.b)=%ld,sizeof(S1.c)=%ld\n",
sizeof(S1), sizeof(S1.a), sizeof(S1.b), sizeof(S1.c));
printf("&S1=%p \n&S1.a=%p \n&S1.b=%p \n&S1.c=%p \n",
&S1, &S1.a, &S1.b, &S1.c);
printf("sizeof(S2)=%ld,sizeof(S2.a)=%ld,sizeof(S2.b)=%ld,sizeof(S2.c)=%ld\n",
sizeof(S2), sizeof(S2.a), sizeof(S2.b), sizeof(S2.c));
printf("&S2=%p \n&S2.a=%p \n&S2.b=%p \n&S2.c=%p \n",
&S2, &S2.a, &S2.b, &S2.c);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sizeof(S1)=6,sizeof(S1.a)=1,sizeof(S1.b)=2,sizeof(S1.c)=1
&S1=0x5576e7e5b012
&S1.a=0x5576e7e5b012
&S1.b=0x5576e7e5b014
&S1.c=0x5576e7e5b016
sizeof(S2)=4,sizeof(S2.a)=1,sizeof(S2.b)=1,sizeof(S2.c)=2
&S2=0x5576e7e5b018
&S2.a=0x5576e7e5b018
&S2.b=0x5576e7e5b019
&S2.c=0x5576e7e5b01a3.1.2 结构体大小分析
我们来分析一下这两个结构体:
struct
{
char a;
short b;
char c;
} S1;
struct
{
char a;
char b;
short c;
} S2;- S1 结构体
(1)成员 a : char 类型,相对于首地址偏移量为 0 ,占据 1 字节空间;
(2)成员 b : short 类型,此时前边有一个 char 类型,这就意味着该成员相对于首地址的偏移量为 1 ,而 short 类型占据 2 个字节,所以前边需要补一个字节,这就导致该成员的偏移量变为 2 ,然后再占据 2 个字节的空间;
(3)成员 c : char 类型,相对于首地址的偏移量为 2+2=4 ,是成员大小的整数倍,并占据 1 字节空间。
所以目前一共就是 2+2+1=5 字节,但是,根据第三个原则,结构体的总大小需要是结构体成员中最大数据类型所占字节数的整数倍,也就是 2 的整数倍,所以需要在成员 c 后边补一个字节,这样整个结构体就占据了 6 个字节,即:

- S2 结构体
(1)成员 a : char 类型,相对于首地址偏移量为 0 ,占据 1 字节空间;
(2)成员 b : char 类型,此时前边有一个 char 类型,这就意味着该成员相对于首地址的偏移量为 1 ,而 char 类型占据 1 个字节,偏移量是成员大小的整数倍,不需要补字节;
(3)成员 c : short 类型,相对于首地址的偏移量为 1+1=2 ,是成员大小的整数倍,并占据 2 字节空间。
所以一共就是 1+1+2=4 字节,即:

3.2 实例2
3.2.1 测试示例
#include <stdio.h>
struct
{
char a;
char b;
double c;
short d;
} S1;
int main(int argc, char *argv[])
{
printf("sizeof(S1)=%ld,sizeof(S1.a)=%ld,sizeof(S1.b)=%ld,sizeof(S1.c)=%ld, sizeof(S1.d)=%ld\n",
sizeof(S1), sizeof(S1.a), sizeof(S1.b), sizeof(S1.c), sizeof(S1.d));
printf("&S1=%p \n&S1.a=%p \n&S1.b=%p \n&S1.c=%p \n&S1.d=%p\n",
&S1, &S1.a, &S1.b, &S1.c, &S1.d);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sizeof(S1)=24,sizeof(S1.a)=1,sizeof(S1.b)=1,sizeof(S1.c)=8, sizeof(S1.d)=2
&S1=0x55da00a7c020
&S1.a=0x55da00a7c020
&S1.b=0x55da00a7c021
&S1.c=0x55da00a7c028
&S1.d=0x55da00a7c0303.2.2 结构体大小分析
我们来分析一下这个结构体:
struct
{
char a; /* # */
char b; /* # */
double c; /* ######## */
short d; /* ##000000 */
} S1;- S1 结构体
(1)成员 a : char 类型,相对于首地址偏移量为 0 ,占据 1 字节空间;
(2)成员 b : char 类型,此时前边有一个 char 类型,这就意味着该成员相对于首地址的偏移量为 1 ,是成员大小的整数倍;
(3)成员 c : double 类型,相对于首地址的偏移量为 1+1=2 ,不是成员大小的整数倍,它的大小为 8 字节,所以前边补 6 个字节,那么这个成员的实际偏移量是 8 了,然后再占据 8 字节空间。
(4)成员 d : short 类型,相对于首地址偏移量为 8+8=16 ,是成员大小的整数倍。
所以目前一共就是 1+1+6+8+2=18 字节,但是根据第三条规则,结构体的总大小需要是结构体成员中最大数据类型所占字节数的整数倍,也就是 8 的整数倍,所以最少需要在成员 d 后边补 6 个字节,这样整个结构体就占据了 24 个字节,即

3.3 实例3
3.3.1 测试示例
#include <stdio.h>
struct
{
char a;
int b;
char c[5];
} S1;
int main(int argc, char *argv[])
{
printf("sizeof(S1)=%ld,sizeof(S1.a)=%ld,sizeof(S1.b)=%ld,sizeof(S1.c)=%ld\n",
sizeof(S1), sizeof(S1.a), sizeof(S1.b), sizeof(S1.c));
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sizeof(S1)=16,sizeof(S1.a)=1,sizeof(S1.b)=4,sizeof(S1.c)=5
&S1=0x562315725020
&S1.a=0x562315725020
&S1.b=0x562315725024
&S1.c=0x5623157250283.3.2 结构体大小分析
我们来分析一下这个结构体:
struct
{
char a;
int b;
char c[5];
} S1;- S1 结构体
(1)成员 a : char 类型,相对于首地址偏移量为 0 ,占据 1 字节空间;
(2)成员 b : int 类型,此时前边有一个 char 类型,这就意味着该成员相对于首地址的偏移量为 1 ,这就不是成员大小的整数倍了,前边补 3 个字节, b 的实际偏移量变成 4 然后再占据 4 字节空间;
(3)成员 c : char 类型数组,包含 5 个成员,成员大小为 5 字节,相对于首地址的偏移量为 4+4=8 ,偏移量是数据类型的整数倍。
所以目前一共就是 1+3+4+5=13 字节,但是根据第三条规则,结构体的总大小需要是结构体成员中最大数据类型所占字节数的整数倍,也就是 4 的整数倍,所以最少需要在成员 c 后边补 3 个字节,这样整个结构体就占据了 16 个字节。
3.4 实例4
3.4.1 测试示例
#include <stdio.h>
struct
{
char a;
int b;
struct
{
char c;
int d;
double e;
} s;
float f;
} S1;
int main(int argc, char *argv[])
{
printf("sizeof(S1) =%2ld, addr=%p\n",sizeof(S1), &S1);
printf("sizeof(S1.a) =%2ld, addr=%p\n",sizeof(S1.a), &S1.a);
printf("sizeof(S1.b) =%2ld, addr=%p\n",sizeof(S1.b), &S1.b);
printf("sizeof(S1.s) =%2ld, addr=%p\n",sizeof(S1.s), &S1.s);
printf("sizeof(S1.s.c)=%2ld, addr=%p\n",sizeof(S1.s.c), &S1.s.c);
printf("sizeof(S1.s.d)=%2ld, addr=%p\n",sizeof(S1.s.d), &S1.s.d);
printf("sizeof(S1.s.e)=%2ld, addr=%p\n",sizeof(S1.s.e), &S1.s.e);
printf("sizeof(S1.f) =%2ld, addr=%p\n",sizeof(S1.f), &S1.f);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sizeof(S1) =32, addr=0x556695193040
sizeof(S1.a) = 1, addr=0x556695193040
sizeof(S1.b) = 4, addr=0x556695193044
sizeof(S1.s) =16, addr=0x556695193048
sizeof(S1.s.c)= 1, addr=0x556695193048
sizeof(S1.s.d)= 4, addr=0x55669519304c
sizeof(S1.s.e)= 8, addr=0x556695193050
sizeof(S1.f) = 4, addr=0x5566951930583.4.2 结构体大小分析
我们来分析一下这个结构体:
struct
{
char a;
int b;
struct
{
char c;
int d;
double e;
} s;
float f;
} S1;- S1 结构体

3.5 实例5
3.5.1 测试示例
该实例使用了 __attribute__ 机制,设置了 __packed__ 属性。
#include <stdio.h>
struct __attribute__((__packed__))
{
char a;
int b;
struct
{
char c;
int d;
double e;
} s;
float f;
} S1;
int main(int argc, char *argv[])
{
printf("sizeof(S1) =%2ld, addr=%p\n",sizeof(S1), &S1);
printf("sizeof(S1.a) =%2ld, addr=%p\n",sizeof(S1.a), &S1.a);
printf("sizeof(S1.b) =%2ld, addr=%p\n",sizeof(S1.b), &S1.b);
printf("sizeof(S1.s) =%2ld, addr=%p\n",sizeof(S1.s), &S1.s);
printf("sizeof(S1.s.c)=%2ld, addr=%p\n",sizeof(S1.s.c), &S1.s.c);
printf("sizeof(S1.s.d)=%2ld, addr=%p\n",sizeof(S1.s.d), &S1.s.d);
printf("sizeof(S1.s.e)=%2ld, addr=%p\n",sizeof(S1.s.e), &S1.s.e);
printf("sizeof(S1.f) =%2ld, addr=%p\n",sizeof(S1.f), &S1.f);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sizeof(S1) =25, addr=0x556da3484020
sizeof(S1.a) = 1, addr=0x556da3484020
sizeof(S1.b) = 4, addr=0x556da3484021
sizeof(S1.s) =16, addr=0x556da3484025
sizeof(S1.s.c)= 1, addr=0x556da3484025
sizeof(S1.s.d)= 4, addr=0x556da3484029
sizeof(S1.s.e)= 8, addr=0x556da348402d
sizeof(S1.f) = 4, addr=0x556da3484035这样的话,结构体的大小就直接是各个变量大小之和了。
参考资料: