结构体(含位域)的sizeof 对齐相关

本文深入解析了结构体的sizeof属性及其字节对齐机制,通过实例展示了sizeof的计算过程,以及如何通过编译器的pack指令调整对齐方式,最终提供了关于结构体大小计算的一般公式和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载自:http://blog.youkuaiyun.com/hyljqr/article/details/500899


结构体的sizeof
先看一个结构体:http://www.cwba.com.cn
struct S1
{
    char c;
    int i;
};

http://www.shishangmeinv.com.cn

 sizeof(s1)在VC6中按默认设置得到的结果为8。
我们先看看sizeof的定义——sizeof的结果等于对象或者类型所占的内存字节数,好吧,那就让我们来看看S1的内存分配情况:http://www.cncaijing.com.cn
S1 s1 = { 'a', 0xFFFFFFFF };
定义上面的变量后,加上断点,运行程序,观察s1所在的内存,你发现了什么
以我的VC6.0(sp6)为例,s1的地址为0x0012FF78,其数据内容如下:
0012FF78: 61 CC CC CC FF FF FF FF
发现了什么怎么中间夹杂了3个字节的CC看看MSDN上的说明:
When applied to a structure type or variable, sizeof returns the actual size, which may include padding bytes inserted for alignment.
原来如此,这就是传说中的字节对齐啊!一个重要的话题出现了。
为什么需要字节对齐计算机组成原理教导我们这样有助于加快计算机的取数速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。这样,两个数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
让我们交换一下S1中char与int的位置:
struct S2
{
    int i;
    char c;
};
看看sizeof(S2)的结果为多少,怎么还是8再看看内存,原来成员c后面仍然有3个填充字节,这又是为什么啊别着急,下面总结规律。

字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
对于上面的准则,有几点需要说明:
1) 前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢因为有了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。想想为什么。

结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在stddef.h中定义,如下:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
例如,想要获得S2中c的偏移量,方法为size_t pos = offsetof(S2, c);// pos等于4
2) 基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。这里叙述起来有点拗口,思考起来也有点挠头,还是让我们看看例子吧(具体数值仍以VC6为例,以后不再说明):
struct S3
{
    char c1;
    S1 s;
    char c2
};
S1的最宽简单成员的类型为int,S3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型为int,这样,通过S3定义的变量,其存储空间首地址需要被4整除,整个sizeof(S3)的值也应该被4整除。c1的偏移量为0,s的偏移量呢这时s是一个整体,它作为结构体变量也满足前面三个准则,所以其大小为8,偏移量为4,c1与s之间便需要3个填充字节,而c2与s之间就不需要了,所以c2的偏移量为12,算上c2的大小为13,13是不能被4整除的,这样末尾还得补上3个填充字节。最后得到sizeof(S3)的值为16。
通过上面的叙述,我们可以得到一个公式:
结构体的大小等于最后一个成员的偏移量加上其大小再加上末尾的填充字节数目,即:

sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( tr
ailing padding )

到这里,朋友们应该对结构体的sizeof有了一个全新的认识,但不要高兴得太早,有一个影响sizeof的重要参量还未被提及,那便是编译器的pack指令。它是用来调整结构体对齐方式的,不同编译器名称和用法略有不同,VC6中通过#pragma pack实现,也可以直接修改/Zp编译开关。#pragma pack的基本用法为:#pragma pack( n ),n为字节对齐数,其取值为1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
offsetof( item ) = min( n, sizeof( item ) )
再看示例:
#pragma pack(push) // 将当前pack设置压栈保存
#pragma pack(2)// 必须在结构体定义之前使用
struct S1
{
    char c;
    int i;
};
struct S3
{
    char c1;
    S1 s;
    char c2
};
#pragma pack(pop) // 恢复先前的pack设置
计算sizeof(S1)时,min(2, sizeof(i))的值为2,所以i的偏移量为2,加上sizeof(i)等于6,能够被2整除,所以整个S1的大小为6。同样,对于sizeof(S3),s的偏移量为2,c2的偏移量为8,加上sizeof(c2)等于9,不能被2整除,添加一个填充字节,所以sizeof(S3)等于10。
现在,朋友们可以轻松的出一口气了,:)
还有一点要注意,“空结构体”(不含数据成员)的大小不为0,而是1。试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。如下:
struct S5 { };
sizeof( S5 ); // 结果为1

含位域结构体的sizeof:
前面已经说过,位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。
C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。
使用位域的主要目的是压缩存储,其大致规则为:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字
段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字
段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方
式,Dev-C++采取压缩方式;
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

还是让我们来看看例子。
示例 1
struct BF1
{
    char f1 : 3;
    char f2 : 4;
    char f3 : 5;
};
其内存布局为:
 |__f1___|____f2___ |__|____f3______|______|
 |__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|

位域类型为 char ,第 1 个字节仅能容纳下 f1 f2 ,所以 f2 被压缩到第 1 个字节中,而 f3
能从下一个字节开始。因此 sizeof(BF1) 的结果为 2
示例 2
struct BF2
{
    char f1 : 3;
    short f2 : 4;
    char f3 : 5;
};
由于相邻位域类型不同,在 VC6 中其 sizeof 6 ,在 Dev-C++ 中为 2
示例 3
struct BF3
{
    char f1 : 3;
    char f2;
    char f3 : 5;
};
非位域字段穿插在其中,不会产生压缩,在 VC6 Dev-C++ 中得到的大小均为 3
----
---
<think>嗯,用户这次聚焦在SOME/IP协议中结构体对齐处理,这是个相当专业的嵌入式通信问题。从用户引用标准条款[PRS_SOMEIP_00611]来看,ta应该正在开发符合AUTOSAR标准的车用ECU通信模块,且遇到了序列化的具体实现难题。 用户可能正在调试某个包状态标志结构体,比如车辆控制单元里的紧凑型状态寄存器(1字节包8个布尔标志)。这类场景下既要节省带宽又要满足对齐要求,确实棘手。ta特意强调“不是最后一个成员”的引用条款,暗示可能遇到了结构体中间引发的填充问题。 需要特别注意三个技术矛盾点:首先是天然的非字节对齐特性与SOME/IP强制4字节对齐的矛盾;其次是内存紧凑性要求(如Autosar强调不自动填充)与实际传输必须补的矛盾;最后是C/C++实现依赖编译器而SOME/IP需要确定性的矛盾。 从引用[3]提到的“肉眼解析报文”需求看,用户可能需要手动验证序列化结果。这提示我除了给出规范,还要提供可操作的验证方案。另外用户前次问题涉及动态数组,这次转向,说明ta在系统性地解决复杂结构体序列化问题,后续可能会问联合体(union)的处理。 专业层面要重点解释:①如何将分散重组为可对齐的整型(示例中的uint8_t方案),②重组时的大小端隐患(引用[3]强调过),③长度字段的包关系(引用[2]的lenth计算规则)。特别要警告编译器差异风险——比如GCC和IAR的内存布局可能不同,这会导致ECU间通信灾难。 安全方面必须强调:车辆控制信号(如刹车标志)若因解析错误可能引发事故,所以示例代码特意添加静态断言和运行时检查。最后保留的验证工具问题,其实是引导用户走向CANoe等专业工具,毕竟手动验证风险太高。</think>在SOME/IP协议中处理结构体(bit-field)的序列化对齐时,需遵循以下核心规则和实现策略: ### 一、对齐基本原则 1. **整体对齐** 被视为其底层整数类型(如`uint32`)的扩展,**整个结构必须满足4字节对齐**(偏移量$offset \mod 4 = 0$)[^1][^2]。 ```c typedef struct { uint32_t status : 8; // 低8 uint32_t mode : 3; // 后续3 uint32_t : 5; // 保留5(填充至16边界) uint32_t code : 16; // 高16 } BitFieldStruct; // 总大小=4字节(视为uint32) ``` 2. **非末尾的填充要求** 当后紧跟非成员时,需插入填充使后续成员满足对齐: ```c typedef struct { uint8_t header; // 1字节 @偏移0 uint32_t flags : 16; // 16 // 此处自动插入2字节填充(因后续id需4字节对齐) uint32_t id; // 4字节 @偏移4(满足4对齐) } MixedStruct; // 总大小=1+2()+1(隐式填充)+4=8字节 ``` > 依据:[PRS_SOMEIP_00611] 非末尾成员后需插入对齐元素[^1] ### 二、关键处理策略 1. **打包规则** - 同类型成员可连续打包(如示例1) - 跨类型(如`uint8`与`uint16`混合)需按最大类型对齐: ```c typedef struct { uint8_t a : 4; // uint8类型 uint16_t b : 12; // uint16类型 → 整体按2字节对齐 uint8_t pad[1]; // 填充1字节(使总大小为4的倍数) } MixedBitField; // 总大小=2()+1(填充)=3→4字节(向上对齐) ``` 2. **动态长度配置** 结构体需显式配置长度字段(Length Field),确保接收方能正确解析: ```c typedef struct { uint32_t length; // 长度字段(4字节) uint8_t type : 4; uint8_t priority : 2; uint8_t reserved : 2; // 总大小=1字节 uint8_t pad[3]; // 填充至4字节边界 } DynamicStruct; // 长度字段值=4(length自身除外) ``` > 长度字段值 = 结构体总大小 - sizeof(length)[^2] ### 三、实现验证方法 1. **静态断言检查** 使用编译时断言确保结构体大小符合预期: ```c static_assert(sizeof(BitFieldStruct) == 4, "BitFieldStruct size mismatch"); ``` 2. **运行时偏移校验** 验证非成员的偏移量: ```c MixedStruct s; assert((uintptr_t)&s.id % 4 == 0); // 必须满足4字节对齐 ``` 3. **网络字节序转换** 需按底层整型进行端序转换(大端序)[^3]: ```c void serialize_bitfield(BitFieldStruct* bf) { uint32_t raw_value = *(uint32_t*)bf; uint32_t net_value = htonl(raw_value); // 主机序→网络序 memcpy(buffer, &net_value, 4); } ``` ### 四、特殊场景处理 | **场景** | **解决方案** | |-------------------------|------------------------------------------| | 跨越整型边界 | 拆分为两个独立+填充 | | 结构体末尾 | 无需填充,但需确保整体大小4字节对齐[^1] | | 与动态数组共存 | 后需填充至4字节边界再放置长度字段[^2]| > ⚠️ **重要提示**: > 不同编译器对内存布局存在差异(如MSVC与GCC),需通过`#pragma pack`显式指定对齐方式,并在ARXML中明确定义结构[^1]。 --- ### 示例:完整结构体序列化 ```c #pragma pack(push, 1) typedef struct { uint32_t length; // 长度字段 @偏移0(4字节) uint16_t : 4; // 保留 uint16_t engineOn : 1; uint16_t brake : 1; uint16_t gear : 3; // 总大小=2字节 uint8_t pad[2]; // 填充至4字节边界(因length后需4对齐) float rpm; // 4字节 @偏移8 } VehicleStatus; // 总大小=4+2+2+4=12 → length=8(扣除自身) #pragma pack(pop) // 序列化流程: // 1. 计算length = sizeof(VehicleStatus) - sizeof(length) = 8 // 2. 转整型:uint16_t bits = (gear<<3) | (brake<<2) | (engineOn<<1) // 3. 转换大端序:length=htonl(8), bits=htons(bits), rpm=htonf(rpm) ``` 验证:`rpm`偏移量=8,满足$8 \mod 4 = 0$[^1][^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值