【C语言 | 预处理】C语言预处理详解(三)——内存对齐、手把手教你计算结构体大小

news2024/12/26 15:44:02

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍内存对齐、手把手教你计算结构体大小🍭
😎金句分享😎:🍭🍭

本文未经允许,不得转发!!!

目录

  • 🎄一、内存对齐是什么?
  • 🎄二、为什么需要内存对齐?
  • 🎄三、计算结构体大小
    • ✨3.1 对齐参数
    • ✨3.2 计算结构体大小的步骤和例子
  • 🎄四、#pragma pack 的使用方法
    • ✨4.1 用法一:#pragma pack (n)、#pragma pack ()
    • ✨4.2 用法二:#pragma pack(push)、#pragma pack(n)、#pragma pack(pop)
  • 🎄五、总结


在这里插入图片描述

🎄一、内存对齐是什么?

内存对齐(Memory Alignment) 是指将数据存储在内存中时,是按照特定的规则将数据放置在地址为其大小倍数的位置上。具体而言,内存对齐要求变量的起始地址是它对齐参数的整数倍

看下面这两个结构体,看看按照内存对齐的要求,是怎么存储的:

struct Test1
{
	char c1;
	short s;
	char c2;
	int i;
};

struct Test2
{
	char c1;
	char c2;
	short s;
	int i;
};

两个结构体虽然结构体成员一样,但他们所占用的内存大小却不一样,Test1占用12个字节,Test2占用8个字节,他们在内存中的存储大致如下图:
看看struct Test1按照内存对齐要求是怎样安排的,假设结构体首地址为0:

  • 成员 c1 自身大小为 1 个字节,是结构体第一成员,所以直接在地址0
  • 成员 s 自身大小为 2 个字节,按照要求,其起始地址必须是 2 的整数倍,所以不能放在地址1 的位置,起始地址为地址2
  • 成员 c1 自身大小为 1 个字节,起始地址需要为 1 的整数倍,直接安排在地址4
  • 成员 i 自身大小为 4 个字节,起始地址需要为 4 的整数倍,从地址5 开始往后找,地址8是4的整数倍。
    在这里插入图片描述

在这里插入图片描述

🎄二、为什么需要内存对齐?

计算机处理器为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。

所以,内存对齐的主要目的是提高访问数据的效率。当数据按照规定的对齐方式存放在内存中时,处理器可以更快地读取和存储数据,而不需要执行额外的内存操作。未对齐的数据可能导致性能下降,甚至在某些架构中导致程序崩溃。

所以,默认情况下,编译器都会将结构、栈中的成员数据进行内存对齐。

在这里插入图片描述

🎄三、计算结构体大小

按照内存对齐的要求,结构体在内存中是怎么存储的?这小节介绍复杂结构体各个成员在内存中怎么存储?

这里的复杂结构体是指包含了结构体和数组的结构体。先看看下面结构体ST_2,你能清楚它各个成员的在结构体中的偏移量吗?

typedef struct st
{
	char c;		// 起始地址 0; 结束地址 1
	int  i;		// 起始地址 4; 结束地址 8
	double d;	// 起始地址 8; 结束地址 16
}ST; // 结构体大小 16 字节

typedef struct st2
{
	char c;         
	int  i;        
	ST   st;       
	double d;       
	char c2;       
	ST   st_arr[3];
	long l;     
	int  i_arr[9]; 
}ST2;

✨3.1 对齐参数

在计算结构体大小之前,先了解几个概念:

  • 对齐参数:编译器进行内存对齐时,会涉及到一个对齐参数,不同的对齐参数,计算出来的结构体大小会不一样。可以通过下面代码查看编译器的默认对齐参数,(并非"32位系统就是4,64位系统为8");
    // default_align.c
    // gcc default_align.c -std=c11
    #include <stdio.h>
    #include <stddef.h>
    int main(void)
    {
    	printf("Default alignment: %zu\n", __alignof__(max_align_t));
    	printf("Biggest alignment: %d\n", __BIGGEST_ALIGNMENT__);
    	return 0;
    }
    
  • 基础数据类型的对齐方式:对齐参数就是类型大小;
  • 数组的对齐方式:对齐参数就是单个数组元素的大小。比如:char a[3];它的对齐方式和分别写 3 个 char 是一样的;也就是说它还是按 1 个字节对齐。
  • 结构体的对齐方式:对齐参数就是成员中的最大对齐参数。比如:上面的结构体 ST ,对齐参数就是成员d的对齐参数 8。

✨3.2 计算结构体大小的步骤和例子

计算结构体大小的步骤(计算各个成员的地址):

  • 1、确定成员对齐参数:成员的对齐参数是 自身对齐参数系统对齐参数中较小的一个。
  • 2、确定成员起始地址:起始地址为成员对齐参数的整数倍;
  • 3、确定结构体大小:最终结构体大小必须是最大对齐参数的整数倍。

在这里插入图片描述
按照步骤计算上面 ST2 结构体大小,假设结构体起始地址为0,系统对齐参数为 4,系统是32位系统:

  • 第一个成员 c:位于结构体第一个,起始地址为 0,结束地址为 1;

  • 第二个成员 i:自身对齐参数为 4 (sizeof(int)=4),等于系统对齐参数 4,所以成员对齐参数为 4
    起始地址为:从上个成员结束地址1开始数,找到4的整数倍,结果为 4,所以起始地址为 4,结束地址为 8;

  • 第三个成员 st:自身对齐参数为 8 (ST结构体最大对齐参数为8),大于系统对齐参数 4,所以成员对齐参数为 4
    起始地址为:从上个成员结束地址8开始数,找到4的整数倍,结果为 8,所以起始地址为 4,结束地址为 24;

  • 第四个成员 d:自身对齐参数为 8 (sizeof(double)=8),大于系统对齐参数 4,所以成员对齐参数为 4
    起始地址为:从上个成员结束地址24开始数,找到4的整数倍,结果为 24,所以起始地址为 24,结束地址为 32;

  • 第五个成员 c2:自身对齐参数为 1 (sizeof(char)=1),小于系统对齐参数 4,所以成员对齐参数为 1
    起始地址为:从上个成员结束地址32开始数,找到1的整数倍,结果为 32,所以起始地址为 32,结束地址为 33;

  • 第六个成员 st_arr:是一个结构体数组,对齐参数就是单个数组元素的对齐参数,数组元素又是一个结构体ST,按照该结构体最大对齐参数8作为自身对齐参数,大于系统对齐参数 4,所以成员对齐参数为 4
    起始地址为:从上个成员结束地址33开始数,找到4的整数倍,结果为 36,所以起始地址为 36,结束地址为 84;

  • 第七个成员 l:自身对齐参数为 4 (sizeof(double)=4),等于系统对齐参数 4,所以成员对齐参数为 4
    起始地址为:从上个成员结束地址84开始数,找到4的整数倍,结果为 84,所以起始地址为 84,结束地址为 88;

  • 第八个成员 i_arr:是一个结构体int型数组,数据元素是int类型的,所以自身对齐参数为 4 (sizeof(int)=4),等于系统对齐参数 4,所以成员对齐参数为 4
    起始地址为:从上个成员结束地址88开始数,找到4的整数倍,结果为 88,所以起始地址为 88,结束地址为 124;

计算完成员地址后,找出最大对齐参数,这个例子是 4,目前内存存储到124个字节,是4的整数倍,所以结构体ST2的大小是 124。

可以使用下面的代码验证是否计算正确:

// memAlign.c
// gcc memAlign.c -o memAlign
#include <stdio.h>
#include <stddef.h>

#pragma pack (4)
typedef struct st
{
	char c;		// 起始地址 0; 结束地址 1
	int  i;		// 起始地址 4; 结束地址 8
	double d;	// 起始地址 8; 结束地址 16
}ST; // 结构体大小 16 字节

typedef struct st2
{
	char c;         
	int  i;        
	ST   st;       
	double d;       
	char c2;       
	ST   st_arr[3];
	long l;     
	int  i_arr[9]; 
}ST2;
#pragma pack ()

int main(void)
{
	printf("c:%zu i:%zu st:%zu d:%zu c2:%zu st_arr:%zu l:%zu i_arr:%zu\n", 
		offsetof(ST2, c), offsetof(ST2, i), offsetof(ST2, st), 
		offsetof(ST2, d), offsetof(ST2, c2), offsetof(ST2, st_arr), 
		offsetof(ST2, l), offsetof(ST2, i_arr));
	return 0;
}

在这里插入图片描述

🎄四、#pragma pack 的使用方法

#pragma pack 可以用来改变编译器的默认对齐方式,也就是改变上文提到的系统对齐参数;

#pragma pack(n) 的n只能是2的次方幂,目前测试了,n的值可以为1、2、3、8、16,当设置32时会报错。

✨4.1 用法一:#pragma pack (n)、#pragma pack ()

使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
用法可以参考下面代码,表示从#pragma pack (4)开始到#pragma pack ()之间的代码的系统对齐参数是4:

#pragma pack (4)
typedef struct st
{
	char c;		// 起始地址 0; 结束地址 1
	int  i;		// 起始地址 4; 结束地址 8
	double d;	// 起始地址 8; 结束地址 16
}ST; // 结构体大小 16 字节

typedef struct st2
{
	char c;         
	int  i;        
	ST   st;       
	double d;       
	char c2;       
	ST   st_arr[3];
	long l;     
	int  i_arr[9]; 
}ST2;
#pragma pack ()

✨4.2 用法二:#pragma pack(push)、#pragma pack(n)、#pragma pack(pop)

#pragma pack(push):保存当前对其方式到 packing stack;
#pragma pack(n):设置编译器按照 n 个字节对齐;
#pragma pack(pop):packing stack 出栈,并设置为对齐参数;
用法参考下面代码:

#pragma pack (push)	// 保存现在的对齐参数 
#pragma pack (4)	// 将对齐参数改为 4
typedef struct st
{
	char c;		// 起始地址 0; 结束地址 1
	int  i;		// 起始地址 4; 结束地址 8
	double d;	// 起始地址 8; 结束地址 16
}ST; // 结构体大小 16 字节

typedef struct st2
{
	char c;         
	int  i;        
	ST   st;       
	double d;       
	char c2;       
	ST   st_arr[3];
	long l;     
	int  i_arr[9]; 
}ST2;
#pragma pack (pop)	// 恢复之前保存的对齐参数

在这里插入图片描述

🎄五、总结

本文介绍内存对齐,也解释了为什么需要内存对齐,最后演示了一个结构体是怎样按照计算占用内存大小的。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

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

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

相关文章

Elasticsearch:Lucene 中引入标量量化

作者&#xff1a;BENJAMIN TRENT 我们如何将标量量化引入 Lucene。 Lucene 中的自动字节量化 虽然 HNSW 是一种强大而灵活的存储和搜索向量的方法&#xff0c;但它确实需要大量内存才能快速运行。 例如&#xff0c;查询 768 维的 1MM float32 向量大约需要 1,000,000*4*(7681…

Spring Boot 集成 ElasticSearch

1 加入依赖 首先创建一个项目&#xff0c;在项目中加入 ES 相关依赖&#xff0c;具体依赖如下所示&#xff1a; <dependency><groupId>org.elasticsearch</groupId><artifactId>elasticsearch</artifactId><version>7.1.0</version&g…

「算法小记」-2:矩阵链相乘的方案数【迭代/递归/动态规划/区域化DP/记忆化搜索】(C++ )

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主、前后端开发、人工智能研究生。公粽号&#xff1a;程序员洲洲。 &#x1f388; 本文专栏&#xff1a;本文…

PHP 使用递归方式 将其二维数组整合为层级树 其中层级id 为一个uuid的格式 造成的诡异问题 已解决

不啰嗦 直接上源代码 <?php function findChildren($list, $p_id){$r array();foreach ($list as $k > $item) {if ($item[fid] $p_id) {unset($list[$k]);$length count($r);$r[$length] $item;if ($t findChildren($list, $item[id])) {$r[$length][children] …

Godot Shader -变量的声明

变量的声明 uniform 可以将值传递给着色器。这些值对整个着色器来说是全局的&#xff0c;被称为 uniform。当一个着色器后来被分配给一个材质时&#xff0c;uniform 将作为可编辑的参数出现在其中。uniform 不能从着色器内部写入。 可以在材质编辑器的修改这些uniform的值&a…

万宾科技内涝积水监测仪使用效果一览

当一个城市突降暴雨&#xff0c;对城市管理部门来讲首当其中的是防止积水成患。随着城市人口快速增长&#xff0c;基础设施建设也日益受到更多的关注&#xff0c;城市内涝问题频繁增加&#xff0c;会给城市带来严重的经济损失和人员的安全问题。城市生命线工程建设过程中&#…

黑客(网络安全)技术——高效自学1.0

前言 前几天发布了一篇 网络安全&#xff08;黑客&#xff09;自学 没想到收到了许多人的私信想要学习网安黑客技术&#xff01;却不知道从哪里开始学起&#xff01;怎么学 今天给大家分享一下&#xff0c;很多人上来就说想学习黑客&#xff0c;但是连方向都没搞清楚就开始学习…

屏幕提词软件Presentation Prompter mac中文版使用方法

Presentation Prompter for mac是一款屏幕提词器软件&#xff0c;它可以将您的Mac电脑快速变成提词器&#xff0c;支持编写或导入&#xff0c;可以在一个或多个屏幕上平滑地滚动&#xff0c;Presentation Prompter 下载是为适用于现场表演者&#xff0c;新闻广播员&#xff0c;…

Ps:选区的基本操作

在 Photoshop 中&#xff0c;选区是为处理局部图像而自行定义的一个区域。 定义选区后&#xff0c;操作被限制在选区之内。选区周围显示的虚线边框&#xff0c;俗称“蚂蚁线”。 全选 Select All Ps菜单&#xff1a;选择/全部 Select/All 快捷键&#xff1a;Ctrl A 提示&#…

JVM字符串常量池StringTable

目录 一、StringTable为什么要调整 二、String的基本特性 三、String的内存分配 四、字符串拼接操作 五、intern()方法 六、Stringtable的垃圾回收 七、G1中String去重操作 一、StringTable为什么要调整 jdk7之前&#xff0c;hotspot对于方法区的实现是永久代&#xff…

C语言之文件操作(详解版)

不知不觉我们已经学到C语言的文件操作部分了&#xff0c;这部分内容其实很有意思&#xff0c;因为它可以直接把我们代码中的数据写入硬盘&#xff0c;而不是我们关掉这个程序&#xff0c;代码就没有了&#xff0c;让我们开始学习吧&#xff01; 目录 1.为什么使用文件 2.什么…

伪造referer [极客大挑战 2019]Http1

打开题目 没有发现什么&#xff0c;我们查看源代码 在这里我们发现了提示 访问一下页面得到 提示说不能来自于https://Sycsecret.buuoj.cn&#xff0c;我们尝试访问一下这个url 发现访问不了 我们bp抓包一下 伪造个referer头 referer:https://Sycsecret.buuoj.cn 发包过去…

ChatGPT、GPT-4 Turbo接口调用

接口地址 https://chat.xutongbao.top/api/light/chat/createChatCompletion 请求方式 post 请求参数 model可选值&#xff1a; “gpt-3.5-turbo-1106”、 “gpt-3.5-turbo-16k” 、 “gpt-4”、“gpt-4-1106-preview”。 默认值为&#xff1a; “gpt-3.5-turbo-1106” to…

Delphi TCP服务端监听端口获取客户端RFID网络读卡器上传的刷卡数据

本示例使用设备介绍&#xff1a;液显WIFI无线网络HTTP协议RFID云读卡器可编程实时可控开关TTS语-淘宝网 (taobao.com) unit Unit1;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs, ComCtrls, ScktComp, StdCtrls, ScktCom…

JVM源码剖析之软、弱、虚引用的处理细节

目录 写在前面&#xff1a; 源码剖析&#xff1a; Java层面&#xff1a; JVM层面&#xff1a; 使用危险点&#xff1a; 总结&#xff1a; 版本信息&#xff1a; jdk版本&#xff1a;jdk8u40 垃圾回收器&#xff1a;Serial new/old 写在前面&#xff1a; 不同的垃圾回收…

PROFINET和UDP、MODBUS-RTU通信速度对比实验

这篇博客我们介绍PROFINET 和MODBUS-RTU通信实验时的数据刷新速度,以及这种速度不同对控制系统带来的挑战都有哪些,在介绍这篇对比实验之前大家可以参考下面的文章链接: S7-1200PLC和SMART PLC的PN智能从站通信 S7-200 SMART 和 S7-1200PLC进行PROFINET IO通信-CSDN博客文…

远程电脑未连接显示器时分辨率太小的问题处理

背景&#xff1a;单位电脑显示器坏了&#xff0c;使用笔记本通过向日葵远程连接&#xff0c;发现分辨率只有800*600并且不能修改&#xff0c;网上找了好久找到了处理方法这里记录一下&#xff0c;主要用到的是一个虚拟显示器软件usbmmidd_v2 1)下载usbmmidd_v2 2&#xff09;…

Flink SQL自定义表值函数(Table Function)

使用场景&#xff1a; 表值函数即 UDTF&#xff0c;⽤于进⼀条数据&#xff0c;出多条数据的场景。 开发流程&#xff1a; 实现 org.apache.flink.table.functions.TableFunction 接⼝实现⼀个或者多个⾃定义的 eval 函数&#xff0c;名称必须叫做 eval&#xff0c;eval ⽅法…

什么是进程等待?

什么是进程等待 在了解进程等待之前&#xff0c;我们要回顾一下什么是僵尸进程&#xff1a;是指一个已经终止执行的进程&#xff0c;但其父进程还没有通过 wait() 系统调用来获取该进程的退出状态信息。当一个进程正常退出或者被终止时&#xff0c;其所占用的系统资源会被操作…

基于SSM的智能物业管理网站的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…