程序人生 | C语言字节对齐问题详解 - 对齐/字节序/位序/网络序等(上)

字节序解析(附C++代码示例) 字节序解析(附C++转换代码示例) 阅读详情

本文首发于 2014-07-21 15:32:28

1. 引言

考虑下面的结构体定义:

typedef struct{
    char  c1;
    short s; 
    char  c2; 
    int   i;
}T_FOO;

假设这个结构体的成员在内存中是紧凑排列的,且c1的起始地址是0,则s的地址就是1,c2的地址是3,i的地址是4。

现在,我们编写一个简单的程序:

int main(void){  
    T_FOO a; 
    printf("c1 -> %d, s -> %d, c2 -> %d, i -> %d\n", 
          (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
          (unsigned int)(void*)&a.s  - (unsigned int)(void*)&a, 
          (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a, 
          (unsigned int)(void*)&a.i  - (unsigned int)(void*)&a); 
    return 0;
}

运行后输出:

c1 -> 0, s -> 2, c2 -> 4, i -> 8

为什么会这样?这就是字节对齐导致的问题。

本文在参考诸多资料的基础上,详细介绍常见的字节对齐问题。因成文较早,资料来源大多已不可考,敬请谅解。

2. 什么是字节对齐

现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量,但实际上在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是一个接一个地顺序存放,这就是对齐。

3. 对齐的原因和作用

  1. 不同硬件平台对存储空间的处理上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。例如 Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常,因此在这种架构下编程必须保证字节对齐。
  2. 如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的 Intel 处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。因此,通过合理的内存对齐可以提高访问效率。 为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。
  3. 合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度,因此,需要考虑处理器类型。同时,还应考虑编译器的类型,在VC/C++和GNU GCC中都是默认是4字节对齐。

4. 对齐的分类和准则

本小节主要基于 Intel X86 架构介绍结构体对齐和栈内存对齐,位域本质上为结构体类型。

对于 Intel X86 平台,每次分配内存应该是从4的整数倍地址开始分配,无论是对结构体变量还是简单类型的变量。

4.1. 结构体对齐

在C语言中,结构体是种复合数据类型,其构成元素既可以是基本数据类型(int、long、float等)的变量,也可以是一些复合数据类型(数组、结构体、联合等)的数据单元。编译器为结构体的每个成员按照其自然边界(alignment)分配空间。各成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

字节对齐的问题主要就是针对结构体。

4.1.1. 简单示例

先看个简单的例子(32位,X86处理器,GCC编译器):

【例1】假设结构体定义如下:

struct A{
    int    a;
    char   b;
    short  c;
};

struct B{
    char   b;
    int    a;
    short  c;
};

已知32位机器上各数据类型的长度为:char为1字节、short为2字节、int为4字节、long为4字节、float为4字节、double为8字节。那么上面两个结构体大小如何呢?

结果是:sizeof(strcut A)值为8;sizeof(struct B)的值却是12。

结构体A和B中字段一样,包含一个4字节的int数据,一个1字节char数据和一个2字节short数据,只是顺序不同。按理说A和B大小应该都是7字节,之所以出现上述结果,就是因为编译器要对数据成员在空间上进行对齐。

4.1.2. 对齐准则

先来看四个重要的基本概念:

  1. 数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。
  2. 结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。
  3. 指定对齐值:#pragma pack (value) 指定对齐值 value。
  4. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。

基于上面这些原则,就可以方便地讨论具体数据结构的成员和其自身的对齐方式。

其中,有效对齐值 N 是最终用来决定数据存放地址方式的值。有效对齐值 N 表示“对齐在N上”,即该数据的存放起始地址 % N = 0。而数据结构中的数据变量都是按定义的先后顺序存放。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐存放,结构体本身也要根据自身的有效对齐值圆整(即结构体成员变量占用总长度为结构体有效对齐值的整数倍)。

以此分析3.1.1节中的结构体B:

假设B从地址空间0x0000开始存放,且指定对齐值默认为4(4字节对齐)。成员变量b的自身对齐值是1,比默认指定对齐值4小,所以其有效对齐值为1,其存放地址0x0000符合0x0000%1=0。

成员变量a自身对齐值为4,所以有效对齐值也为4,只能存放在起始地址为0x0004~0x0007四个连续的字节空间中,符合0x0004%4=0且紧靠第一个变量。

变量c自身对齐值为2,所以有效对齐值也是2,可存放在0x0008~0x0009两个字节空间中,符合0x0008%2=0。

所以从0x0000~0x0009存放的都是B内容。

再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b),也就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求,0x0000~0x0009=10字节,(10+2)%4=0。

所以0x0000A~0x000B也为结构体B所占用。故B从0x0000到0x000B,共有12个字节,sizeof(struct B)=12。

之所以编译器在后面补充2个字节,是为了实现结构数组的存取效率。试想如果定义一个结构B的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?

按照数组的定义,数组中所有元素都紧挨着。如果我们不把结构体大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐。因此要把结构体补充成有效对齐大小的整数倍。

其实对于char/short/int/float/double等已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知,所以他们的自身对齐值也就已知。

上面的概念非常便于理解,不过个人还是更喜欢下面的对齐准则。

结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:

  1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
  2. 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
  3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。

对于以上规则的说明如下:

  • 第一条:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
  • 第二条:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
  • 第三条:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

【例2】假设4字节对齐,以下程序的输出结果是多少?

/* OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移 */
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
typedef struct{
    char  a;
    short b;
    char  c;
    int   d;
    char  e[3];
}T_Test;
 
int main(void){  
    printf("Size = %d\n  a-%d, b-%d, c-%d, d-%d\n  e[0]-%d, e[1]-%d, e[2]-%d\n",
           sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
           OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
           OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
    return 0;
}

执行后输出如下:

Size = 16
a-0, b-2, c-4, d-8
e[0]-12, e[1]-13, e[2]-14

下面来具体分析:

首先char a占用1个字节,没问题。

short b本身占用2个字节,根据上面准则2,需要在b和a之间填充1个字节。

char c占用1个字节,没问题。

int d本身占用4个字节,根据准则2,需要在d和c之间填充3个字节。

char e[3];本身占用3个字节,根据原则3,需要在其后补充1个字节。

因此,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字节。

4.1.3. 对齐的隐患

4.1.3.1. 数据类型转换

代码中关于对齐的隐患,很多是隐式的。例如,在强制类型转换的时候:

int main(void){  
    unsigned int i = 0x12345678;
    unsigned char *p = (unsigned char *)&i;
    *p = 0x00;
    unsigned short *p1 = (unsigned short *)(p+1);
    *p1 = 0x0000;
    return 0;
}

最后两句代码,从奇数边界去访问 unsigned short 型变量,显然不符合对齐的规定。在X86上,类似的操作只会影响效率;但在MIPS或者SPARC上可能导致error,因为它们要求必须字节对齐。

又如对于3.1.1节的结构体 struct B,定义如下函数:

void Func(struct B *p){
     //Code
}

在函数体内如果直接访问 p->a,则很可能会异常。因为MIPS认为a是int,其地址应该是4的倍数,但p->a的地址很可能不是4的倍数。

如果p的地址不在对齐边界上就可能出问题,比如p来自一个跨CPU的数据包(多种数据类型的数据被按顺序放置在一个数据包中传输),或p是经过指针移位算出来的。因此要特别注意跨CPU数据的接口函数对接口输入数据的处理,以及指针移位再强制转换为结构指针进行访问时的安全性。

解决方式如下:

  1. 定义一个此结构的局部变量,用memmove方式将数据拷贝进来。
void Func(struct B *p){
    struct B tData;
    memmove(&tData, p, sizeof(struct B));
     //此后可安全访问tData.a,因为编译器已将tData分配在正确的起始地址上
}

注意:如果能确定p的起始地址没问题,则不需要这么处理;如果不能确定(比如跨CPU输入数据、或指针移位运算出来的数据),则需要这样处理。
2. 用#pragma pack (1)将 STRUCT_T 定义为1字节对齐方式。

4.1.3.2. 处理器间数据通信

处理器间通过消息(对于C/C++而言就是结构体)进行通信时,需要注意字节对齐以及字节序的问题。

大多数编译器提供一些内存选项供用户使用。这样用户可以根据处理器的情况选择不同的字节对齐方式。例如:C/C++编译器提供的#pragma pack(n) n=1,2,4等,让编译器在生成目标文件时,使内存数据按照指定的方式排布在1,2,4等字节整除的内存地址处。

然而在不同编译平台或处理器上,字节对齐会造成消息结构长度的变化。编译器为了使字节对齐可能会对消息结构体进行填充,不同编译平台可能填充为不同的形式,大大增加处理器间数据通信的风险。

下面以32位处理器为例,提出一种内存对齐方法以解决上述问题。

对于本地使用的数据结构,为提高内存访问效率,采用4字节对齐方式;同时为了减少内存的开销,合理安排结构体成员的位置,减少4字节对齐导致的成员之间的空隙,降低内存开销。

对于处理器之间的数据结构,需要保证消息长度不会因不同编译平台或处理器而导致消息结构体长度发生变化,使用1字节对齐方式对消息结构进行紧缩;为保证处理器之间的消息数据结构的内存访问效率,采用字节填充的方式自己对消息中成员进行4字节对齐。

数据结构的成员位置要兼顾成员之间的关系、数据访问效率和空间利用率。顺序安排原则是:4字节的放在最前面,2字节的紧接最后一个4字节成员,1字节紧接最后一个2字节成员,填充字节放在最后。

举例如下:

typedef struct tag_T_MSG{
    long  ParaA;
    long  ParaB;
    short ParaC;
    char  ParaD;
    char  Pad;   //填充字节
}T_MSG;
4.1.3.3. 排查对齐问题

如果出现对齐或者赋值问题,可查看:

  1. 编译器的字节序大小端设置;
  2. 处理器架构本身是否支持非对齐访问;

如果支持,则看是否设置对齐;

如果没有,则看访问时是否需要加某些特殊的修饰来标志其特殊访问操作。

4.1.4. 更改对齐方式

主要是更改C编译器的缺省字节对齐方式。

在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

  • 使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;
  • 使用伪指令#pragma pack():取消自定义字节对齐方式。

另外,还有如下的一种方式(GCC特有语法):

  • __attribute__((aligned (n))):让所作用的结构成员对齐在n字节自然边界上。如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐。
  • __attribute__((packed)):取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

注意:

__attribute__机制是GCC的一大特色,可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。

在编码时,可用#pragma pack动态修改对齐值。具体语法说明见附录5.3节。

自定义对齐值后要用#pragma pack()来还原,否则会对后面的结构造成影响。

【例3】分析如下结构体C:

#pragma pack(2)  //指定按2字节对齐
struct C{
    char  b;
    int   a;
    short c;
};
#pragma pack()   //取消指定对齐,恢复缺省对齐

变量b自身对齐值为1,指定对齐值为2,所以有效对齐值为1,假设C从0x0000开始,则b存放在0x0000,符合0x0000%1=0;

变量a自身对齐值为4,指定对齐值为2,所以有效对齐值为2,顺序存放在0x0002~0x0005四个连续字节中,符合0x0002%2=0。

变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006~0x0007中,符合0x0006%2=0。

所以从0x0000到0x00007共8字节存放的是C的变量。

C的自身对齐值为4,所以其有效对齐值为2。又8%2=0,C只占用0x0000~0x0007的八个字节。所以sizeof(struct C)=8。

注意:结构体对齐到的字节数并非完全取决于当前指定的pack值,例如:

#pragma pack(8)
struct D{
    char  b;
    short a;
    char  c;
};
#pragma pack()

虽然#pragma pack(8),但依然按照2字节对齐,所以 sizeof(struct D) 的值为6。所以,对齐到的字节数=min{当前指定的pack值,最大成员大小}。

另外,GNU GCC编译器中按1字节对齐可写为以下形式:

#define GNUC_PACKED __attribute__((packed))
struct C{
    char  b;
    int   a;
    short c;
}GNUC_PACKED;

此时 sizeof(struct C) 的值为7。

4.2. 栈内存对齐

在VC/C++中,栈的对齐方式不受结构体成员对齐选项的影响,总是保持对齐在4字节边界上。

【例4】分析栈内存对齐方式:

#pragma pack(push, 1)  //后面可改为1, 2, 4, 8
struct StrtE{
    char m1;
    long m2;
};
#pragma pack(pop)
 
int main(void){  
    char a;
    short b;
    int c;
    double d[2];
    struct StrtE s;
        
    printf("a    address:   %p\n", &a);
    printf("b    address:   %p\n", &b);
    printf("c    address:   %p\n", &c);
    printf("d[0] address:   %p\n", &(d[0]));
    printf("d[1] address:   %p\n", &(d[1]));
    printf("s    address:   %p\n", &s);
    printf("s.m2 address:   %p\n", &(s.m2));
    return 0;
}

结果如下:

a    address:   0xbfc4cfff
b    address:   0xbfc4cffc
c    address:   0xbfc4cff8
d[0] address:   0xbfc4cfe8
d[1] address:   0xbfc4cff0
s    address:   0xbfc4cfe3
s.m2 address:   0xbfc4cfe4

可以看出都是对齐到4字节,并且前面的char和short并没有被凑在一起(成4字节),这和结构体内的处理是不同的。

至于为什么输出的地址值是变小的,这是因为该平台下的栈是倒着“生长”的。

4.3. 位域对齐

4.3.1. 位域定义

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空间和处理简便,C语言提供了一种数据结构,称为位域或位段。

位域是一种特殊的结构成员或联合成员(即只能用在结构或联合中),用于指定该成员在内存存储时所占用的位数,从而在机器内更紧凑地表示数据。每个位域有一个域名,允许在程序中按域名操作对应的位,这样就可用一个字节的二进制位域来表示几个不同的对象。

位域定义与结构定义类似,其形式为:

struct 位域结构名
       { 位域列表 };

其中位域列表的形式为:

类型说明符位域名:位域长度

位域的使用和结构成员的使用相同,其一般形式为:

位域变量名.位域名

位域允许用各种格式输出。

位域在本质上就是一种结构类型,不过其成员是按二进位分配的。位域变量的说明与结构变量说明的方式相同,可先定义后说明、同时定义说明或直接说明。

位域的使用主要为下面两种情况:

  1. 当机器可用内存空间较少而使用位域可大量节省内存时。例如:把结构作为大数组的元素时。
  2. 当需要把一结构体或联合映射成某预定的组织结构时。例如:需要访问字节内的特定位时。

4.3.2. 对齐准则

位域成员不能单独被取sizeof值。下面主要讨论含有位域的结构体的sizeof。

C99规定 int、unsigned int 和 bool 可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型的存在。位域作为嵌入式系统中非常常见的一种编程工具,优点在于压缩程序的存储空间。

其对齐规则大致为:

  1. 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
  2. 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
  3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++和GCC采取压缩方式;
  4. 如果位域字段之间穿插着非位域字段,则不进行压缩;
  5. 整个结构体的总大小为最宽基本类型成员大小的整数倍,而位域则按照其最宽类型字节数对齐。

【例5】

struct BitField{
    char element1  : 1;
    char element2  : 4;
    char element3  : 5;
};

位域类型为char,第1个字节仅能容纳下element1和element2,所以element1和element2被压缩到第1个字节中,而element3只能从下一个字节开始。因此 sizeof(BitField) 的结果为2。

【例6】

struct BitField1{
    char element1   : 1;
    short element2  : 5;
    char element3   : 7;
};

由于相邻位域类型不同,在VC6中其sizeof为6,在Dev-C++中为2。

【例7】

struct BitField2{
    char element1  : 3;
    char element2  ;
    char element3  : 5;
};

非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。

【例8】

struct StructBitField{
    int element1   : 1;
    int element2   : 5;
    int element3   : 29;
    int element4   : 6;
    char element5  :2;
    char stelement;  //在含位域的结构或联合中也可同时说明普通成员
};

位域中最宽类型int的字节数为4,因此结构体按4字节对齐,在VC6中其sizeof为16。

4.3.3. 注意事项

关于位域操作有几点需要注意:

1)位域的地址不能访问,因此不允许将&运算符用于位域。不能使用指向位域的指针也不能使用位域的数组(数组是种特殊指针)。例如,scanf函数无法直接向位域中存储数据:

int main(void){  
    struct BitField1 tBit;
    scanf("%d", &tBit.element2); //error: cannot take address of bit-field 'element2'
    return 0;
}

可用scanf函数将输入读入到一个普通的整型变量中,然后再赋值给tBit.element2。

2)位域不能作为函数返回的结果。
3)位域以定义的类型为单位,且位域的长度不能够超过所定义类型的长度。例如:定义 int a:33 是不允许的。
4)位域可以不指定位域名,但不能访问无名的位域。

位域可以无位域名,只用作填充或调整位置,占位大小取决于该类型。例如,char :0 表示整个位域向后推一个字节,即该无名位域后的下一个位域从下一个字节开始存放,同理 short :0 和 int :0 分别表示整个位域向后推两个和四个字节。

当空位域的长度为具体数值N时(如 int :2),该变量仅用来占位N位。

【例9】

struct BitField3{
    char element1  : 3;
    char  :6;
    char element3  : 5;
};

结构体大小为3。因为element1占3位,后面要保留6位而char为8位,所以保留的6位只能放到第2个字节。同样element3只能放到第3字节。

struct BitField4{
    char element1  : 3;
    char  :0;
    char element3  : 5;
};

长度为0的位域告诉编译器将下一个位域放在一个存储单元的起始位置。如上,编译器会给成员element1分配3位,接着跳过余下的4位到下一个存储单元,然后给成员element3分配5位。所以,上面的结构体大小为2 。

5)位域的表示范围:

  • 位域的赋值不能超过其可以表示的范围。
  • 位域的类型决定该编码能表示的值的结果。

对于第二点,若位域为unsigned类型,则直接转化为正数;若非unsigned类型,则先判断最高位是否为1,若为1,则表示补码,则对其除符号位外的所有位取反再加一得到最后的结果数据(原码)。例如:

unsigned int p:3 = 111;   //p表示7
int p:3 = 111;            //p 表示-1,对除符号位之外的所有位取反再加一

6)带位域的结构在内存中各个位域的存储方式取决于编译器,既可从左到右也可从右到左存储。

【例10】在VC6下执行下面的代码:

int main(void){  
    union{
        int i;
        struct{
            char a : 1;
            char b : 1;
            char c : 2;
        }bits;
    }num;
 
    printf("Input an integer for i(0~15): ");
    scanf("%d", &num.i);
    printf("i = %d, cba = %d %d %d\n", num.i, num.bits.c, num.bits.b, num.bits.a); 
    return 0;

输入i值为11,则输出为i = 11, cba = -2 -1 -1。

Intel x86 处理器按小字节序存储数据,所以bits中的位域在内存中放置顺序为ccba。当num.i置为11时,bits的最低有效位(即位域a)的值为1,a、b、c按低地址到高地址分别存储为10、1、1(二进制)。

但为什么最后的打印结果是a=-1而不是1?

因为位域a定义的类型signed char是有符号数,所以尽管a只有1位,仍要进行符号扩展。1做为补码存在,对应原码-1。

如果将a、b、c的类型定义为unsigned char,即可得到cba = 2 1 1。1011即为11的二进制数。

注:C语言中,不同的成员使用共同的存储区域的数据构造类型称为联合(或共用体)。联合占用空间的大小取决于类型长度最大的成员。联合在定义、说明和使用形式上与结构体相似。

7)位域的实现会因编译器的不同而不同,使用位域会影响程序可移植性。因此如无必要,最好不要使用位域。

8)尽管使用位域可以节省内存空间,但却增加了处理时间。当访问各个位域成员时,需要把位域从它所在的字中分解出来或反过来把一值压缩存到位域所在的字位中。

5. 总结

让我们回到引言部分的问题。

缺省情况下,C/C++编译器默认将结构、栈中的成员数据进行内存对齐。因此,引言程序输出就变成"c1 -> 0, s -> 2, c2 -> 4, i -> 8"。

编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。

也正是这个原因,引言例子中sizeof(T_ FOO)为12,而不是8。

总结说来,就是:在结构体中,综合考虑变量本身和指定的对齐值;在栈上,不考虑变量本身的大小,统一对齐到4字节。

补充:

本文第二篇见 程序人生 | C语言字节对齐问题详解 - 对齐/字节序/位序/网络序等(下) 。

本文转载自 https://www.cnblogs.com/clover-toeic/p/3853132.html


欢迎关注我的微信公众号【数据库内核】:分享主流开源数据库和存储引擎相关技术。

标题网址
GitHubhttps://dbkernel.github.io
知乎https://www.zhihu.com/people/…
思否(SegmentFault)https://segmentfault.com/u/db…
掘金https://juejin.im/user/5e9d3e…
开源中国(oschina)https://my.oschina.net/dbkernel
博客园(cnblogs)https://www.cnblogs.com/dbkernel
【C/C++】字节对齐 ALIGN宏 如何实现 先看一下代码中随处可见的字节对齐宏定义: #define BYTE_ALIGN(x,a) ( ((x) + ((a) - 1) ) & ( ~((a) - 1) ) ) 字节对齐实现了什么功能呢? 其实就是计算不小x的、a的最小公倍数。 经过实验,我发现,a要为2的n次方,才会实现上面说的对齐功能,如果a不止2的n次方,BYTE_ALIGN(x,a)后的值还是x。 实验验证 #include <iostream> #include <cstdio> usin 阅读详情

相关推荐

序列化、字节对齐、字节序(C++)

记录下自己对这3个问题的理解,C++编程方式下 字节对齐,存在的意义:是因为C++下,写网络协议的时候,经常写成struct或类class,类似于下边 struct _arp{          unsigned short      arp_hrd;          unsigned short      arp_pro;          unsigned char

老虎 1370

C语言字节对齐问题详解

C语言字节对齐

godleading的专栏 1621

网络字节序之大小端(字节序与比特序)

引言:最近在网上看了很多博客,想要深入了解大小端问题,主要是做毕设时,RTP包协议的结构体定义有两种方式,即大端和小端。但是一些博客并没有讲到理解大小端的本质问题,在这里按自己的理解扩充一下,可能有错,望理解!!! 1. 字节序 字节序即字节的存储顺序,如果数据都是单字节的,那怎么存储无所谓了,但是对于多字节数据,比如int,double等,就要考虑存储的顺序了。字节序是硬...

qq_40334837的博客 1819

C语言之内存对齐规则

C语言之内存对齐规则 考虑如下代码: #include <stdio.h> struct A { short a; int b; char c; }; struct B { char a; short b; int c; }; int main() { printf("sizeof(struct A) = %d\n", sizeof(struct A)); printf("sizeof(struct B) = %d\n", sizeof(struct B)); return 0

WTomster的博客 926

【C语言面试题】请使用宏定义实现字节对齐!

最近博主在后台收到一位朋友的咨询,说他最近参加了一场技术面试,有这么一道笔试题: 请使用C语言的宏定义实现一个功能,求得某个整型数M在N字节对齐的时,它的值大小。 说明: 1.M是一个非负整数; 2.N是2的整数倍,值可能是1,2,4,8,16等等。 要求: 1.不得使用除法(/); 2.不能使用函数实现,只能用宏实现; 3.自行设计测试用例,明确得出测试用例执行成功与否。 刚好,今天比较清闲,茶余饭后,顺手撸了一把代码: #include <stdio.h> #include

一个专注于嵌入式IoT领域的架构师,深耕IoT领域多年,深度掌握IoT领域的相关技术栈,包括但不限于RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、嵌入式IoT系统的架构设计等。 3万+

✨从零搭建 Ubuntu22.04 + Python3.11 + PyTorch2.5.1 GPU Docker 镜像并上传 Docker Hub

构建一个带 GPU 支持的深度学习开发镜像;使用 PyTorch 2.5.1(CUDA 12.4)环境;发布并托管 Docker 镜像至 Docker Hub;验证 GPU 环境正常运行!🎉 这将极大提升你的项目部署效率,尤其是在多人协作或跨平台部署场景中。

用大模型重构生活:猫奴程序员的AI智能日常记录 1581

程序人生 | C语言字节对齐问题详解 - 对齐/字节序/位序/网络序等(下)

字节序,顾名思义就是字节的高低位存放顺序。对于单字节,大部分处理器以相同的顺序处理比特位,因此单字节的存放和传输方式一般相同。对于多字节数据,如整型(32位机中一般占4字节),在不同的处理器的存放方式主要有两种(以内存中 0x0A0B0C0D 的存放方式为例)。在计算机中,存储介质以下面方式存储整数 0x0A0B0C0D,则称为大字节序:其中,最高有效位(MSB,Most Significant Byte)0x0A存储在最低的内存地址处。下个字节0x0B存在后面的地址处。同时,最高的16bit单元0x0A0

dbkernel 的博客 3135

Zemax非序列模式微透镜阵列仿真:从建模到优化完整指南

光线追迹是光学系统仿真的核心技术,其原理基于几何光学与物理光学的光线传播模型,通过精确计算光线在光学元件中的折射、反射与散射路径,实现对系统性能的预测与评估。在微透镜阵列等复杂多通道光学系统中,传统序列模式因光线路径受限而难以准确模拟串扰与杂散光,非序列光线追迹技术则凭借其灵活的空间传播机制成为关键手段。该技术广泛应用于激光整形、光束匀化、3D传感及光场成像等领域,能够有效支持阵列结构设计与优化。Zemax非序列模式为微透镜阵列仿真提供了完整的建模、优化与公差分析流程,是光学工程师进行复杂系统设计的核心工具

weixin_29967003的博客 339

深入理解字节对齐

C语言--字节对齐基础知识了解一、操作系统位数、CPU位数、指令集1.操作系统--32-bit(x86)和64-bit(x64)(1)位数(2)64-bit2.处理器CPU位数3.CPU指令集4.寄存器5.关系6.计算机字长(机器字长)7.总结理解字节对齐二、字节对齐1.什么是字节对齐?2.为什么要字节对齐①首先,不同硬件平台对存储空间的处理上存在不同;②根本原因在于CPU访问数据的效率问题;③其...

CharlesJhonson的博客 2万+

用AI+Playwright实现零代码UI自动化测试:以登录页面为例的实战指南

本文详细介绍了如何利用AI与Playwright结合实现零代码UI自动化测试,特别以登录页面为例进行实战演示。通过自然语言描述生成测试脚本,大幅降低技术门槛,提升测试效率,并支持跨浏览器验证。适合非技术人员快速上手专业级自动化测试。

weixin_28842269的博客 403

Linux中断子系统(二)—— 通用框架处理

1、数据结构分析 (1)中断描述符结构structirq_desc: 1)打开CONFIG_SPARSE_IRQ宏(中断编号不连续),中断描述符以radix- tree来组织,用户在初始化时进行动态分配,然后再插入radix-tree中; 2)关闭CONFIG_SPARSE_IRQ宏(中断编号连续),中断描述符以数组的 形式组织,并且已经分配好; 3)不管哪种形式,最终都可以通过linuxirq号来找到对应的中断描述符; (2)stru...

weixin_38428439的博客 4873

字节对齐_大小端_位序

字节序 字节序,顾名思义就是字节的高低位存放顺序。1 对于单字节,大部分处理器以相同的顺序处理比特位,因此单字节的存放和传输方式一般相同。 对于多字节数据,如整型(32位机中一般占4字节),在不同的处理器的存放方式主要有两种(以内存中0x0A0B0C0D的存放方式为例)。 大端,Big-Endian 在计算机中,存储介质以下面方式存储整数0x0A0B0C0D则称为大字节序: 0x0A 0xOB 0xOC 0xOD 0x0000 0x0001 0x0002 0x0003 其中,最高有效位(

lkzp12的博客 1069

从零开始使用CubeMX配置STM32使用lwip实现httpd服务器以及使用vscode编辑阅读keil代码(三)

使用stm32搭载lwip实现HTTPd服务器

qq_30623327的博客 5934

C语言字节对齐问题详解(对齐、字节序、网络序等)

阅读目录 引言 一  什么是字节对齐 二  对齐的原因和作用 三  对齐的分类和准则 3.1 结构体对齐 3.2 栈内存对齐 3.3 位域对齐 四  总结 五  附录 5.1 字节序与网络序 5.2 对齐时的填充字节 5.3 pragma pack语法说明 5.4 Intel关于内存对齐的说明 5.5 不同架构处理器的对齐要求 5.6 ARM下的对齐...

liangxiaoxi7的博客 1016

基于STM32设计自动输液监测系统(241)

该系统集成了液位传感模块、光电传感器、电机模块、按键模块和OLED显示屏等多个功能模块,以实现全面的输液过程监控和控制。液位传感模块能够实时监测输液瓶中的液位,当液位到达限定值时,系统会自动控制步进电机夹紧输液管,并发出报警提醒,确保输液过程不会因液位过低而中断。

1343

桌面运维工程师面试题详解(一)

桌面运维工程师面试题详解(一)

jks212454的博客 1428

C语言字节对齐详解

现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空间和处理简便,C语言提供了一种数据结构,称为“位域”或“位段”。

Magicamazing的博客 2181

基于STM32、HAL库的MCP4013T-103E/CH 数字电位器驱动程序设计

define MCP4013_I2C_ADDR (0x5F << 1) // HAL库使用7位地址左移1位,或直接使用0xBE/0xBF。#include "i2c.h" // 包含你的I2C初始化相关的头文件,例如由CubeMX生成的 i2c.h。大于127的值会被截断。// MCP4013的读操作:主机发起读取请求,从机返回当前滑片位置的一个字节。// 使用HAL_I2C_Master_Transmit直接发送数据。// 使用HAL_I2C_Master_Receive读取一个字节。

corlin6688的博客 127

什么是字节序(端序、低端字节序、高端字节序、网络字节序)

字符串12在内存中的第一个字节到底是什么?本篇文章将详细解释字节序、比特序与端序的概念。

qq_30576521的博客 6068

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

CAN总线位序与字节序

字节序(Byte Order)是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。在计算机中是以字节为单位,每个地址对应一个字节,一个字节8bit。在C中,除了8bit的char以外,还有16bit的short,32位的int,64位long,当然具体要由编译器决定,可以通过sizeof来获取不同类型在内存中占用的字节数。在计算机系统中,当物理单位的长度大于1个字节时,就要区分字节顺序。

April的博客 1万+

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

C语言字节对齐

字节对齐(Byte Alignment)是指数据在内存中的存储地址需要按照特定规则对齐。在计算机体系结构中,内存通常按照字(word)为单位进行访问,而字的大小取决于具体的硬件平台(如32位系统中通常为4字节,64位系统中通常为8字节)。为了提高内存访问效率,编译器会根据数据类型和平台特性自动进行内存对齐。

Made In SQL 851

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

字节序以及C语言struct中的位域

大端序(BE, Big-Endian):高字节在前,低字节在后(高字节存放在低地址,低字节存放在高地址)。 小端序(LE, Little-Endian):低字节在前,高字节在后(低字节存放在低地址,高字节存放在高地址)。 大小端设备上给struct位域成员分配的位存储空间与单字节数值中的bit位的对应关系为: 1)小端:先从单字节数值中的LSB开始分配; 2)大端:先从单字节数值中的MSB开始分配; 单字节内,大端的位域定义和小端的位域定义顺序相反时,代表了相同的结构体定义。

BitSwimmer的博客 4193

C语言位域与字节序问题深度解析

位域是C语言中用于精确控制内存位级别的数据结构,特别适用于硬件寄存器和网络协议等场景。其原理是通过将字节划分为不同区域,每个区域可单独命名和访问。字节序则决定了多字节数据在内存中的存储顺序,分为大端序和小端序。理解位域和字节序对于开发跨平台应用和网络通信程序至关重要,能有效解决内存布局不一致导致的数据解析问题。本文通过实际案例展示了位域与字节序在网络协议解析中的典型应用,并提供了调试技巧和性能优化建议,帮助开发者避免常见陷阱。

weixin_42590539的博客 527

关于C/C++字节对齐、位域、大小端、柔性数组问题

一、字节对齐 1.概念 字节按照特定的规则在空间上的排列。 2.原则 ①成员变量的顺序会影响结构体的大小。 一般情况下,成员变量所占用的空间,会被其中最宽变量类型所框定,而当定义小于最宽类型的成员变量时,这个变量会和相邻的成员变量合并,拼为成员变量的整数倍形式,如果相邻处没有合适的变量,它将自身占有最宽类型成员变量存储空间大的大小。 ②结构体大小是最宽字节类型的整数倍 当用上述规则束...

789

为什么你的二进制文件读写出错了?深度剖析C语言位域字节序与对齐问题

解决C语言位域读写异常,深入解析二进制文件操作中的字节序与内存对齐问题。涵盖嵌入式、协议解析等场景,揭示跨平台数据存储兼容性关键点,提供可靠位域设计方法。掌握这些技巧,避免数据解析错误,值得收藏。

CompiGlow的博客 1040
上一篇: Linux strace命令使用详解
下一篇: Ubuntu下单网卡多IP地址的配置
dbkernel
博客等级 码龄4年 60粉丝 42原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值