首页 > 系统相关 >结构体内存对齐

结构体内存对齐

时间:2023-09-09 17:45:02浏览次数:31  
标签:变量 成员 偏移量 默认 对齐 数是 体内 结构

结构体内存对齐是什么?

结构体内有一个或者多个成员变量,这些成员变量是要“对齐”的。这么说可能有点抽象,我们先来了解一下内存对齐的规则,以及几个概念。

每个成员变量都有一个“对齐数”,这个对齐数等于其自身大小和默认对齐数的较小值。

举个例子:

struct S
{
    int a;
    char c;
    double d;
];

对于上面这个结构体,有3个成员变量,每个成员变量都有一个对齐数。默认对齐数是什么,我们下一条规则再说,先假设默认对齐数是4。

  • a的大小是4,默认对齐数是4,取两者的较小值,得到a的对齐数是4。
  • c的大小是1,默认对齐数是4,取两者的较小值,得到c的对齐数是1。
  • d的大小是8,默认对齐数是4,取两者的较小值,得到d的对齐数是4。

默认对齐数是由编译器决定的。如:VS的默认对齐数是8,gcc没有默认对齐数。如果没有默认对齐数,每个成员变量的对齐数就是其自身大小(或者也可以假设默认对齐数是正无穷,取自身大小和默认对齐数的较小值,也是自身大小)。关于如何修改默认对齐数,本篇博客后面会讲解。

每个成员变量都有一个“偏移量”。这个偏移量指的是,该成员变量的起始地址与结构体的起始地址相差了几个字节。成员变量会按照声明的顺序,地址从低到高变化。第一个成员变量的偏移量是0,也就是说,第一个成员变量的起始地址和结构体的起始地址相同。或者也可以这样理解,对于一个结构体的空间内,每一个地址都对应一个偏移量,这个偏移量就是该地址和结构体的起始地址相差的字节数。

从第二个成员变量开始,偏移量是其对齐数的整数倍。

还是上面的结构体,我再写一遍,大家就不用向上翻了。

struct S
{
    int a;
    char c;
    double d;
];

假设默认对齐数是8,根据前面的计算,a、c、d的对齐数分别是4、1、4。

  • a作为第一个成员变量,偏移量是0。由于其大小是4,会占用偏移量是0、1、2、3的地址处。
  • 接下来可用的位置是偏移量为4的地址处,由于4就是c的对齐数的整数倍,故c会占用偏移量是4的位置,并且只占用这一个字节,因为c的大小是1个字节。
  • 接下来可用的位置是偏移量为5的地址处,注意,由于5不是d的对齐数的整数倍,接下来的6、7也不是,所以d的偏移量是8,并且占用8个字节,因为d的大小是8个字节,占用了偏移量为8、9、10、11、12、13、14、15的地址处。

综上所述,a占用偏移量为0、1、2、3的地址处,c占用偏移量为4的地址处,偏移量为5、6、7的地址处被浪费了,接下来d占用偏移量为8、9、10、11、12、13、14、15的地址处。由于有的位置被浪费了,这种浪费一定的空间,使得每个成员变量的偏移量都对齐到其对齐数的整数倍处的现象,就是内存对齐。

此时,这个结构体的大小是多少呢?看起来,这些成员变量占用了偏移量从0~15的地址处,总大小应该是16,但是究竟是不是16呢?这就要看下一条规则:

结构体的总大小是其所有成员变量的最大对齐数的整数倍。

由于以上结构体的成员变量的对齐数分别是4、1、4,最大对齐数是4,而16就是4的整数倍,所以它的大小就是16。注意,这是一种巧合,如果16不是最大对齐数的整数倍,还要继续“浪费”空间,最终大小是最大对齐数的整数倍。

假设根据前面偏移量的计算,最后一个成员变量的偏移量是16~19,此时总体的偏移量是0~19,总大小是不是20呢?假设所有成员变量的最大对齐数是8,那么20就不是最终的大小,21也不是,22也不是,23也不是,24是8的整数倍,所以最终算出来的结构体大小是24。

如果有嵌套的结构体呢?

如果有嵌套的结构体,最终的大小是所有成员变量(包括嵌套的结构体的成员变量)的最大对齐数的整数倍。

如果有的成员变量是数组呢?

数组在内存中是连续存放的,只需要计算其首元素的偏移量即可。注意,数组的对齐数只是一个元素的对齐数,也就是说,只需要计算一个元素的大小和默认对齐数的较小值。

如何快速计算结构体的大小?

在一些笔试面试的问题中,会要求计算结构体的大小。此时我们的计算步骤是:

  • 计算每个成员变量的对齐数(自身大小和默认对齐数的较小值)。
  • 计算每个成员变量占用的地址的偏移量。
  • 计算最终大小。

下面我再换一个结构体演示一下这几个步骤。

struct S
{
    char c1;
    double d;
    char c2;
    int i;
};

假设默认对齐数是8。

  1. 计算每个成员变量的对齐数(自身大小和默认对齐数的较小值),我用括号里的数表示该成员变量的对齐数。
struct S
{
    char c;     // (1)
    int i1;     // (4)
    double d;   // (8)
    int i2;     // (4)
    char ch[2]; // (1)
};
  1. 计算每个成员变量占用的地址的偏移量。我用中括号表示浪费的空间,用大括号表示占用的地址的偏移量。
struct S
{
    char c;     // (1) {0}
    int i1;     // (4) [1 2 3] {4 5 6 7}
    double d;   // (8) {8 9 10 11 12 13 14 15}
    int i2;     // (4) {16 17 18 19}
    char ch[2]; // (1) {20 21}
};
  1. 计算最终大小。

最大对齐数是8。从22往后数,直到数到8的倍数。22、23、24,停!没错,最终大小就是24。你学会了吗?

如何利用内存对齐节省结构体占用的内存空间?

观察到,只要把小的成员变量都放到一起,本来有可能会被浪费的空间就有可能被这些小的成员变量利用。感觉上,本来有些空间由于内存对齐的原因被浪费了,产生了空隙,而小的成员变量,就像沙子一样,可以填补这些空隙,这样就减少了空间的浪费,提高了空间的利用率。

为什么结构体要内存对齐?

主要有2个原因。

有些硬件只能访问对齐的位置的地址,如果没有内存对齐,可能有些数据是没办法访问的。
以空间换时间。如果没有内存对齐,可能需要访问2次才能读取一个数据,对齐之后,可能1次访问就能读取到数据,效率更高。举个例子:假设结构体内只有2个成员变量,分别是一个char数据和一个int数据,如果不对齐,char数据和int数据是挨着放的,只占用5个字节的空间,如果想要读取到int数据,假设一次只能读取4个字节,并且只能从对齐的位置(char数据所在的位置)开始读取,第一次只能读取到int数据的前3个字节,第二次读取才能读到int数据的最后一个字节,需要读取2次。但是如果对齐的话,就能直接从int数据的起始地址开始读,一次读取就能读取到整个int数据,效率就提升了,但是浪费了空间。所以说这是一种以空间换时间的策略。

如何修改默认对齐数?

可以用#pragma pack()来修改默认对齐数。比如:

#pragma pack(4)

以上代码就把默认对齐数修改为4。

#pragma pack()

以上代码,由于括号内没有数值,会把默认对齐数重置为编译器的默认值。

总结

结构体的内存对齐指的是,通过某些规则,“浪费”掉一定的空间,把每个成员变量的偏移量对齐到其对齐数的整数倍处。
可以通过3个步骤快速计算结构体大小,分别是:先计算每个成员变量的对齐数,再计算每个成员变量占用的地址的偏移量,最后计算整个结构体的大小。
把较小的成员变量放到一块,可以减小空间的浪费。
结构体的内存对齐是为了适应一些硬件,同时以空间换时间。
使用#pragma pack()来修改默认对齐数。

 

标签:变量,成员,偏移量,默认,对齐,数是,体内,结构
From: https://www.cnblogs.com/god-of-death/p/17689891.html

相关文章

  • 初识python--python的选择分支结构
    python选择结构语句一、if选择结构1、ifelse结构在日常业务中,经常需要进行多条件判断,为了这种场景,引入多分支结构age=int(input('请输入你的年龄:'))ifage<18:print(f"年龄{age},未到18岁,不可使用童工!")elifage>=18&age<=60:print('年龄为%d,合法年龄......
  • 2.10 PE结构:重建重定位表结构
    Relocation(重定位)是一种将程序中的一些地址修正为运行时可用的实际地址的机制。在程序编译过程中,由于程序中使用了各种全局变量和函数,这些变量和函数的地址还没有确定,因此它们的地址只能暂时使用一个相对地址。当程序被加载到内存中运行时,这些相对地址需要被修正为实际的绝对地址,这......
  • 2.11 PE结构:添加新的节区
    在可执行PE文件中,节(section)是文件的组成部分之一,用于存储特定类型的数据。每个节都具有特定的作用和属性,通常来说一个正常的程序在被编译器创建后会生成一些固定的节,通过将数据组织在不同的节中,可执行文件可以更好地管理和区分不同类型的数据,并为运行时提供必要的信息和功能。节的......
  • 2.10 PE结构:重建重定位表结构
    Relocation(重定位)是一种将程序中的一些地址修正为运行时可用的实际地址的机制。在程序编译过程中,由于程序中使用了各种全局变量和函数,这些变量和函数的地址还没有确定,因此它们的地址只能暂时使用一个相对地址。当程序被加载到内存中运行时,这些相对地址需要被修正为实际的绝对地址,......
  • 2.11 PE结构:添加新的节区
    在可执行PE文件中,节(section)是文件的组成部分之一,用于存储特定类型的数据。每个节都具有特定的作用和属性,通常来说一个正常的程序在被编译器创建后会生成一些固定的节,通过将数据组织在不同的节中,可执行文件可以更好地管理和区分不同类型的数据,并为运行时提供必要的信息和功能。节的......
  • 9.9数据结构
    ADT抽象数据类型:数据抽象、数据封装特点:数据封装,实现与现实分离,信息隐藏 数据元素:是数据的基本单位,在计算机中通常作为一个整体进行考虑和处理数据项:是组成数据元素的,有独有的含义,不可分割的最小单位 在计算机中存储数据时,通常不仅要存储各数据元素的值,还要存储数据元素......
  • 在公司学习日,学习了结构思考力
    大家好,我是Edison。近日,在公司的学习日上,学习了李忠秋老师的在线直播课《结构思考力》,虽然只有短短的一小时内容,但却令我印象深刻,以至于我开始逐渐刻意训练自己的结构化思维。所谓的《结构思考力》核心内容其实来源于一本经典的书籍巴巴拉·明托的《金字塔原理》,我老早就听说过,......
  • 数据结构-封装队列
    list_queue.h#ifndefLIST_QUEUE_H#defineLIST_QUEUE_H#include<stdio.h>#include<stdlib.h>#include<stdbool.h>#defineTYPEint// 节点结构typedefstructNode{ TYPEdata; structNode*next;}Node;// 设计链式队列结构typedefstructList......
  • Java对象创建过程,类的生命周期,Java的对象结构
    一、Java对象创建过程1、JVM遇到一条新建对象的指令时,首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用,然后加载这个类;2、为对象分配内存。一种办法时“指针碰撞”,一种办法是“空闲列表”,最终常用的办法是“本地线程缓冲分配”;3、将除对象头外的对象内存空间初始化......
  • C语言-结构体、共用体,内存管理
    结构体结构体的定义及变量使用#include<stdio.h>#include<string.h>structstudent{charname[20];intage;charsex;}stu3;//定义结构体的同时定义结构体变量。此时是全局变量intmain(intargc,charconst*argv[]){structstudentstu1,s......