1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > C语言基础 - 结构体类型字节对齐总结

C语言基础 - 结构体类型字节对齐总结

时间:2022-07-08 13:40:58

相关推荐

C语言基础 - 结构体类型字节对齐总结

一、什么是字节对齐

在计算机中,内存空间是按照字节(1B = 8 bit)划分的,每一个字节都有一个编号,这就是字节的地址。理论上可以从任意起始地址访问任意数据类型的变量,但在实际使用中,访问特定数据类型变量时需要在特定的内存起始地址进行访问,这就需要各种数据类型按照一定的规则在空间上进行排列,而不是顺序地一个接一个地存放,这就是字节对齐。

如果一个变量的内存起始地址正好是其数据类型长度的整数倍,就被称作自然对齐。比如,在32系统下,假设一个int型变量的起始地址为0x00000004,那它就是自然对齐的。

1.1 C语言基本数据类型占用的字节大小

C语言基本数据类型有:

整数型:char, short, int, long, long long。

浮点型:float, double

指针类型:任意数据类型的指针变量占用的存储空间都是相同的。

我们以64位系统(x64)为例,使用 sizeof() 可以输出各个基本数据类型占用的字节长度,代码如下:

#include <stdio.h>int main(){printf("sizeof(char)=%d\n", sizeof(char));printf("sizeof(short)=%d\n", sizeof(short));printf("sizeof(int)=%d\n", sizeof(int));printf("sizeof(long)=%d\n", sizeof(long));printf("sizeof(long long)=%d\n", sizeof(long long));printf("sizeof(float)=%d\n", sizeof(float));printf("sizeof(double)=%d\n", sizeof(double));printf("sizeof(char*)=%d, sizeof(int*)=%d, sizeof(float*)=%d\n", sizeof(char*), sizeof(int*), sizeof(float*));return 0;}

运行结果:

sizeof(char)=1

sizeof(short)=2

sizeof(int)=4

sizeof(long)=4

sizeof(long long)=8

sizeof(float)=4

sizeof(double)=8

sizeof(char*)=8, sizeof(int*)=8, sizeof(float*)=8

<说明> 在32位系统(x86)上,指针类型变量是4字节;在64位系统(x64)上,指针类型变量是8字节。这跟计算机的字长有关。32位系统中,CPU一次可以存取4字节的数据;64位系统中,CPU一次可以存取8字节的数据。

二、字节对齐的原因和作用

需要字节对齐的根本原因在于CPU访问内存数据的效率问题。

(1)不同硬件平台对内存空间的存取处理方式存在不同。某些硬件平台对特定数据类型的存取只能从特定地址开始,而不允许其在内存中随意存放。

一些硬件系统对字节对齐要求非常严格,比如 SPARC系列处理器,如果取未对齐的数据会发生错误,例如:

char ch[8];char *p=&ch[1];int i = *(int *)p;

运行时会报 segment error,因为在第3行代码中,试图从一个奇数起始地址处读取一个int型的数据,而在Intel的x86处理器上就不会出现错误,只是效率下降。

(2)如果不按照硬件平台的要求对数据存放进行对齐处理,会影响CPU访问内存的效率。比如,对于32位系统的计算机,CPU通过数据总线访问(包括读和写)内存数据,每个总线周期从偶地址开始访问32位内存数据,内存数据是以字节为单位存放的,如果一个32位数据没有存放在4字节整除的起始地址处,那么CPU就需要2个总线周期的时间对其进行访问,显然访问效率下降了。因此,通过合理的内存字节对齐可以提高CPU访存效率。为使CPU能够对内存数据进行快速访问,数据的起始地址应具有“对齐”特性。比如,4字节数据的起始地址应位于4字节边界上,8字节数据位于8字节边界上。

(3)合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机器中使用1字节或2字节对齐,反而会降低内存访问速度。除了需要考虑处理器类型,还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认以4字节对齐。

三、字节对齐的分类和准则

主要基于Intel 的 x86硬件架构介绍结构体对齐、栈对齐和位域对齐,位域对齐本质上为结构体对齐。

对于Intel x86平台,每次分配内存都是从4的整数倍的起始地址开始分配,无论是对结构体变量还是基本数据类型的变量。

3.1 结构体字节对齐

在C语言中,结构体类型是一种复合数据类型,其构成成员既可以是基本数据类型(如 char, int, float等)的变量,也可以是复合数据类型(如数组、结构体、共用体等)的变量。编译器在编译阶段,会为结构体的每个成员变量按照其自然边界分配存储空间。各成员按照他们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的起始地址相同。

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

3.1.1 简单示例

先看一个结构体对齐的简单示例(32位系统,x86处理器,GCC编译器)

#include <stdio.hstruct A{char a;short b;int c;};struct B{char a;int c;short b;};int main(){printf("sizeof(struct A)=%d\n", sizeof(struct A));printf("sizeof(struct B)=%d\n", sizeof(struct B));return 0;}

运行结果:>a.exe

sizeof(struct A)=8

sizeof(struct B)=12

分析:可以看到,结构体A 和 B的成员是一样的,只是声明顺序不同,但是最终这两个结构体占用的内存空间大小却是不同的。之所以出现上述结果,就是因为编译器在编译程序时要对结构体的成员在存储空间上进行字节对齐的缘故。

3.1.2 对齐规则

先说明一下四个重要的基本概念:

(1)基本数据类型自身对齐值:基本数据类型自身占用的存储空间大小,上面已经给出了各个基本数据类型占用的字节数大小。

(2)结构体类型自身的对齐值:是结构体成员变量中自身对齐值最大的那个。比如上面的 结构体类型 struct A,其成员变量中最大的对齐值是int类型的对齐值(4字节),那么该结构体本身的对齐值也就是4字节。

(3)指定对齐值:#pragma pack (value)时的指定对齐值value。这个我们在下面再讨论。

(4)结构体成员、结构体的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。

其中,有效对齐值 N 是最终用来决定数据存放的对齐值方式。有效对齐N表示“对齐在N上”,即存放数据的起始地址 % N == 0。

结构体中的成员变量都是按定义的先后顺序存放的。第一个成员变量的起始地址就是结构体变量本身的起始地址。结构体成员变量要对齐存放,同时结构体本身也要根据自身的有效对齐值进行对齐处理(即结构体占用存储空间的总长度为结构体有效对齐值的整数倍)。

综上要求,我们给出结构体字节对齐的规则如下:

(1)结构体各个成员变量的首地址必须是其自身对齐值的整数倍。

(2)结构体各个成员相对于结构体起始地址的偏移量(offset)是该成员数据类型大小的整数倍,如有需要编译器会在成员之间加上填充字节。

(3)结构体分配的总空间大小必须是其最宽基本数据类型成员的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

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

第1条:编译器在给结构体开辟存储空间时,首先找到结构体成员中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的地址,作为结构体的首地址。将这个最宽的基本数据类型的大小作为该结构体自身的对齐值。

第2条:为结构体的一个成员变量开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移量是否是该成员数据类型大小的整数倍,若是,则存放该成员;若不是,则在该成员和上一个成员之间填充一定数量的多余字节,已达到整数倍的要求,也就是将预开辟空间的首地址后移若干字节。

第3条:结构体实际占用空间大小是包括了填充字节的,最后一个成员除了满足前面两条之外,还必须满足第(3)条。

实例1:

struct A{char a; //1short b; //2int c; //4};

sizeof(struct A) = ? 是 1+2+4=7吗?答案是:8。分析如下:

(1)首先确定该结构体自身的对齐值是多少?因为最宽的基本数据类型为int型,占4个字节,因此结构体自身的对齐值为4。

(2)成员a自身对齐值为1,成员b自身对齐值为2,成员c自身对齐值为4。首先成员a占1个字节没有问题,如果成员b存放在成员a的下一个字节的位置的话,那它的偏移量就是1了,不满足规则2的要求,因此成员b的起始地址需要再后移一个字节,即:1(a)+1(填充字节)+2(b)。

(3)成员b之后的下一个字节的起始地址到结构体的首地址的偏移量刚好是4,而成员c的自身对齐值为4,满足条件2,因此成员c可以存放在紧随成员b之后的位置上,此时,整个结构体占用的空间大小=2+2+4=8,刚好是4的倍数,满足规则3。

综上所述,sizeof(struct A) = 8。

实例2:

struct B{char a; // 1int c; // 4short b; //2};

sizeof(struct B) = ? 可以看到,struct B 与 struct A 结构体的成员变量是一样的,只是成员的顺序有变化。那么,sizeof(struct B) 是否也是等于8呢?

不是!正确答案是:sizeof(struct B) = 12。分析如下:

(1)struct B 的自身对齐值和 struct A 是一样的,都是4,这是没问题的。

(2)成员a占用一个字节,这也是没问题的,但是成员c的对齐值是4,那么其存放的起始地址与结构体的首地址的偏移量必须是4的倍数才行,因此成员a和成员c之间须先填充3个字节,然后下一个字节才是成员c的起始地址,即:1(a) + 3(填充字节) + 4(c)。

(3)c成员之后的下一个字节与结构体首地址的偏移量为8,而成员b的对齐值为2,满足规则2,此时,结构体的空间大小:8+2(b) = 10。可以发现,虽然结构体的各个成员都已符合对齐规则,但是不满足规则3,即结构体的空间大小不是其自身对齐值的整数倍。因此,需要在成员b的尾部加上2个填充字节,即:10 + 2(填充字节) = 12,这才满足规则3。

综上所述,sizeof(struct B) = 12。

可以发现,通过调整结构体各成员的定义顺序,合理利用字节对齐的规则,可以有效地节省结构体的占用空间。

3.1.3 指定对齐方式

主要是更改编译器的默认字节对齐方式。在默认情况下,C编译器(如GCC)为每一个变量按其自然字节对齐规则分配存储空间。一般可以通过下面的方法来改变默认的字节对齐的条件:

#pragma pack(n) //编译器按照n个字节的条件对齐#pragma pack() //取消自定义字节对齐方式

<说明> #pragma 是一个预处理指令,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma pack的主要作用就是改变编译器的内存对齐方式。

当我们在程序代码中主动设置了自定义的字节对齐方式后,3.1.2节中所讲的字节对齐规则会有一些变化:

(1)结构体各个成员变量的首地址必须是 min{自身对齐值,指定对齐值} 的整数倍。

(2)结构体各个成员相对于结构体起始地址的偏移量(offset)是 min{该成员数据类型大小,指定对齐值} 的整数倍,如有需要编译器会在成员之间加上填充字节。

(3)结构体分配的总空间大小必须是 min{其最宽基本数据类型成员,指定对齐值} 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

实例3:

struct Test1{char a;int b;short c;};#pragma pack(2)struct Test2{char a;int b;short c;};#pragma pack()

sizeof(struct Test1) = 12, sizeof(struct Test2) = 8。分析如下:

这里,结构体Test1就不赘述了,主要分析结构体Test2的情况:

(1)结构体自身对齐大小为int型长度4字节,而程序中指定的对齐长度为2,根据规则1,因此结构体自身有效对齐值为2。

(2)成员a的自身对齐值为1,指定对齐值为2,取两者中较小的为有效对齐值,即为1,因此,成员a占用1个字节的存储空间。

(3)成员b的自身对齐值为2,指定对齐值为2,取两者中较小的为有效对齐值,即为2,而成员b的起始地址偏移量必须是2的整数倍,因此,需要在成员a和成员b之间添加一个填充字节。此时,占用的内存空间=1(a) + 1(填充字节) + 4(b)

(4)成员c的自身对齐值为2,指定对齐值为2,取两者中较小的为有效对齐值,即为2,而成员b之后下一个字节的地址的偏移量为6,满足规则2,因此,成员c紧跟在成员b之后,此时,占用的内存空间=6 + 2(c),即8字节长度,恰好是结构体自身有效对齐值的倍数。

综上所述,sizeof(struct Test2) = 8。

实例4:

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

sizeof(struct Test3) = ? 答案是:6。分析如下:

(1)结构体自身对齐大小为short型长度2字节,而程序中指定的对齐长度为8,因此结构体自身有效对齐值为2。根据规则1,此时结构体自身的有效对齐值为2,而不是程序中指定的对齐长度8。

(2)成员a占一个字节,而成员b的有效对齐值为 min(2, 8)=2,因此,成员a和成员b之间需要加一个填充字节,1(a) + 1(填充字节) + 2(b)。

(3)成员c占一个字节,4+1=5,不满足规则3,因此需要在成员c之后添加一个填充字节,即:4 + 1(c) + 1(填充字节)=6字节。

综上所述,sizeof(struct Test3) = 6。

实例5:

#pragma pack(1)struct Test4{char a;int b;short c;};#pragma pack()

sizeof(struct Test4) = ? 答案是:7。

结构体 Test4 是以1个字节作为指定对齐值,因此结构体本身以及结构体的所有成员都是按1字节长度作为有效对齐值进行字节对齐的。

所以,结构体占用的存储空间大小为:1+4+2=7。

另外,GNU GCC编译器中按1字节长度进行字节对齐可以写成如下的形式,改写结构体 Test4 的定义如下:

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

实例6:微软面试题解析。

#pragma pack(8)struct s1{short a;long b;};struct s2{char c;struct s1 d;int e;};#pragma pack()

问:1、sizeof(struct s2) = ? 2、s2的成员s1中的a后面空了几个字节接着才是b?

答案:1、sizeof(struct s2) = 16 2、空了2个字节。分析如下:

(1)首先分析结构体 s1的内存分配情况,结构体自身对齐值为long型的长度4,指定对齐值为8,因此结构体s1的有效对齐值为4。成员a的有效对齐值为2,成员b的有效对齐值为4,其偏移量必须是4的倍数,因此,在成员a和成员b之间添加2个填充字节,此时,占用的空间大小为:2 + 2(填充字节) + 4 = 8,满足规则3,故,结构体s1占用的空间大小=8字节。

(2)再来分析结构体s2,结构体s2的自身对齐值为其成员中最宽基本数据类型的长度,即为成员e的int型,长度为4字节,指定对齐值为8,故结构体s2的有效对齐值为4字节。s2中的成员c按1字节对齐,而成员d是一个8字节的结构体,而结构体s1的有效对齐值为4,所以成员d按4字节对齐,需要在成员c和d之间添加3个填充字节,此时,占用的空间大小为:1 + 3(填充字节) + 8 = 12字节。

(3)成员e的有效对齐值,可知是4,其偏移量为12,刚好是其对齐值的倍数,此时,占用的空间大小为:12 + 4 = 16字节,也恰好是结构体s2的有效对齐值的倍数,因此,结构体s2最终占用的空间大小为16字节,即:1 + 3(填充字节) + 8 + 4 = 16字节。

结构体s2的各个成员变量在内存中的布局如下表所示:

参考

C语言字节对齐问题详解

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。