C++结构体内存对齐规则

背景介绍

最近在使用Java语言写一个Java客户端,对接一个C/C++语言编写的Server时,采用TCP协议进行通信,在将C++结构体序列化的输出流转换为Java结构体时,需要按照结构体每个字段对应的字节长度截取字节流转换为Java类型,遇到了内存对齐的问题,在此总结一下。

内存对齐概念

什么是内存对齐

用一个例子带出这个问题,看下面的一段代码,理论上,32位系统下,int占4byte,char占一个byte,那么将它们放到一个结构体中应该占4+1=5byte;
但是实际上,通过运行程序得到的结果是8 byte,这就是内存对齐所导致的。

//32位系统
#include<stdio.h>
struct{int x;char y;
} s;int main()
{printf("%d\n",sizeof(s);  // 输出8return 0;
}

字节长度计算:sizeof函数

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。

为什么要进行内存对齐

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的。它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度

现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。

假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的连续四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。
在这里插入图片描述
现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
在这里插入图片描述
总结如下:

平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。所以内存对齐能够提高访问效率。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。

内存对齐规则

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n)n = 1,2,4,8,16来改变这一系数。

有效对齐值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位。

了解了上面的概念后,我们现在可以来看看内存对齐需要遵循的规则:

(1) 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

(2) 结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节

下面给出几个例子以便于理解:

//32位系统
#include<stdio.h>
struct
{int i;    char c1;  char c2;  
}x1;struct{char c1;  int i;    char c2;  
}x2;struct{char c1;  char c2; int i;    
}x3;int main()
{printf("%d\n", sizeof(x1));  // 输出8printf("%d\n", sizeof(x2));  // 输出12printf("%d\n", sizeof(x3));  // 输出8return 0;
}

以上测试都是在Linux环境下进行的,Linux下默认#pragma pack(4),且结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以s2来分析其内存布局:

首先使用规则1,对成员变量进行对齐:

sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元;
sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元;
sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元;

然后使用规则2,对结构体整体进行对齐:

s2中变量i占用内存最大占4字节,而有效对齐单位也为4字节,两者较小值就是4字节。因此整体也是按照4字节对齐。由规则1得到s2占9个字节,此处再按照规则2进行整体的4字节对齐,所以整个结构体占用12个字节。

根据上面的分析,不难得出上面例子三个结构体的内存布局如下:
在这里插入图片描述

#pragma pack(n)

不同平台上编译器的 pragma pack 默认值不同。而我们可以通过预编译命令#pragma pack(n), n= 1,2,4,8,16来改变对齐系数。

例如,对于上个例子的三个结构体,如果前面加上#pragma pack(1),那么此时有效对齐值为1字节,此时根据对齐规则,不难看出成员是连续存放的,三个结构体的大小都是6字节。
在这里插入图片描述
如果前面加上#pragma pack(2),有效对齐值为2字节,此时根据对齐规则,三个结构体的大小应为6,8,6。内存分布图如下:
在这里插入图片描述
经过上面的实例分析,大家应该对内存对齐有了全面的认识和了解,在以后的编码中定义结构体时需要考虑成员变量定义的先后顺序了。

结构体偏移量

下面给大家介绍一个宏叫做offsetof,它可以用来计算结构体成员相对于起始位置的偏移量

其语法如下:

size_t offsetof(type, member)

其中,type是结构体的类型,member是结构体中的成员名。
offsetof返回一个 size_t 类型的值,表示指定成员在结构体中的偏移量。偏移量是指该成员相对于结构体起始地址的字节偏移量。

使用示例:

#include<iostream>
#include<string>
#include<stddef.h>
using namespace std;#pragma pack(8) //修改默认对齐数为8struct A {char a;  char b;double c;
};struct B {char a;double b;char c;
};int main() {cout << "结构体A的大小:" << sizeof(A) << " a偏移-" <<offsetof(A, a) << " b偏移-" << offsetof(A, b) << " c偏移-" << offsetof(A, c) << endl;cout << "结构体B的大小:" << sizeof(B) << " a偏移-" <<offsetof(B, a) << " b偏移-" << offsetof(B, b) << " c偏移-" << offsetof(B, c) << endl;system("pause");
}

运行结果如下:

结构体A的大小:16 a偏移-0 b偏移-1 c偏移-8
结构体B的大小:24 a偏移-0 b偏移-8 c偏移-16

设计结构体的技巧

在了解了结构体的对齐规则之后,有没有一种方法能让我们在设计结构体的时候既满足对齐规则,又能尽量的节省空间呢?其实是有的,方法就是:让占用空间小的成员尽量集中在一起。就像的习题,我们把占用空间下的 a 和 b 放在一起,从而使得 struct A 比 struct B 小了8个字节。

填充字节规则

在C++中,当结构体(或类)的成员变量在内存中进行对齐时,为了满足特定的对齐要求,编译器可能会在成员之间或结构体/类的末尾插入填充字节(padding bytes)。这些填充字节的值并不是由C++标准指定的,也就是说,它们可以是任何值,这取决于具体的编译器实现和运行时环境。

在大多数情况下,这些填充字节的内容是未定义的,也就是说,你不能假设它们有任何特定的值。在某些情况下,这些字节可能包含垃圾值(即它们可能包含任何随机数据),在其他情况下,它们可能已经被其他代码段写入了不同的值。

因此,你不应该依赖这些填充字节的值来编写你的代码。如果你需要特定的内存布局或值,你应该显式地初始化它们,或者使用特定的编译器选项或属性来控制对齐和填充行为(尽管这可能会降低代码的可移植性)。

例如,在GCC和Clang中,你可以使用__attribute__((packed))来告诉编译器不要插入任何填充字节,但这可能会降低数据访问的效率,因为某些平台可能无法高效地访问未对齐的内存。同样,你也可以使用其他编译器特定的属性来控制对齐行为。但是,请注意,这些属性并不是C++标准的一部分,因此它们可能不适用于所有编译器或平台。

  1. 不要假设填充字节为任何特定的值,比如0;
  2. 从字节流转换为Java类型时,请使用有效字节数;

参考资料:

【底层原理】C/C++内存对齐详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/34662.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

19 Shell编程之条件语句

目录 19.1 条件测试操作 19.1.1 文件测试 19.1.1 整数值比较 19.1.3 字符串比较 19.1.4 逻辑测试 19.2 if条件语句 19.2.1 if语句的结构 19.2.2 if语句应用示例 19.3 case分支语句 19.3.1 case语句的结构 19.3.2 case语句应用示例 19.1 条件测试操作 Shell环境根据命令执行后…

栈与队列 Leetcode 239 滑动窗口最大值

栈与队列 Leetcode 239 滑动窗口最大值 Leetcode 239 要点&#xff1a;1.能想到单调队列&#xff0c;积累技巧&#xff1b; class Solution{ public:class My_Monotonous_Queue{public:deque<int> Mono_queue;// 重写队列pop函数内部实现&#xff0c;移除队列最前方的…

内容安全复习 1 - 信息内容安全概述

文章目录 信息内容安全简介网络空间信息内容安全大模型 人工智能简介 信息内容安全简介 网络空间 网络空间是融合物理域、信息域、认知域和社会域&#xff0c;控制实体行为的信息活动空间。 上图展示了网络空间安全的结构。可以看到将网络空间划分为了网络域和内容域两个部分。…

分布式缓存框架Hazelcast与Java整合详解

引言 在现代的分布式系统中&#xff0c;缓存是提高性能的关键组件之一。Hazelcast作为一个开源的分布式内存数据网格&#xff08;IMDG&#xff09;&#xff0c;提供了分布式缓存、集群和并发数据结构等功能。本文将详细介绍如何在Java应用中整合Hazelcast&#xff0c;并通过代…

Kotlin设计模式:深入解析Facade模式

Kotlin设计模式&#xff1a;深入解析Facade模式 在软件开发中&#xff0c;随着系统复杂度的增加&#xff0c;管理和使用多个相关接口变得越来越困难。这时候&#xff0c;Facade模式&#xff08;外观模式&#xff09;就显得尤为重要。本文将深入探讨Kotlin中的Facade模式&#…

利用LabVIEW和数字孪生技术实现PCB电路板测试

利用LabVIEW和数字孪生技术对PCB电路板进行测试&#xff0c;可以通过动画展示实现测试过程的生动、形象和直观。本文详细说明了如何结合LabVIEW与数字孪生技术进行PCB电路板的测试&#xff0c;包括系统架构、实现方法以及具体展示效果&#xff0c;适合对外展示。 在现代电子制造…

前端项目外包出去,是我痛苦的开始。如何破?

不止一个老铁给我反馈&#xff0c;他们把其前端项目外包出去&#xff0c;非常的痛苦&#xff0c;远不如用自己的员工省心。明面上钱省了&#xff0c;实际精力大量耗费在上面&#xff0c;一算账并没省&#xff0c;反而闹了一肚子气&#xff0c;问我这事该如何破&#xff1f;其实…

Leetcode Hot100之数组

1.最大子数组和 题目描述 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组是数组中的一个连续部分。解题思路 动态规划&#xff0c;遍历数组求取以当前元素为结尾的子…

mysql 查询排名,包括并列排名和连续排名

在MySQL中&#xff0c;根据不同的需求&#xff0c;查询排名可以分为并列排名和连续排名两种情况。 以下是分别实现这两种排名的方法&#xff0c;考虑到兼容性&#xff0c;这里会提供适合较早版本MySQL&#xff08;即8.0之前版本&#xff09;的解决方案&#xff0c;同时也提及M…

influxdb内存存储改为硬盘存储

根据直接部署和docker配置寻找到配置文件&#xff0c;添加内容 [data]engine "tsi1"index-version "tsi1" 即可降低内存占用

Java对象List根据ID去重

Java对象List根据ID去重 一、前言1. 使用HashSet去重2. 使用Stream API去重3. 使用HashMap去重4. 使用TreeSet去重并保持顺序5. 使用LinkedHashMap确保插入顺序 一、前言 在Java中处理大数据时&#xff0c;常常会遇到需要去重的情况。假设我们有一个对象数组&#xff0c;其中对…

CloudCompare二次开发目录(C++长期更新版)

目录 一、环境配置二、功能开发 本文由CSDN点云侠原创&#xff0c;原文链接。如果你不是在点云侠的博客中看到该文章&#xff0c;那么此处便是不要脸的爬虫与GPT。 一、环境配置 WIN10系统下VS2019编译CloudCompare2.12.4CloudCompare与PCL数据格式的相互转换 二、功能开发 …

2024年电商618观察:这是最好的时代 这是最坏的时代

内容提要 目前阶段增长势头更强劲的是中小商家&#xff0c;而星图的核心数据还是10万多个品牌。 十九世纪&#xff0c;英国最伟大的作家狄更斯在他的小说《双城记》开篇中写道&#xff1a; 这是最好的时代&#xff0c;这是最坏的时代&#xff1b; 这是智慧的年代&#xff0c;…

游戏工作室的得力助手:探索高效代理IP软件的选择与应用

在数字化浪潮的推动下&#xff0c;游戏产业蓬勃发展&#xff0c;游戏工作室作为这一领域的重要参与者&#xff0c;其运营效率和稳定性成为了影响业务成功的关键因素。而在众多提升运营效率的工具中&#xff0c;代理IP软件凭借其独特的功能和优势&#xff0c;成为了游戏工作室不…

【LeetCode】每日一题:判断子序列

给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;例如&#xff0c;"ace"是"abcde"的一个子序列&#x…

Softhsm2和Cryptoki的基本操作

0. 测试模块是否正常工作 sudo pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --show-info Cryptoki version 2.30 Manufacturer SoftHSM Library Implementation of PKCS11 (ver 2.2) 1. 列出所有槽(Slots) sudo pkcs11-tool --module /usr/lib/soft…

MyBatis(4)MyBatis 如何配置和使用

MyBatis 的配置和使用可以分为几个步骤进行详细解析。请注意&#xff0c;完整的源码分析是非常庞大的工作&#xff0c;这里会给出一个高层次的视图和关键代码演示。 1. MyBatis 配置 配置通常通过 mybatis-config.xml 配置文件进行&#xff0c;这个文件包括了对 MyBatis 行为…

台式扫描电镜工作距离越远观察区越大?

台式扫描电镜&#xff08;Scanning Electron Microscope, SEM&#xff09;是一种高分辨率的显微镜&#xff0c;它利用电子束扫描样品表面&#xff0c;通过样品与电子束相互作用产生的信号来形成图像。这种显微镜广泛应用于材料科学、生物学和医学等领域&#xff0c;以观察样品的…

提示词绕过大模型安全限制

大模型安全绕过策略 简介 本文使用简单的提示词&#xff0c;可以在所有场景中实现针对某开源模型的安全策略绕过。 glm-4-9b-chat 的安全措施还有待完善。 上一代的6b比这一代的9B&#xff0c;要安全&#xff1b;上一代的6B大模型这一招没有用。 正常对话 若在下述正常互动…

html +css 控制文本高度超出变成省略号

.overflow{height: 50px;display: -webkit-box; /* 使用Webkit的弹性盒子模型显示 */-webkit-line-clamp: 2; /* 限制在一个块元素显示的文本的行数 */-webkit-box-orient: vertical; /* 设置或检索伸缩盒对象的子元素的排列方式 */overflow: hidden; /* 隐藏超出容器的内容 */…