C语言结构体对齐是怎么计算

在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++中结构体的字节对齐问题,通过一个简单的示例,展示字节对齐的实际现象,以及通过打印出地址,说明结构体成员是如何存储的。

相关推荐
mangge082 小时前
Arduino IDE开发ESP8266的离线配置
c语言
黎雁·泠崖2 小时前
吃透Java操作符入门:分类差异+进制转换+原反补码 核心前置知识(Java&C对比)
java·c语言·开发语言
天天摸鱼的java工程师2 小时前
volatile 关键字底层原理:为什么它不能保证原子性?
java·后端
钟良堂2 小时前
Java完整实现 MinIO 对象存储搭建+封装全套公共方法+断点上传功能
java·minio·断点上传
小杨同学492 小时前
C 语言实战:堆内存存储字符串 + 多种递归方案计算字符串长度
数据库·后端·算法
名字不好奇2 小时前
C++虚函数表失效???
java·开发语言·c++
小码编匠2 小时前
完美替代 Navicat,一款开源免费、集成了 AIGC 能力的多数据库客户端工具!
数据库·后端·aigc
linuxxx1102 小时前
正则匹配应用小案例
数据库·正则表达式
u0104058362 小时前
Java中的服务监控:Prometheus与Grafana的集成
java·grafana·prometheus