存储和传输/探究结构数据(C/C++结构体)在内存中的对齐和填充规则

概述

本文深入探讨了C/C++语言中结构体数据在内存中的实际存储细节,尤其关注结构体对齐问题、对齐规则和填充规则,并分别在大小端字节序系统环境中进行了相关实践验证。正文将主要围绕如下问题展开讨论,如,
如何定义一个结构体类型,才能兼顾空间和效率?(根本目标)
使用强制1字节对齐,是否会损失数据访问效率?(被证明是过度担心了)
定义结构体类型时,为什习惯把较长类型的字段靠前放置?必须要这样吗? (可以继续保持这个习惯)
结构体字段自然对齐后,额外的字节被填在了什么位置上,高地址还是低地址? (从低到高地址填充)
结构体对象及其字段的对齐规则和填充规则在小端系统和大端系统上,是否保持一致?(Yes)

@History
在CSDN草稿里,8年前就第一次关注结构体对齐问题了,记录的是sizeof(T)结果与预期不符合。后来读到一些编程规范,有具体条款上建议将结构体中的长字节字段靠前排放,或建议去人为地填充字节,以使得结构体各字段存储在对齐的地址上。近来在整理《语言基础/分析和实践 C&C++ 位域结构数据类型》和《网络通信/大小端字节序由来、决定因素、给编程带来的困扰》等文章时,无可避免的又钻到了字节对齐的问题中,勾起来以前的诸多疑问,于是多管齐下,耗费5个晚上,整理输出此文。

转载请标明出处,
https://blog.csdn.net/quguanxin/category_6223029.html

结构体的对齐规则

我暂时认为,只有结构数据才有字节对齐问题值得讨论。单一数据类型,无论是单字节还是多字节,虽然也有变量地址对齐发生,但那通常不会让人产生疑问,也没有必要进行相关讨论。在 “C和指针” 书中提到,C提供了两种类型的聚合数据类型,数组和结构。其中数组是相同数据类型,本质上是单一数据类型。多唠一点,在书中,是如下描述结构和聚合数据类型的,
数据经常以成组的形式存在,例如,雇主必须明了每位员工的姓名、年龄和工资。如果这些值能够存储到一起,访问起来就会简单一些。但是如果这些值的类型不同,它们无法存储于一个数组中。在C中,使用结构可以把不同类型的值存储在一起。聚合数据类型(aggregate data type)能够同时存储超过一个的单独数据。

为什么要对齐

计算机中内存大小的基本单位是字节,理论上来讲可从任意地址访问某种基本数据类型,但是实际上并非如此。主要原因在于,CPU处理器通常以固定宽度(例如32位或64位)从内存中读取数据。如果数据没有按照处理器的对齐要求存储,处理器就需要额外的操作来获取正确的数据。通过字节对齐,可以确保数据按照处理器要求的对齐方式存储,从而避免额外的操作,提高内存访问效率。(CSDN@大河qu)

少数架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐,如某平台可能从硬件层级上就有此规定,否则会抛出硬件异常,如Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常。 但更常见的情况是,如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。对于现代常见的CPU架构,通常会从4字节或8字节的整数倍地址上读取数据。还有些其他原因,这里不再描述。

在定义通信数据结构时,常常使用强制的1字节对齐,并把这些结构视作是应用层协议的一部分,若要改变这些结构定义,可能要先修改已发布的通信协议中的部分条款。后文会继续讨论,在一字节对齐的情况下,兼顾通信各端的数据读写效率,这里暂不讨论,可以肯定的是,使用pack(1)来定义交互数据结构,此时的存储结构和数据传输流肯定是空间效率最高的!

对代码移植的影响
在整理《存储和传输/寻找大端字节序/有哪款MCU或MPU是真支持大端》的过程中,阅读到 《ARM Cortex-A Series Programmer’s Guide for ARMv7-A 》 Ver4.0 Chapter 14 章节时,其中 14.2 讲述的就是ARM内核中的字节对齐,基本内容如下,做个参考,

在ARM内核上,访问对齐是很重要的。在旧版ARM处理器上,对未对齐地址的访问是可能的,但与使用ARMv7架构的访问行为不同。在ARM7和ARM9处理器上,执行一个未对齐的LDR(汇编指令,Load Register,用于将数据从内存加载到寄存器中)在内存系统中与对齐访问相同,但返回的数据被旋转,以使请求地址处的数据放置在加载寄存器的最低有效字节中。一些旧版编译器和操作系统能够利用这种行为进行巧妙的优化。当将代码从ARMv4或ARMv5移植到ARMv7架构时,这可能会导致可移植性问题。
对于Cortex-A系列处理器,支持未对齐访问,尽管您必须通过在CP15:SCTL寄存器中设置U位来启用此功能,指示允许未对齐访问。这意味着读取或写入字或半字的指令可以访问未对齐到字或半字边界的地址。但是,加载和存储多个指令(LDM和STM)以及加载和存储双字(LDRD或STRD)必须至少对齐到一个字边界。浮点值的加载和存储必须始终对齐。ABI(Application Binary Interface,是一种定义了应用程序与操作系统或其他软件组件之间交互的规范,规定了函数调用约定、数据类型的表示方式、内存布局等细节,确保不同模块之间的兼容性和可移植性)可能会施加比ARM架构规定的更严格的额外对齐约束。
一个“对齐效果可能对性能产生显著影响”的例子是memcpy()函数的使用。在字对齐地址之间复制少量字节将被编译为LDM或STM指令。将内存块按字边界对齐复制通常会使用优化的库函数,该函数也会使用LDM或STM。若,复制内存块的起始或结束点不在字边界上可能会导致调用通用的memcpy()函数,而这可能会明显减慢速度。尽管,如果源和目的地同样未对齐,那么只有起始和结束片段是非最佳的。每当执行显式类型转换时,该转换始终会带有对齐的影响。

结构的存储分配

在正式讨论上述问题前,我们先来看 《C和指针》10.3 <结构的存储分配> 中的描述。
编译器按照结构体成员列表的顺序一个接一个给它们分配内存,当将要存储的成员需要满足边界对齐要求时,成员之间才可能出现用以填充的额外的内存空间。为了说明上述观点,考虑下边这个结构:

struct TAlign {  //test in  windows x86
	char a;
	int  b;
	char c;
};

如果某个机器的int长度为4字节,并且结构对象的起始存储位置(地址)必须能被4整除(如Windows X86),那么这一结构在内存中的存储将如下所示:
在这里插入图片描述
接下来的这句描述我认为是比较关键的,
系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的类型所要求的位置。因此,成员a(最左侧的那个方框)必须存储于一个能被4整除的地址。结构的下一个成员是int整型值,所以它必须跳过3个字节(用灰色显示)到达合适的边界才能存储。在整型之后是最后一个字符。
呢?后边这段让我有些怀疑,
如果声明了(结构类型)相同类型的第2个变量,它的起始存储位置也必须满足4这个边界,所以第一个结构的后面还要再跳过3个字节才能存储第2个结构。因此,每个结构将占据12个字节的内存空间但是实际上只使用其中的6个字节,这个利用率可不是很出色。
上面这段描述会让人误解:最后被跳过的3个字节只有在某些情况下才会出现。较真的话,如果 TAlign aObject 对象后,紧跟着被定义的是一个单独的char变量,那不成 aObject 的长度就成了9,而不再是12啦?这显然与实时不符合。所以书中的上述描述欠妥。

结构体的自然对齐

结构体自然对齐是指编译器根据特定的对齐规则,将结构体中的成员按照一定的字节边界进行对齐。这个过程是编译器自动完成的,编译器会根据结构体中成员的类型和大小,自动选择合适的对齐方式,以确保结构体整体对齐的合理性,提高内存访问的效率和性能。与结构体的自然对齐相对应的是结构体的强制对齐(forced alignment)。强制对齐是一种手动设置结构体成员对齐方式的方法,而不是依赖编译器的默认对齐方式,可以通过一些特定的编译指令或预处理器指令来实现。开发人员可手动指定每个成员的对齐方式,从而更精细地控制结构体在内存中的布局。

对齐规则延伸说明

前文《C和指针》中的那段,让我们了解到一个叫做 “边界要求最严格的类型” 的概念,这里延伸出两条规则:
0、临时把结构体字段的对齐要求,称为字段或成员的对齐参数。
1、结构体总长度必须为所有对齐参数的整数倍,即符合最严格的边界要求。
2、并不是每个成员都要符合最严格的边界要求,这点必须要明确。具体到每个成员时,只要能按其类型大小和指定对齐参数n中较小的一个进行对齐就可以。就Windows系统来说,一些类型的对齐参数,如,long long、double 是8字节,long、int 是4字节,short 是2字节,char是1字节,89C51上int是2字节,在具体的实践中,可以通过 sizeof等手段得到这些参数值,没有必要去记忆。
3、如果结构体内最宽字段的长度不是是1或2,则结构体的总长度可能是2或3或6等,而不一定是4的倍数。

在有的地方,可能会出现自然边界的概念。当讨论结构体的自然边界时,通常是指结构体整体的对齐方式,即结构体的起始地址应符合结构体中最大基本数据类型的大小。这样做有助于确保结构体在内存中的布局是按照一定规则对齐的,提高内存访问的效率和性能。虽然自然边界是针对结构体整体的,但结构体中的每个成员也会受到对齐规则的影响。每个成员在结构体中的偏移量和大小也会根据对齐要求进行调整,以确保整个结构体内存布局的对齐性。假设我们有一个结构体包含以下成员:

struct ExampleStruct {
    char  dta;   //1
    short dtb;   //2
    int   dtc;   //4
};  //定义objTabc

在这个例子中,char类型占用1个字节,short类型占用2个字节,而int类型占用4个字节。按照我们之前的定义,
1、objTabc 对象的地址要按照最严格的对齐参数4字节对齐
2、dta 受到objTabc 存放地址要求的影响,即存在4倍数的地址上,不是1或2倍数的地址上
3、dtb 类型自身的对齐参数是2,并不会因为结构体边界要求而变成4字节,因此,dta和dtb之间只需要填充1个字节,而不是3个字节。
4、dta占1字节 + 填充占1字节 + dtb 占2个字节,正好达到了dtc的边界需求。objTabc 共占用8字节。

结合编译器讲述对齐规则

结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:
1、结构体变量的首地址能够被其最宽基本类型成员的大小所整除。编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找一个能被该基本数据类型宽度所整除的内存地址,作为结构体的首地址。
2、结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求。
3、最后一个成员满足上面两条以外,还要满足使得结构体的总大小(包括所有字段间的填充字节)为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

结构体的填充规则

本节是对结构体对象在内存中补齐规则的探究,重点问题是:为了对齐结构体中的各个字段,被填充用来补位的字节存放到哪里去了? 具体的,填充字节是补充到了高字节还是低字节、高地址还是低地址上、在大端系统和小端系统中是否一致?

结构成员重新排列

引用《C和指针》10.3 章节中的部分描述。你可以在声明中对结构体的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。这种做法可以最大限度的减少因边界对齐带来的控件损失。例如,对前文的结构重新排列,

struct TAlign2 {  //test in  windows x86
    int  b;
	char a;
	char c;
};

在这里插入图片描述
也就是说,除非有特殊考量,否则,将边界需求最严苛的成员(字节数最长的字段)作为在结构中的最靠前的字段是被推荐的。还需要说明的是,当程序需要创建成百上千个结构时,减少内存浪费的要求就比程序的可读性更为急迫。在这种情况下,在声明中增加注释可避免可读性方面的损失。

大小端系统的填充比较

因字节对齐需求而填充的字节(显示为灰色),其位置在大端系统和小端系统上会有区别吗?即,还是假设左边是低地址,数据c的存储会不会因为大小端的不同,其存储分别为c/0/0/0 和 0/0/0/c 呢 ?但我转念一想,这应该不可能,数据a的c无论在大端还是小端系统都应该在地址上才是,否则对结构体对象的a和c取地址操作时,岂不会乱套?本着事实胜于雄辩的原则,我又进行了后续的实践。

在写这节前,我卡了几天,因为我找不到一个真正的大端环境,并开始着手整理《存储和传输/寻找大端字节序/有哪款MCU或MPU是真支持大端》。好在我很快发现并验证了之前使用的STC89C51就是一个大端,尽管后来确认其算不上真正大端系统,但是这解了燃眉之需,验证写想法。
定义如下结构和代码,分别在大端和小端系统上验证其内存存储,

//#pragma pack(1/*4*/)
typedef struct tagAlign {
    char a;
#ifdef __cplusplus //temp windows
    int  b;
#else              //stc89c51 /其int为16bit /long为4Byte
    long b;
#endif
    char c;
} TAlign;
//#pragma pack()
//
void main() {	
	TAlign tAlian;
	int size = sizeof(TAlign);  //自然对齐:6 
	void *ptAlian = (void*)&tAlian;
    memset(&tAlian, 0xAA, sizeof(TAlign));
    tAlian.a = 0x5A; tAlian.b = 0x12345678; tAlian.c = 0x5C;
	...
}

在VS2017 x86 和 Keil C51 中分别编译运行调试,内存监视如下,
小端(Windows x86)
在这里插入图片描述

大端(STC89C51)
在这里插入图片描述
也清楚的看到了,有些遗憾的是 STC89C51+KeilC51 只能是默认的1字节对齐方式,其并不支持 #pragma pack 指令,无法强制按照多字节对齐。这就导致我不能通过实践进一步确认大端字节序环境下的填充字节最终落在哪里?即如上图1中 AA AA AA 在大端系统中是被填充到5A/5C字节的前还是后?哈哈,这纯粹是给自己挖坑,我看不上89C51+Keil的大端环境,就是想说服自己搭建一套字节满意的大端环境!

用联合结构来验证

文章写道上一节,我又暂停了一周多,期间我去寻找‘真正’的大端环境了,继续整理《存储和传输/寻找大端字节序/有哪款MCU或MPU是真支持大端》这篇文章。截止落笔此处,我找到了正在支持大端的处理器,但是我嫌麻烦,并名没有真正去实践,只是读了些芯片首次和架构手册。虽然没有切实地去到一个正真的支持多字节的大端环境下验证那些离谱的问题,但是在整理它的过程中,我逐渐地打开了心结和执拗的思路,已经通过文档和理论就解决了问题。

我在 “假的” 大端环境(STC89C51+Keil)下,定义一个如下联合结构,

union UTestFillPos {
	unsigned long format1;
	unsigned char format2;
};

在这里插入图片描述
如上,在大端字节序环境下,format2==0x12,即union对象的最高有效字节,即最低地址上的字节值

在小端 Windows10 VS2015 x86 开发环境下,同样运行上述测试过程,

int main() {
    UTestFillPos uFillPos;
    void *pAddr1 = (void*)&uFillPos.format1;
    void *pAddr2 = (void*)&uFillPos.format2;   //==	pAddr1
    uFillPos.format1 = 0x12345678;
    printf("union format2:0x%x", uFillPos.format2);
    ...

在这里插入图片描述
如上,在小端字节序环境下,format2==0x78,即union对象的最低有效字节,即最低地址上的字节值

于是可以得出一个结论,(结论是对的,哈哈,推理可能不严谨)
综合上述在大端和小端环境下的测试,无论是大端还是小端,被额外填充的字节,都是放置在真实数据所在地址之后,从低地址到高地址填充。其实细看之下,上述也是验证一个系统是大端字节序还是小端字节序的方法之一。话说回来,结构体是要支持对其中的数据字段取地址操作的,而所谓的具体字段的地址,一定是这个字段占用内存的最低地址。如此,我们只要明晰一条原则即可,用以补齐的填充字节,存在真实字节所在地址的后续高地址上。

结构成员的定义和存储顺序

在《语言基础/分析和实践 C&C++ 位域结构数据类型》中,我们有试验结果如下,
在这里插入图片描述
无论在小端还是大端字节序系统中,字段的定义和字段整体(不关心字段内字节序)的存储区位置之间的相对顺序是一致的,即它们都是将第一个字段存part1(最低地址区间), 0x0203 存在 MemArea 的part2(地址0x0083F921-0x0083F922 / 或 I:0x19- I:0x20), 最后字段 segC==0x04050607存在part3(最高地址空间)。将上述结论延伸下:对于一个结构体对象,假设其在小端系统和大端系统上的内存地址是一致的,那么,无论是大小端系统的哪个,该结构对象中,各个结构体字段的地址也应该是一致的,不一样的只是多字节字段内部的字节序。

字节序和字段序

上一节我们讲述了在大小端系统中,字段序是一致的。多年前第一次读到前述小节 [结构的存储分配] 中的那个结构在内存中的存储示意图时,其中一个疑问是,对于4字节的数据b,其在大小端系统中的字节排布是怎样的?
在这里插入图片描述
在《网络通信/大小端字节序的概念、决定因素、给编程带来的困扰》等文章中,可以找到这个问题的更加详细的回答,这里不再细述,直接写结论。如上图,左边是内存低地址,B0代表最高有效字节。则,在小端系统中,数据b的字节顺序是B3/B2/B1/B0。在大端系统中正好相反,数据b的字节顺序是B0/B1/B2/B3,最高有效字节存在低内存地址上。

机器字长/OS位数不影响填充

机器字长是指计算机中用于处理整数和地址的基本数据单元的位数。它决定了计算机一次能够处理的最大数据量和内存地址空间的范围,其大小会影响计算机的性能、内存寻址能力以及对数据的处理能力。通常情况下,较大的机器字长可以提高计算机的处理速度和数据处理能力,但也会增加硬件成本和能耗。机器字长通常以位(bit)为单位来表示,常见的机器字长包括8位、16位、32位和64位。例如,一个32位机器字长意味着计算机可以一次处理32位的数据或地址,64位机器字长则表示计算机可以一次处理64位的数据或地址。

机器字长通常更多地与计算机的硬件相关,它决定了计算机处理数据和地址的基本单位的位数,直接影响计算机的性能和功能。机器字长的大小取决于计算机的处理器架构和设计,例如x86架构通常有32位和64位两种机器字长。而操作系统位数则更多地与软件层面相关。操作系统位数通常指的是操作系统能够管理的内存地址空间大小,影响操作系统对内存的管理和程序的运行。操作系统位数可以决定一个程序能否在该操作系统上正常运行,以及能否充分利用计算机的硬件资源。

我测测试机器使用的是,酷睿 i5 10代 CPU(第十代英特尔酷睿i5处理器)采用的是英特尔的Ice Lake架构,64位指令集,其机器字长为64位。那么,只配置VS环境的平台类型为x86和x64,做一个简单的测试。

struct TAlign1 {
    char   a;   //1B
    double b;   //8B 
};      
struct TAlign2 {
    char   a;   //1B
    short  b;   //2B 
};
struct TAlign3 {
    char   a;   //1B
    void*  b;   //4B/8B 
};
//
int main() {
#ifdef _WIN64
    printf("Win x64: sizeof(void*/tAlign1/tAlign2/tAlign3) == %zd,%zd,%zd,%zd \n",
        sizeof(void*), sizeof(TAlign1), sizeof(TAlign2), sizeof(TAlign3));
#else
    printf("Win x86: sizeof(void*/tAlign1/tAlign2/tAlign3) == %zd,%zd,%zd,%zd \n",
        sizeof(void*), sizeof(TAlign1), sizeof(TAlign2), sizeof(TAlign3));
#endif // 
    system("pause");
}

在这里插入图片描述
如上,在x86平台和x64平台下,只有指针类型的长度是不一致的,我暂时看不到,这会给结构体定义和字节对齐带来什么问题。当然,如果结构体中包含指针类型,那情况就不一样了。在不含指针类型的情况下,尽管结构体的定义在32位和64位系统中可能是相同的,但在具体的编译和对齐过程中可能会存在一些微妙的差异。开发人员在编写跨平台代码时,需要考虑到这些潜在的差异,以确保代码在不同系统上的正确性和性能。

结构体套结构体的情况

如果结构体A作为结构体B的其中一个字段类型,假设sizeof(A)==48,那么B结构体要以48字节对齐参数?这显然不太可能?我的猜测是,B应该会继承A的最严苛字节宽度,如,A中最长类型是8,B包含了A,则B的自然边界也至少是8字节,相当于是结构体A在结构体B内展开?写一个简单的例子,

typedef struct tagTA {
    unsigned char  a;
    double         b;
} TA;  //sizeof == 6
//TA作为TB的字段
typedef struct tagTB {
    unsigned int d;
    TA  a;
} TB;

在这里插入图片描述
如上,结构体B的长度是24 == 16+4+4,以sizeof(double)==8为结构体的对齐边界,并在20字节上填充4字节,满足为8的整数倍。

结构体强制对齐

大多数编译器提供内存对齐的预处理指令供用户使用,使得用户可以根据处理器的情况选择不同的字节对齐方式。例如C/C++编译器通常会提供的#pragma pack(n) n=1,2,4等,让编译器在生成目标文件时,使内存数据按照指定的方式排布。编译器为了使字节对齐可能会对消息结构体进行填充,不同编译平台可能有不同的填充形式,进而造成传输数据长度不一致和内容错乱,大大增加处理器间数据通信的风险。
因此,在设计不同CPU下的通信协议时,或编写硬件驱动程序使用寄存器结构时,为了保证数据长度不会因不同编译平台或处理器而导致消息结构体长度发生变化,通常使用一字节对齐方式对消息结构进行紧缩。同时,为保证处理器之间的消息数据结构的内存访问效率,采用字节填充的方式由程序员自己对消息中成员进行四字节对齐。
对于本地使用的数据结构,为提高内存访问效率,采用四字节对齐方式;同时为了减少内存的开销,合理安排结构体成员的位置,减少四字节对齐导致的成员之间的空隙,降低内存开销。这块不理解的,可以倒回去阅读 <结构体成员重新排列> 那一小节。

一字节对齐下的访问效率

特别要注意的是,在合理的字段排序和手动填充下,使用1字节对齐,并不会影响对结构数据的访问效率。如下对同一个聚合需求的不同结构定义形式,
在这里插入图片描述
时间宽裕的话,你可以尝试对 TAssertA 和 TAssertB的结构体对象进行赋值操作,执行个几千万次,耗时差距是很明显的。字节对齐的本质目的是为了提升CPU访问内存的效率,但是一字节对齐并不一定会带来访问效率的损失,不过这必须要求程序猿在定义一字节对齐的结构时格外的注意。

更直白些说,强制一字节对齐,直接提升了结构对象的存储效率,但只要合理按照字段,可以不影响访问效率,达到访问效率与自然对齐情况下一致。一个结构数据,你必须要先存在内存里,才有访问效率一说,CPU在访问内存时,也并不是(至少并不总是能)一次性读取结构对象的所有数据,更可能的是,按部就班,按字段类型读取一个数据。也就是说,结构体的每个字段的存储地址都被期望是自然对齐的,否则就可能会增加CPU访问内存的次数,这也与自然对齐的理论知识相互照应。

如上述TMyAssertB的u32Context字段被强硬的存在了非自然4字节对齐的地址上,那么CPU就极有可能需要两次操作来读写该数据。同理,如果TMyAssertC中没有reserve字段,在一字节对齐的情况下,TMyAssertC的u32Context字段也是要两次操作。但是reserve字段的存在,使得情况发生了变化,使得TMyAssertC与TAssertA访问效率一致,不同之处在于TAssertA占用了更少的空间。总之如前述章节中的讲述,按照TAssertA的定义(最宽的字段靠前排列),具有最高的访问效率和内存最小空间占用。

一字节对齐下的结构对象地址

理论上来说,似乎如下,
如果一个结构体使用的是一字节对齐,那么结构体对象的地址就不需要被4整除。在一字节对齐的情况下,结构体中的每个成员变量都会按照其自身的大小堆叠到内存区域,每个成员变量的地址都是连续的,没有填充。
但实际上并非如此,
在这里插入图片描述
如上,一字节对齐的 TData 结构的两个对象,其地址都是可以被4整除的,我没细究这里的原因,只是简单猜测,
这里并不排除,编译器会做出一些优化,使得结构体的起始地址在任何情况下都要满足对齐要求。另外的一种可能是,#pragma pack 虽然可以影响结构体内字段的对齐参数,但是不会影响整个结构体的对齐参数,即,结构体本身的对齐方式可能会受到编译器或平台的默认设置影响。

不是所有环境都支持 pragma pack

在 STC89C51+Keil4 开发环境下,定义如下结构,编译告警,如下,

#pragma pack(1/*4*/)
typedef struct tagAlign {
    char a;
#ifdef __cplusplus //temp windows
    int  b;
#else              //stc89c51 /其int为16bit /long为4Byte
    long b;
#endif
    char c;
} TAlign;
#pragma pack()

在这里插入图片描述
这里我没有找到 STC89C51+Keil4 不支持 pragma pack 的理由,这可能是编译器的问题,甚至是我IDE配置的问题,我没再细究,有清楚的可以@我。

pragma pack 对于结构的别名无效

在typedef定义一个结构体的别名时,使用字节对齐设置的效果是什么?

typedef struct my_struct_1 {
    char a;     //1+补3
    int  b;     //+4
} T1;           //总 8/4==2

//试图对二次typedef的T101使用1字节对齐
#pragma pack(1)
typedef  T1 /*别名定义*/ T101;
#pragma pack()
//
int main() {
    T1 t1;
    printf("size of struct t1 = %u\n", sizeof(t1));      //print 8
    T101 t101;
    printf("size of struct t101 = %u\n", sizeof(t101));  //print 8 not 5

上述测试说明,当一个结构声明完成后,企图在typedef定义的别名再使用#pragma pack改变其内存对齐方式,是不能实现的!

pragma pack 可能不改变结构长度

如果 pack(n) 中的n大于结构体中边界要求最严苛的数据类型的长度值,则其形同虚设。如果结构体本身就是自然对齐或已经由程序员填充对齐过,则其也不起作用。
前边没提的,结构体的自然边界对齐参数,有pack的n和最宽字段宽度综合决定,两者取小。

//sizeof == 2
typedef struct tagItemID {
    unsigned char a;
    unsigned char b;
} TItemID;
//sizeof == 3
typedef struct tagItemID2 {
    unsigned char a;
    unsigned char b;
    unsigned char c;
} TItemID2;
//sizeof == 6
typedef struct tagItemID3 {
    unsigned char  a;
    unsigned short b;
    unsigned short c;
} TItemID3;

即使附加#pragma pack(4),也不会改变对齐和填充,上述3个结构sizeof保持不变。

pragma pack 高级用法

#pragma pack( [show] | [push | pop] [, identifier], n )

暂时没怎么用到,不再整理。

位域结构的对齐和填充

关于位域的具体讲述,参见《语言基础/分析和实践 C&C++ 位域结构数据类型》。本质上,我们常说的位域/位段结构,是包含位段的结构体。
在这里插入图片描述
如上,TBitX结构压根不能通过编译。TBitY当然是没有编译问题的,它将我们的9bit数据分散到了两个字段中,iba浪费3bit、ibb浪费4bit,我们看看被浪费的位在哪里?

    TBitY tBitY;
    memset(&tBitY, 0, sizeof(TBitY));
    tBitY.iba = 0b11111;
    tBitY.ibb = 0b1111;
    tBitY.ibc = 0xFF;
    void *pvDtBityAddr = &tBitY;

在这里插入图片描述
如上,是在小端x64上进行的测试,内存中的 1f 在低地址,如果 tBitY整体看做一个数据,则1f所在位置算作是最低有效字节,上述与16进制字节值对应的二进制值,右侧是高位,左侧是低位。因此,iba和ibb字段中浪费的位,都是字段的低位。我们可以这么想,位域的存储类型就是结构体字段,且位段宽度决不能超过该字段的类型位宽。在上述前提下,任你如何折腾地从结构字段上分配位段宽度,都不会对结构体本身的对齐和填充造成任何影响,唯一可能发生的是,部分存储字段的低bit位被你浪费掉了。最后,贴一个CAN标准帧仲裁段位域定义,结束此篇文章吧,

typedef struct {
    MR_U32 bitDevID:5;     //设别标识
    MR_U32 bitDevType:4;   //设别类型
    MR_U32 bitPriority:2;  //优先级
    MR_U32 bitReserve:21;  //高位不使用 /参见CAN驱动代码
} TCanIDStd;

相关文章

近期整理了些与大小端字节序、网络字节序、结构、位域结构、CAN通信等相关的文章,会陆续添加链接。
《语言基础/分析和实践 C&C++ 位域结构数据类型》
《存储和传输/大小端字节序概念、决定因素、给编程带来的困扰》
《网络通信/协议栈内网络字节序与主机字节序的转换实现》
《网络编程/在哪些场景中不必要进行网络字节序转换?》
《存储和传输/寻找大端字节序/有哪款MCU或MPU是真支持大端?》
《CAN总线/CAN应用层协议设计,理解并实践仲裁段位域定义》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值