在C/C++开发中,结构体是一种常用的数据结构形式,在某些应用场景中,需要特别关注结构体对齐问题。
本篇就来通过一个实际例子,来探究结构体对齐的具体表现以及结构体对齐应该怎么计算。
1 结构体对齐问题
举个例子,一个结构体中,有多个成员,那结构体的整体空间占用大小,等于各个成员大小的累加和吗?再进一步,结构体套结构体,最外面结构体的整体大小,等于各个成员结构体大小的累加和吗?
这就涉及到了结构体对齐问题,如果结构体没对齐,就会出现累加和和整体的大小不一样的情况,那这些对齐具体是怎样的情况,就是本篇要讨论的。
在写代码之前,先来简单介绍下sizeof与offsetof。
1.1 sizeof
sizeof 是一个C 语言关键字/运算符,用于计算某个数据类型、变量或表达式在内存中占用的总字节数,编译时就会确定结果,无运行时开销。
sizeof结果类型是size_t,无符号整数,打印用%zu格式符- 结构体的
sizeof结果会包含内存填充(padding),也就是结构体对齐 - 数组名用
sizeof会计算整个数组的字节数(sizeof(name[10])=10),而数组名传参后会退化为指针,sizeof(指针)的大小就是4(32位系统)或8(64位系统)
1.2 offsetof
offsetof 是 <stddef.h> 中定义的宏 ,用于计算结构体中某个成员相对于结构体起始地址的字节偏移量,即该成员距离结构体开头有多少字节。因为是宏,也是编译时计算
| sizeof | offsetof | |
|---|---|---|
| 本质 | 关键字(运算符) | 宏(基于地址计算) |
| 作用 | 计算 "总字节数"(含填充) | 计算 "成员相对于结构体开头的偏移" |
| 适用对象 | 类型、变量、表达式 | 仅结构体 / 联合体的成员 |
| 结果含义 | 占用的总内存大小 | 成员的内存偏移位置 |
| 依赖头文件 | 无需(内置关键字) | 必须包含 <stddef.h> |
2 代码实测
2.1 实例代码-整体sizeof与累加sizeof
首先定义3个结构体,每个结构体包含一些自定义的变量:
- DataA_t
- DataB_t
- DataC_t
然后再定义一个大的结构DataAll_t,包含DataA_t、DataB_t、DataC_t,外加一个char数组的data_d
为了便于通过循环的方式来展示结构体成员的大小,这里定义了一个StructMemberInfo_t结构:
char *name:结构体成员的名称,需要手动填入size_t offset:结构体成员的偏移量,使用offsetof计算size_t size:结构体成员的大小,使用sizeof计算
将所有成员的信息,写入结构体数组后,就可以进行循环打印展示,在循环的过程中,计算各个成sizeof的累加和,最后和整体的sizeof进行对比,观察是否大小一致。
c
// gcc 1_calc_size.c -o 1_calc_size
#include <stdio.h>
#include <stddef.h>
typedef struct{
int m1;
float m2;
char m3[2];
}DataA_t;
typedef struct{
char n1[3];
double n2;
}DataB_t;
typedef struct{
char x1;
char x2[2];
}DataC_t;
typedef struct{
DataA_t data_a;
DataB_t data_b;
DataC_t data_c;
char data_d[1];
}DataAll_t;
typedef struct{
char * name;
size_t offset;
size_t size;
}StructMemberInfo_t;
void show_struct_member_size_info(DataAll_t *data)
{
StructMemberInfo_t info[] = {
{"data_a", offsetof(DataAll_t, data_a), sizeof(data->data_a)},
{"data_b", offsetof(DataAll_t, data_b), sizeof(data->data_b)},
{"data_c", offsetof(DataAll_t, data_c), sizeof(data->data_c)},
{"data_d", offsetof(DataAll_t, data_d), sizeof(data->data_d)},
};
int num = sizeof(info) / sizeof(info[0]);
size_t sum_size = 0;
size_t total_size = sizeof(DataAll_t);
printf("datap:%p\t total_size:%zu\n", (void *)data, total_size);
for (int i = 0; i < num; i++)
{
void *ptr = (void *)data + info[i].offset;
sum_size += info[i].size;
printf("[%d] p:%p\t name:%s\t offset:%zu\t size:%zu\t sum_size:%zu\n",
i, ptr, info[i].name, info[i].offset, info[i].size, sum_size);
}
if (sum_size == total_size)
{
printf("struct data member size check ok, sum_size:%zu, total_size:%zu\n", sum_size, total_size);
}
else
{
printf("struct data member size check fail, sum_size:%zu, total_size:%zu\n", sum_size, total_size);
}
}
int main()
{
DataAll_t data_all;
show_struct_member_size_info(&data_all);
return 0;
}
运行结果如下,可以看到是不一致的,说明存在字节对齐的现象:

2.2 增加结构体成员的地址打印
那具体是怎样的对齐,我们可以把各个成员的地址打印出来:
show_struct_member_addr:将成员的地址打印出来show_struct_align:将使用的字节对齐方式打印出来
c
// gcc 2_calc_size.c -o 2_calc_size
#include <stdio.h>
#include <stddef.h>
typedef struct{
int m1;
float m2;
char m3[2];
}DataA_t;
typedef struct{
char n1[3];
double n2;
}DataB_t;
typedef struct{
char x1;
char x2[2];
}DataC_t;
typedef struct{
DataA_t data_a;
DataB_t data_b;
DataC_t data_c;
char data_d[1];
}DataAll_t;
typedef struct{
char * name;
size_t offset;
size_t size;
}StructMemberInfo_t;
void show_struct_member_size_info(DataAll_t *data)
{
StructMemberInfo_t info[] = {
{"data_a", offsetof(DataAll_t, data_a), sizeof(data->data_a)},
{"data_b", offsetof(DataAll_t, data_b), sizeof(data->data_b)},
{"data_c", offsetof(DataAll_t, data_c), sizeof(data->data_c)},
{"data_d", offsetof(DataAll_t, data_d), sizeof(data->data_d)},
};
int num = sizeof(info) / sizeof(info[0]);
size_t sum_size = 0;
size_t total_size = sizeof(DataAll_t);
printf("datap:%p\t total_size:%zu\n", (void *)data, total_size);
for (int i = 0; i < num; i++)
{
void *ptr = (void *)data + info[i].offset;
sum_size += info[i].size;
printf("[%d] p:%p\t name:%s\t offset:%zu\t size:%zu\t sum_size:%zu\n",
i, ptr, info[i].name, info[i].offset, info[i].size, sum_size);
}
if (sum_size == total_size)
{
printf("struct data member size check ok, sum_size:%zu, total_size:%zu\n", sum_size, total_size);
}
else
{
printf("struct data member size check fail, sum_size:%zu, total_size:%zu\n", sum_size, total_size);
}
}
void show_struct_member_addr(DataAll_t *data)
{
printf("====== data:%p\n", (void *)data);
printf("------ data_a:%p\n", (void *)&data->data_a);
printf("data_a,m1:%p\n", (void *)&data->data_a.m1);
printf("data_a,m2:%p\n", (void *)&data->data_a.m2);
printf("data_a,m3:%p\n", (void *)&data->data_a.m3);
printf("------ data_b:%p\n", (void *)&data->data_b);
printf("data_b,n1:%p\n", (void *)&data->data_b.n1);
printf("data_b,n2:%p\n", (void *)&data->data_b.n2);
printf("------ data_c:%p\n", (void *)&data->data_c);
printf("data_c,x1:%p\n", (void *)&data->data_c.x1);
printf("data_c,x2:%p\n", (void *)&data->data_c.x2);
printf("------ data_d:%p\n", (void *)&data->data_d);
}
void show_struct_align(DataAll_t *data)
{
printf("====== data align:%zu\n", __alignof(DataAll_t));
printf("------ data_a align:%zu\n", __alignof(data->data_a));
printf("------ data_b align:%zu\n", __alignof(data->data_b));
printf("------ data_b align:%zu\n", __alignof(data->data_c));
printf("------ data_d align:%zu\n", __alignof(data->data_d));
}
int main()
{
DataAll_t data_all;
show_struct_member_addr(&data_all);
printf("\n");
show_struct_align(&data_all);
printf("\n");
show_struct_member_size_info(&data_all);
return 0;
}
运行结果如下:

可以看到,最外层结构体是8字节对齐,内部的子结构体,存在多种对齐:
- DataA_t:4字节对齐,int和float的大小是4
- DataB_t:8字节对齐,double的大小是8
- DataC_t:1字节对齐,char的大小是1
再根据各个成员的地址,可以画出如下示例图,看出字节对齐对应的内存填充(padding)的位置:
- 例如4字节对齐的data_a,从前到后,如果存在连续的成员组合起来的大小不是4的倍数,则进行内存填充,确保组合后是4的倍数,data_a的最后一个成员,只剩2了,所以要在末尾填充2,所以sizeof(data_a)的12
- 对于8字节对齐的data_b,第一个成员是3,和后面的成员组合在一起,也不是8的倍数,所以需要先在第1个成员后添加5
- 对于8字节对齐data_all,显示需要在data_a后补4个,凑够了8,data_b是8的倍数,不用管,后面的data_c和data_d,加在一起还不够8,所以最后再补上4

2.3 增加对齐的计算
经过上述的分析,可以对show_struct_member_size_info增加字节对齐中内存填充大小的计算,然后再验证累加的大小(包括字节填充)与整体的大小是否一致,代码如下:
c
// gcc 3_calc_size.c -o 3_calc_size
#include <stdio.h>
#include <stddef.h>
typedef struct{
int m1;
float m2;
char m3[2];
}DataA_t;
typedef struct{
char n1[3];
double n2;
}DataB_t;
typedef struct{
char x1;
char x2[2];
}DataC_t;
typedef struct{
DataA_t data_a;
DataB_t data_b;
DataC_t data_c;
char data_d[1];
}DataAll_t;
typedef struct{
char * name;
size_t offset;
size_t size;
}StructMemberInfo_t;
void show_struct_member_size_info(DataAll_t *data)
{
StructMemberInfo_t info[] = {
{"data_a", offsetof(DataAll_t, data_a), sizeof(data->data_a)},
{"data_b", offsetof(DataAll_t, data_b), sizeof(data->data_b)},
{"data_c", offsetof(DataAll_t, data_c), sizeof(data->data_c)},
{"data_d", offsetof(DataAll_t, data_d), sizeof(data->data_d)},
};
int num = sizeof(info) / sizeof(info[0]);
size_t sum_size = 0;
size_t total_size = sizeof(DataAll_t);
printf("datap:%p\t total_size:%zu\n", (void *)data, total_size);
for (int i = 0; i < num; i++)
{
void *ptr = (void *)data + info[i].offset;
// sum_size加上字节对齐的值
if (i > 0)
{
size_t padding = info[i].offset - sum_size;
if (padding > 0)
{
printf("[%d] name:%s need add padding:%zu(info[%d].offset:%zu, sum_size:%zu)\n",
i-1, info[i-1].name, padding, i, info[i].offset, sum_size);
sum_size += padding;
}
}
sum_size += info[i].size;
printf("[%d] p:%p\t name:%s\t offset:%zu\t size:%zu\t sum_size:%zu\n",
i, ptr, info[i].name, info[i].offset, info[i].size, sum_size);
}
// 最后加上字节对齐的值
{
size_t align = __alignof(data);
if (sum_size % align != 0)
{
size_t padding = align - (sum_size % align);
sum_size += padding;
printf("in the end, need add padding:%zu\n", padding);
}
}
if (sum_size == total_size)
{
printf("struct data member size check ok, sum_size:%zu, total_size:%zu\n", sum_size, total_size);
}
else
{
printf("struct data member size check fail, sum_size:%zu, total_size:%zu\n", sum_size, total_size);
}
}
void show_struct_member_addr(DataAll_t *data)
{
printf("====== data:%p\n", (void *)data);
printf("------ data_a:%p\n", (void *)&data->data_a);
printf("data_a,m1:%p\n", (void *)&data->data_a.m1);
printf("data_a,m2:%p\n", (void *)&data->data_a.m2);
printf("data_a,m3:%p\n", (void *)&data->data_a.m3);
printf("------ data_b:%p\n", (void *)&data->data_b);
printf("data_b,n1:%p\n", (void *)&data->data_b.n1);
printf("data_b,n2:%p\n", (void *)&data->data_b.n2);
printf("------ data_c:%p\n", (void *)&data->data_c);
printf("data_c,x1:%p\n", (void *)&data->data_c.x1);
printf("data_c,x2:%p\n", (void *)&data->data_c.x2);
printf("------ data_d:%p\n", (void *)&data->data_d);
}
void show_struct_align(DataAll_t *data)
{
printf("====== data align:%zu\n", __alignof(DataAll_t));
printf("------ data_a align:%zu\n", __alignof(data->data_a));
printf("------ data_b align:%zu\n", __alignof(data->data_b));
printf("------ data_b align:%zu\n", __alignof(data->data_c));
printf("------ data_d align:%zu\n", __alignof(data->data_d));
}
int main()
{
DataAll_t data_all;
show_struct_member_addr(&data_all);
printf("\n");
show_struct_align(&data_all);
printf("\n");
show_struct_member_size_info(&data_all);
return 0;
}
运行结如下,可以看到最终计算一致了:

3 总结
本篇探究了C/C++中结构体的字节对齐问题,通过一个简单的示例,展示字节对齐的实际现象,以及通过打印出地址,说明结构体成员是如何存储的。