C语言的结构体和对齐的理解

C语言的结构体和对齐的理解

本文为C语言基础的复习笔记
时间: 2019-04-23

1 引言

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

2 对齐的原因和作用

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

3 结构体对齐

对齐准则:对于Intel X86平台,每次分配内存应该是从4的整数倍地址开始分配,无论是对结构体变量还是简单类型的变量。
在C语言中,结构体是种复合数据类型,其构成元素既可以是基本数据类型(如int、char、double等)的变量,也可以是一些复合数据类型(如数组、结构体、联合等)的数据单元。编译器为结构体的每个成员按照其自然边界(alignment)分配空间。各成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
字节对齐的问题主要就是针对结构体。

3.1 示例

32位 x86 gcc 4.7

#include <stdio.h>
#include <string.h>

struct student {
    char sex;
    int age;
    double height;
    double weight;
};
struct teacher {
    char sex;
    char note;
    double height;
    double weight;
    int age;
};
struct A {
    char a;
    int b;
    short c;
};
struct B {
    int a;
    char b;
    short c;
};

int main(int argc, char const *argv[])
{
    /* code */
    struct student stu = {'F', 12, 170.5, 122};
    struct teacher tea = {'M', 'N', 180.4, 150, 30};
    struct A a = {'k', 12, 2};
    struct B b = {4, 'm', 1};

    printf("The system size of data struct!\n");
    printf("-----------------------------------\n");
    printf("Size of int    = %d\n", sizeof(int));
    printf("Size of char   = %d\n", sizeof(char));
    printf("Size of short  = %d\n", sizeof(short));
    printf("Size of double = %d\n", sizeof(double));
    printf("-----------------------------------\n");
    printf("Size of struct student: %d\n", sizeof(stu));
    printf("Size of struct teacher: %d\n", sizeof(tea));
    printf("Size of struct A: %d\n", sizeof(a));
    printf("Size of struct B: %d\n", sizeof(b));
    
    return 0;
}

测试结果:

The system size of data struct!
-----------------------------------
Size of int    = 4
Size of char   = 1
Size of short  = 2
Size of double = 8
-----------------------------------
Size of struct student: 24
Size of struct teacher: 32
Size of struct A: 12
Size of struct B: 8

注意结构体A 和 B 如果没有对齐,其应该是1+2+4 = 7。实际上由于对齐的原因导致结果是12 或者8。
为什么是12或8而不是其他的值呢?

3.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节中的结构体A和结构体B,回顾一下代码:

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

假设A从地址空间0x0000开始存放,且指定对齐值默认为4(4字节对齐)。成员变量a的自身对齐值是1,比默认指定对齐值4小,所以其有效对齐值为1,其存放地址0x0000符合:0x0000 % 1 == 0。成员变量b自身对齐值为4,所以有效对齐值也为4,只能存放在起始地址为0x0004~0x0007四个连续的字节空间中,符合:0x0004 % 4 == 0 且紧靠第一个变量。变量c自身对齐值为 2,所以有效对齐值也是2,可存放在0x0008~0x0009两个字节空间中,符合:0x0008 % 2 == 0。所以从0x0000~0x0009存放的都是A内容。
再考虑数据结构A的自身对齐值为其变量中最大对齐值(这里是int b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求,0x0000~0x0009=10字节,由(10+2) % 4 == 0。所以0x0000~0x000B也为结构体A所占用。故A从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}。


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

来一个实例测试,上代码:

#include <stdio.h>
/* OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移 */
#define OFFSET(TYPE, field) (size_t)&(((TYPE *)0)->field)
typedef struct {
    char a;
    short b;
    int c;
    char d;
    double e;
}A;

int main(int argc, char const *argv[])
{
    // struct test A = {'a', 1, 2, 's', 1.2};
	printf("Size of struct A is %d\n", sizeof(A));
    printf( "--------------------------Analysis---------------------------\n" );
    printf( " | a: 0 | + 1 | b: 2~3 | c: 4~7 | d: 8 | + 9~15 | e: 16~23 | \n" );
    printf( "-------------------------------------------------------------\n" ); 
    printf( "[a, b, c, d, e] address OFFSET is %d, %d, %d, %d, %d\n",
        OFFSET(A, a), OFFSET(A, b), OFFSET(A, c), OFFSET(A, d), OFFSET(A, e) );

    return 0;
}

测试结果:

Size of struct A is 24
--------------------------Analysis---------------------------
 | a: 0 | + 1 | b: 2~3 | c: 4~7 | d: 8 | + 9~15 | e: 16~23 |
-------------------------------------------------------------
[a, b, c, d, e] address OFFSET is 0, 2, 4, 8, 16

可以发现,结果符合准则。首先基于准则一找到对齐模数为最大的double,其宽度为8。然后基于第二条,要使b满足则,需要addr % 2 == 0,因此需要addr为2,同理对e前一样。

更多详细补充参考:REF

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值