结构体和类的内存字节对齐详解

2024-01-04 05:48

本文主要是介绍结构体和类的内存字节对齐详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文转载自:http://www.jizhuomi.com/software/567.html


先说个题外话:早些年我学C程序设计时,写过一段解释硬盘MBR分区表的代码,对着磁盘编辑器怎么看,怎么对,可一执行,结果就错了。当时调试也不太会,又根本没听过结构体对齐这一说,所以,问题解决不了,好几天都十分纠结。后来万般无奈请教一个朋友,才获悉可能是结构体对齐的事,一查、一改,果真如此。

  问题是解决了,可网上的资料多数只提到内存对齐是如何做的,却鲜有提及为什么这样做(即使提,也相当简单)。笔者是个超级健忘者,很难机械式的记住这些破规则,于是仔细想了想,总算明白了原因,这样,这些对齐的规则也就不会再轻易忘记了。

  不光结构体存在内存对齐一说,类(对象)也如此,甚至于所有变量在内存中的存储也有对齐一说(只是这些对程序员是透明的,不需要关心)。实际上,这种对齐是为了在空间与复杂度上达到平衡的一种技术手段,简单的讲,是为了在可接受的空间浪费的前提下,尽可能的提高对相同运算过程的最少(快)处理。先举个例子:

  假设机器字长是32位的(即4字节,下面示例均按此字长),也就是说处理任何内存中的数据,其实都是按32位的单位进行的。现在有2个变量:

C++代码
  1. char A;   
  2. int B;   

  假设这2个变量是从内存地址0开始分配的,如果不考虑对齐,应该是这样存储的(见下图,以intel上的little endian为例,为了形象,每16个字节分做一行,后同):

结构体和类的内存字节对齐详解

  因为计算机的字长是4字节的,所以在处理变量A与B时的过程可能大致为:

  A:将0x00-0x03共32位读入寄存器,再通过左移24位再右移24位运算得到a的值(或与0x000000FF做与运算)

  B:将0x00-0x03这32位读入寄存器,通过位运算得到低24位的值;再将0x04-0x07这32位读入寄存器,通过位运算得到高8位的值;再与最先得到的24位做位运算,才可得到整个32位的值。

  上面叙述可知,对a的处理是最简处理,可对b的处理,本身是个32位数,处理的时候却得折成2部分,之后再合并,效率上就有些低了。

  想解决这个问题,就需要付出几个字节浪费的代价,改为下图的分配方式:

结构体和类的内存字节对齐详解

  按上面的分配方式,A的处理过程不变;B却简单得多了:只需将0x04-0x07这32位读入寄存器就OK了。

  我们可以具体谈结构体或类成员的对齐了:

  结构体在编译成机器代码后,其实就没有本身的集合概念了,而类,实际上是个加强版的结构体,类的对象在实例化时,内存中申请的就是一些变量的空间集合(类似于结构体,同时也不包含函数指针)。这些集合中的每个变量,在使用中,都需要涉及上述的加工原则,自然也就需要在效率与空间之间做出权衡。

  为了便捷加工连续多个相同类型原始变量,同时简化原始变量寻址,再汇总上述最少处理原则,通常可以将原始变量的长度做为针对此变量的分配单位,比如内存可用64个单元,如果某原始变量长度为8字节,即使机器字长为4字节,分配的时候也以8字节对齐(看似IO次数是相同的),这样,寻址、分配时,均可以按每8字节为单位进行,简化了操作,也可以更高效。

  系统默认的对齐规则,追求的至少两点:1、变量的最高效加工 2、达到目的1的最少空间

  举个例子,一个结构体如下:

C++代码
  1. typedef struct T  
  2. {   
  3.     char c; //本身长度1字节   
  4.     __int64 d;  //本身长度8字节  
  5.     int e;  //本身长度4字节  
  6.     short f;  //本身长度2字节  
  7.     char g;  //本身长度1字节  
  8.     short h;  //本身长度2字节  
  9. };   

  假设定义了一个结构体变量C,在内存中分配到了0x00的位置,显然:

  对于成员C.c  无论如何,也是一次寄存器读入,所以先占一个字节。

  对于成员C.d  是个64位的变量,如果紧跟着C.c存储,则读入寄存器至少需要3次,为了实现最少的2次读入,至少需要以4字节对齐;同时对于8字节的原始变量,为了在寻址单位上统一,则需要按8字节对齐,所以,应该分配到0x08-0xF的位置。

  对于成员C.e  是个32位的变量,自然只需满足分配起始为整数个32位即可,所以分配至0x10-0x13。

  对于成员C.f  是个16位的变量,直接分配在0x14-0x16上,这样,反正只需一次读入寄存器后加工,边界也与16位对齐。

  对于成员C.g  是个8位的变量,本身也得一次读入寄存器后加工,同时对于1个字节的变量,存储在任何字节开始都是对齐,所以,分配到0x17的位置。

  对于成员C.h  是个16位的变量,为了保证与16位边界对齐,所以,分配到0x18-0x1A的位置。

  分配图如下(还不正确,耐心读下去):

结构体和类的内存字节对齐详解

  结构体C的占用空间到h结束就可以了吗?我们找个示例:如果定义一个结构体数组 CA[2],按变量分配的原则,这2个结构体应该是在内存中连续存储的,分配应该如下图:

结构体和类的内存字节对齐详解

  分析一下上图,明显可知,CA[1]的很多成员都不再对齐了,究其原因,是结构体的开始边界不对齐。

  那结构体的开始偏移满足什么条件才可以使其成员全部对齐呢。想一想就明白了:很简单,保证结构体长度是原始成员最长分配的整数倍即可。

  上述结构体应该按最长的.d成员对齐,即与8字节对齐,这样正确的分配图如下:

结构体和类的内存字节对齐详解

  当然结构体T的长度:sizeof(T)==0x20;

  再举个例子,看看在默认对齐规则下,各结构体成员的对齐规则:

C++代码
  1. typedef struct A   
  2. {   
  3.     char c;  //1个字节  
  4.     int d;  //4个字节,要与4字节对齐,所以分配至第4个字节处  
  5.     short e;  //2个字节, 上述两个成员过后,本身就是与2对齐的,所以之前无填充  
  6.  }; //整个结构体,最长的成员为4个字节,需要总长度与4字节对齐,所以, sizeof(A)==12   
  7. typedef struct B   
  8. {   
  9.     char c;  //1个字节  
  10.     __int64 d;  //8个字节,位置要与8字节对齐,所以分配到第8个字节处  
  11.     int e;  //4个字节,成员d结束于15字节,紧跟的16字节对齐于4字节,所以分配到16-19  
  12.     short f;  //2个字节,成员e结束于19字节,紧跟的20字节对齐于2字节,所以分配到20-21  
  13.     A g;  //结构体长为12字节,最长成员为4字节,需按4字节对齐,所以前面跳过2个字节,   
  14. //到24-35字节处  
  15.     char h;   //1个字节,分配到36字节处  
  16.     int i;   //4个字节,要对齐4字节,跳过3字节,分配到40-43 字节  
  17. }; //整个结构体的最大分配成员为8字节,所以结构体后面加5字节填充,被到48字节。故:  
  18. //sizeof(B)==48;  

  具体的分配图如下:

结构体和类的内存字节对齐详解

  上述全部测试代码如下:

C++代码
  1. #include "stdio.h"   
  2. typedef struct A   
  3. {   
  4.     char c;   
  5.     int d;   
  6.     short e;   
  7.    
  8. };   
  9. typedef struct B   
  10. {   
  11.     char c;   
  12.     __int64 d;   
  13.     int e;   
  14.     short f;   
  15.     A g;   
  16.     char h;   
  17.     int i;   
  18. };   
  19. typedef struct C   
  20. {   
  21.     char c;   
  22.     __int64 d;   
  23.     int e;   
  24.     short f;   
  25.     char g;   
  26.     short h;   
  27. };   
  28. typedef struct D   
  29. {   
  30.     char a;   
  31.     short b;   
  32.     char c;   
  33. };   
  34. int main()   
  35. {   
  36.    
  37.     B *b=new B;   
  38.     void *s[32];   
  39.     s[0]=b;   
  40.     s[1]=&b->c;   
  41.     s[2]=&b->d;   
  42.     s[3]=&b->e;   
  43.     s[4]=&b->f;   
  44.     s[5]=&b->g;   
  45.     s[6]=&b->h;   
  46.     s[7]=&b->g.c;   
  47.     s[8]=&b->g.d;   
  48.     s[9]=&b->g.e;   
  49.     s[10]=&b->i;   
  50.     b->c= 0x11;   
  51.     b->d= 0x2222222222222222;   
  52.     b->e= 0x33333333;   
  53.     b->f=0x4444;   
  54.     b->g.c=0x50;   
  55.     b->g.d=0x51515151;   
  56.     b->g.e=0x5252;   
  57.     b->h=0x66;   
  58.     int i1=sizeof(A);   
  59.     int i2=sizeof(B);   
  60.     int i3=sizeof(C);   
  61.     int i4=sizeof(D);   
  62.     printf("i1:%d\ni2:%d\ni3:%d\ni4:%d\n",i1,i2,i3,i4);//12 48 32 6   
  63. }   

  运行时的内存情况如下图:

结构体和类的内存字节对齐详解

  最后,简单加工一下转载过来的内存对齐正式原则:

  先介绍四个概念:

  1)数据类型自身的对齐值:基本数据类型的自身对齐值,等于sizeof(基本数据类型)。

  2)指定对齐值:#pragma pack (value)时的指定对齐值value。

  3)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

  4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。

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

  #pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。

  如#pragma pack (1)  /*指定按2字节对齐*/

  #pragma pack () /*取消指定对齐,恢复缺省对齐*/


这篇关于结构体和类的内存字节对齐详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/568333

相关文章

Debezium 与 Apache Kafka 的集成方式步骤详解

《Debezium与ApacheKafka的集成方式步骤详解》本文详细介绍了如何将Debezium与ApacheKafka集成,包括集成概述、步骤、注意事项等,通过KafkaConnect,D... 目录一、集成概述二、集成步骤1. 准备 Kafka 环境2. 配置 Kafka Connect3. 安装 D

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SQL 中多表查询的常见连接方式详解

《SQL中多表查询的常见连接方式详解》本文介绍SQL中多表查询的常见连接方式,包括内连接(INNERJOIN)、左连接(LEFTJOIN)、右连接(RIGHTJOIN)、全外连接(FULLOUTER... 目录一、连接类型图表(ASCII 形式)二、前置代码(创建示例表)三、连接方式代码示例1. 内连接(I

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.