C语言字节对齐分析

1、前言

什么是字节对齐呢?现代计算机中的内存空间都是按字节(byte)划分的,从理论上讲似乎任何类型的变量的访问都可以从任何地址开始,但是实际情况是在访问特定变量的时候经常需要在特定的内存地址进行访问,因此,就需要各种类型数据按照一定的规则在空间上排列,而不是顺序地一个接一个地排放,这就是字节对齐。

2、字节对齐的好处

各个硬件平台对存储空间的处理上有很大的不同,一些平台对某些特定类型的数据只能从某些特定的地址开始存取,比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构上编程必须保证字节对齐。其它平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失,例如某些平台每次读都是从偶地址开始,如果一个int类型(32位系统)变量存放在偶地址开始的地方,那么一个读周期就可以读出32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果进行高低字节的拼凑才能得到该32bit数据。

3、对齐的准则

有四个重要的基本概念,如下:

(1)数据类型自身的对齐值:char类型数据自身对齐值为1字节,short类型数据为2字节,int/float类型数据为4字节,double类型数据为8字节;

(2)结构体或类的自身对齐值:其成员中自身对齐值最大的哪个值;

(3)指定对齐值:#pragma pack(value)时指定对齐值value;

(4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中的较小者,即:

有效对齐值=min{自身对齐值,当前指定的pack值}。

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

其实字节对齐的细节和具体编译器实现密切相关,但是一般而言,满足下面三个准则:

(1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

(2)结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如需要时,编译器会在成员之间加上填充字节;

(3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如需要时,编译器会在最末一个成员之后加上填充字节。

对于上面的三条对齐准则说明如下:

第一条:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被这基本类型所整除的位置,并把这个地址作为结构体的首地址;

第二条:为结构体的一个成员开辟空间之间,编译器首先检查预开辟空间的地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,否则,在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求;

第三条:结构体的总大小是包括填充的字节的,结构体的最后一个成员满足上面两个对齐准则以外,还必须满足第三条准则,否则就必须在最后填充几个字节以满足最后一条准则。

4、结构体字节对齐示例

接下来给出一个结构体字节对齐的简单示例,并对其进行分析:

示例的代码如下所示:

#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>

struct s {
    char a;
    short b;
    char c;
    int d;
    char e[3];
};

int main(int argc, char *argv[])
{
    printf("sizeof(struct s):%zd\n", sizeof(struct s));
    printf("offsets->a:%zd; b:%zd; c:%zd; d:%zd; e[0]:%zd; e[1]:%zd; e[2]:%zd\n",
                    offsetof(struct s, a), offsetof(struct s, b), 
                    offsetof(struct s, c), offsetof(struct s, d),
                    offsetof(struct s, e[0]), offsetof(struct s, e[1]),
                    offsetof(struct s, e[2]));

    exit(EXIT_SUCCESS);
}

使用gcc编译器进行编译,并运行,结果输出如下:

在测试代码中使用了offsetof这个宏,该宏能获取结构体中成员的偏移量,在运行结果中,我们可以看到,使用sizeof计算结构体的总大小为16字节,如果直接计算的话,char类型占1个字节,short类型占2个字节,int类型占4个字节,应该结构体大小为11个字节,为什么最终的大小是16字节呢?是由于用到了上面介绍的字节对齐的准则,下面对其进行分析:

首先,变量a为char类型占用1个字节,变量b为short类型,占用2个字节,根据准则2,需要在成员b和a之间填充1个字节,变量c为char类型,占用1个字节,变量d为int类型,占用4个字节,根据准则2,需要在成员d和c之间填充3个字节,数组e为char类型,占用3个字节,但是根据准则3,需要在最后填充1个字节,因此,结构体的总大小应为16字节。

5、更改对齐方式

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

(1)使用伪指令#pragma pack(n):C编译器将按照n个字节进行对齐;

(2)使用伪指令#pragma pack():取消自定义的字节对齐方式;

另外,还有GCC的特有语法

(3)__attribute__((aligned(n))):让所作用的结构体成员对齐在n字节自然边界上,如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐;

(4)__attribute__((packed)):取消结构体在编译过程中的优化对齐,按照实际占用的字节数进行对齐。

下面就更改对齐方式进行一些例子讲解:

使用#pragma pack()将结构体对齐方式进行更改,代码如下:

#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>

#pragma pack(1)
struct s {
    char a;
    short b;
    char c;
    int d;
    char e[3];
};
#pragma pack()

int main(int argc, char *argv[])
{
    printf("sizeof(struct s):%zd\n", sizeof(struct s));
    printf("offsets->a:%zd; b:%zd; c:%zd; d:%zd; e[0]:%zd; e[1]:%zd; e[2]:%zd\n",
                    offsetof(struct s, a), offsetof(struct s, b), 
                    offsetof(struct s, c), offsetof(struct s, d),
                    offsetof(struct s, e[0]), offsetof(struct s, e[1]),
                    offsetof(struct s, e[2]));

    exit(EXIT_SUCCESS);
}

使用gcc对代码进行编译后运行,结果如下:

使用#pragma pack(1)将字节对齐方式设置为1字节,相当于取消了默认的字节对齐方式,因此编译器将不再对变量之间进行字节填充,所以,使用sizeof()得到就是数据类型占用的实际大小,为11个字节。

当使用#pragma pack(2)将字节对齐方式设置为2字节时,结果又如何呢?

#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>

#pragma pack(2)
struct s {
    char a;
    short b;
    char c;
    int d;
    char e[3];
};
#pragma pack()

int main(int argc, char *argv[])
{
    printf("sizeof(struct s):%zd\n", sizeof(struct s));
    printf("offsets->a:%zd; b:%zd; c:%zd; d:%zd; e[0]:%zd; e[1]:%zd; e[2]:%zd\n",
                    offsetof(struct s, a), offsetof(struct s, b), 
                    offsetof(struct s, c), offsetof(struct s, d),
                    offsetof(struct s, e[0]), offsetof(struct s, e[1]),
                    offsetof(struct s, e[2]));

    exit(EXIT_SUCCESS);
}

使用gcc编译后,然后运行,结果如下: 

可以看到,结构体的总大小变成了14字节,分析如下:

变量a自身对齐值为1,指定对齐值为2,所以有效对齐值为1,假设结构体从0地址开始存储,符合0%1=0,变量b自身对齐值为2,指定对齐值为2,所以有效对齐值为2,从地址2开始存放,符合2%2=0,变量b和a之间需要填充1个字节,变量c自身对齐值为1,指定对齐值为2,有效对齐值为2,从地址4开始存储,符合4%2=0,变量d为int类型,自身对齐值为4,指定对齐值为2,有效对齐值为2,因此变量d和c之间填充1个字节,符合6%2=0,数组e依次类推,到最后一个成员时,由于指定对齐值为2字节,需要在最后填充1个字节,因此结构体的总大小为14字节。

当使用#pragma pack(8)将结构体指定对齐值为8时会怎么样呢?代码如下:

#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>

#pragma pack(8)
struct s {
    char a;
    short b;
    char c;
    int d;
    char e[3];
};
#pragma pack()

int main(int argc, char *argv[])
{
    printf("sizeof(struct s):%zd\n", sizeof(struct s));
    printf("offsets->a:%zd; b:%zd; c:%zd; d:%zd; e[0]:%zd; e[1]:%zd; e[2]:%zd\n",
                    offsetof(struct s, a), offsetof(struct s, b), 
                    offsetof(struct s, c), offsetof(struct s, d),
                    offsetof(struct s, e[0]), offsetof(struct s, e[1]),
                    offsetof(struct s, e[2]));

    exit(EXIT_SUCCESS);
}

使用gcc编译,并运行,结果如下:

可以看到结构体的总大小为16字节,而且并不是按照8字节进行对齐,而是按照4字节进行对齐,因为下面这个对齐准则:

有效对齐值=min{自身对齐值,当前指定的pack值}

所以,上面的结构体将按照4字节进行对齐。

6、小节

本文主要简单介绍了C语言中结构体的字节对齐,对其基本的对齐准则以及一些具体例子进行了描述。

参考:

http://www.baike.com/wiki/%E5%AD%97%E8%8A%82%E5%AF%B9%E9%BD%90

https://www.jianshu.com/p/f69652c7df99

https://www.cnblogs.com/clover-toeic/p/3853132.html

猜你喜欢

转载自www.cnblogs.com/Cqlismy/p/11440057.html
今日推荐