内存对齐详解(C语言修仙版)

简介: 内存对齐详解(C语言修仙版)

一、什么是内存对齐?

你被四只妖物追杀至一片神秘的山脚下,现在它们快追来了,你刚想穿过一片竹林,却不小心绊到了什么,一瞬间,你到达了另一个地方,映入眼帘的是一块巨大石碑,石碑上刻着这样一段代码:


#include<stdio.h>
struct S1{
  char c1;
  int n;
  char c2;
};
struct S2{
  int n;
  char c1;
  char c2;
};
int main()
{
  struct S1 s1;
  struct S2 s2;
  printf("%d\n",sizeof(s1));
  printf("%d\n",sizeof(s2));
  return 0;
 }

上面代码定义了两个结构体,两个结构体成员相同,但是顺序不一样,按理来说两个结构体既然成员一样那么两个结构体大小也应相同,那么到底两个结构体的大小是否相同呢?    

   

 


由结果发现结构体成员不是按照顺序在内存中存放的,而是以一定规则存放在内存中的,结构体成员以一种规则在内存中占用不同的字节数,而这种规则就是我们今天要讲的内存对齐

没想到你误打误撞来到了一方小世界,还发现了内存对齐这等造化,这里被发现也是迟早的事,你想不如进去寻寻机缘,或许有救命之用。


二、内存对齐规则

你往深处走去,发现一道埂古的巨石门,门上愕然刻着“内存对齐四大法则”,你心中大喜,竟是内存对齐,这等莫大的机缘,机不可失失不再来,还没说完你就开门而入。

其实内存对齐也并非神秘莫测,我们只需掌握这四处规则,便可轻松拿捏内存对齐!

1.第一个成员在与结构体变量偏移量为0的地址处

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

3.结构体总大小为最大对齐数(每个成员变量都有对齐数)的整数倍

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整个大小就是最大对齐数(含嵌套结构体的对齐数)的整数倍

看不懂没关系,接下来我来带着你一一领悟这四条规则,实乃大造化,快快三连,保你领悟内存对齐的四大规则!


1.第一规则

第一个规则是入门级规则,简单易懂,首先,你得懂什么是内存的偏移量,内存中的偏移量是指成员变量的首地址相较于“0”地址处的距离,如下图所示,在结构体起始位置处存放的是结构体的第一个成员变量,偏移量为0,不论第一个结构体成员的大小,都是存放在0偏移量处。这第一个规则是不是很简单呢?




2.第二规则

恭喜你过了第一关,但是真正的试炼才刚刚开始!第二个规则,除了结构体起始成员变量放在0偏移量处,其他的结构体成员应该放在哪里呢?刚刚既然看到上面代码所呈现的结构体的大小并不一样,也应该能够理解这其中有什么规则在制约着,要想了解这是什么规则,你必须要了解一下对齐数这个概念 ,对齐数 = 编译器默认的一个对齐数与该成员大小的较小值

在VS编译器下默认对齐数为8(单位byte/字节),在Linux中没有默认对齐数,对齐数就是成员的本身大小。除了第一个结构体成员变量占用0位置处空间,其余成员变量都要按照对齐数来在内存中进行存放,在VS中成员变量的对齐数只要小于VS的默认对齐数,那么成员变量的对齐数就为他本身。

就拿上面代码的两个结构体举例,结构体S1,第一个成员c1,类型为char在0位置处占用一个字节的空间,第二个成员n,类型为int,小于默认对齐数8,则对齐数为4,而c1只是占用了一个字节,如果直接从c1后面开始存放n的话最终占用5个字节,违反了第二规则的最大对齐数整数倍,要想存放位置为整数倍最大对齐数,那只能从4位置开始存放,到7位置,从0到7为8个字节空间,符合最大对齐数的整数倍规则,算下来一共占用了8个字节空间,你可能有个疑问,c1后面还有3个字节空间哪里去了? 其实,为了内存对齐规则,只能把这没有利用的空间给浪费掉了。、

结构体S2,第一个成员n,类型为int在0位置处占用4个字节空间,且对齐数小于默认对齐数,成员c1,类型为char,小于默认对齐数,对齐数为1,5为1的整数倍,则在紧挨着n正下方存储,算下来占用了5个字节。



相信通过这两个例子,你已经理解了存储规则,那么不难得出,S1最后一个成员c2在偏移量为8的位置处存放,结构体S2成员c2在偏移量为5的位置处存放,但是怎么才能证明我说的是对的呢?看在你给我三连的份上,送你一件至宝————offsetof宏,此宝可助你直接查看结构体成员距离起始位置的偏移量,是不是很好用?“居然还有这种至宝不早点拿出来?!”,年轻人,修仙之路靠的是真本事,而不是靠这些身外之物,所以切不可因此宝而不练习真本事。我们来看看它的具体用法吧!



首先用这个·宏需要包含头文件 才能使用,offsetof宏使用起来很简单,它只有两个参数,第一个参数传结构体的类型,第二个参数传结构体成员变量。

#include<stdio.h>
#include<stddef.h>
struct S1{
  char c1;
  int n;
  char c2;
};
struct S2{
  int n;
  char c1;
  char c2;
};
int main()
{
  struct S1 s1;
  struct S2 s2;
  printf("%d\n",offsetof(struct S1,c2));
  printf("%d\n",offsetof(struct S2,c2));
  return 0;
 }



由打印结果来看我说的没错,相信我的答案和你的一样,到目前为止,你已经将第二规则掌握,是不是感觉神清气爽?但是你发现没有,照这么说的话结构体S1的大小应该是9个字节,而结构体S2的大小应该为6个字节啊,但是为什么最开始的答案却和自己的结果并不相同?难道是大道有误?实际上并非如此,别急,真正的答案揭晓还在接下来的第三规则,你,准备好了吗?


3.第三规则

继第二规则,我们发现最终不能彻底的“降妖除魔”,这是因为大道不全,没有发挥真正的威力,无法彻底根除,接下来,就让我们领悟这第三规则。

结构体的总大小,必须为最大对齐数的整数倍大小,也就是说,得先找到结构体的最大对齐数是多少才能确定最终到底需要占用多少字节的空间,而最大对齐数就为结构体成员中最大成员的大小,在结构体S1中,最大成员大小为n,类型为int,则(最大)对齐数为4,所以结构体总大小需要为4的最小倍数,又前两个结构体成员占用了9个字节,所以最小4的倍数就为12,从10-12这三个字节也会浪费,那么我相信S2为什么是8你也能够理解了。

由此可见,在结构体中,相同的成员以不同的顺序存储在内存中,所占用的内存大小也不相同,为了减轻内存的负担,可以以第二种的方式对结构体成员进行计算排序,使得浪费字节空间最小化。

此时,你已经掌握了内存对齐的法则之力!但是还是有一些不完美的地方,我知道一些对内存元空间的浪费你是忍不了的,所以接下来,让你看看法则的变换吧!

使用指令#pragma pack() 来改变默认对齐数大小,其实VS的默认对齐数为8也是这条指令造成的,#pragma pack(8),修改默认对齐数只需要把括号内的值改为自己想要的值即可,这样就可以以自己觉得比较省空间的方式来定义对齐数与结构体,此条指令要放在自定义结构体之前,且最好用完在使用#pragma pack()括号内不加任何东西解除自改变的默认对齐数,毕竟,还有其他道友可能也会来此领悟造化,不可断他人机缘啊施主。

至此,你已经完全领悟法则之力,只需熟练掌握,方可有所作为,正好小世界也被那几只妖魔发现了,可以拿他们来试试手,你明白,尽管方前没有交手,有一个气息要比旁边强大许多,你也要小心了!

首当其冲三只的妖魔来了!“桀桀桀!今天就要你命丧于此!”


struct S1//妖魔小弟1
{
  char c1;
  char c2;
  int a1;
};
struct S2//妖魔小弟2
{
  double s1;
  char c1;
  int a1;
  int a2;
};
struct S3//妖魔小弟3
{
  double s1;
  char c1;
  int a1;
  double s2;
};

“刷刷刷!”,法则之力的使出令妖物猝不及防,“这是?!啊!”, 你一下子就知道了s1为8字节,s2为24字节,s3为24字节!区区小妖何足挂齿!此时后面的大妖在不远处盯着你,“哦?法则之力吗?有点意思,吃了你吾的实力必定大增!桀桀桀!”,说时迟那时快,大妖直接瞬移到你的旁边,你暗自吃惊:“不好,这大妖实力恐怕不在我之下!”,“可恶,那就战吧!”。

struct S
{
  double s1;
  char c1;
  int a1;
  double s2;
};
struct SS
{
  double ss1;
  struct S s;
  int sa1;
};

经过一段时间的打斗,你竟然不敌这厮,“噗!”,你吐出一口鲜血,“难道这次真的要殒命于此了吗?我不甘心啊?”,“桀桀桀,受死吧!”,大妖发出了最后一击,眼看就要冲撞到你的身体,你知道这一击必让你元神俱灭,你实在没底牌了,缓缓闭上眼睛,想着对家人,好友说对不起。就在这时,脑海中的天地发生异变,一道声音从苍穹中传来:“小子,看好了,我只教这一次!法则之力是这样用的!”,你的身体被这个声音占据着...

首先看结构体S,不难看出整个结构体大小为24字节,再看结构体SS,首先ss1占用8个字节,s的类型为struct S,为24个字节,而结构体S当中,最大对齐数大小为8,嵌套的结构体对齐到自己的最大对齐数的整数倍处,则紧接着double ss1向下存储24个字节,现在已经32个字节,但是还剩下int类型的sa1,如果直接存放在s底下总字节数为36字节,不为8的整数倍,所以只能放在s下空四个字节后的位置,这样总占用空间字节大小就为40个字节了。

你突然领悟了,原来一直没出现的第四规则竟是前三种规则的融汇贯通所出现的,这下你终于通晓内存对齐的大造化了,至于那个大妖,在你回神之际就已躺在地下,毫无生机。你方才明白那个响彻天穹的声音竟是这小世界界主的神念,看来这界主生前很痛恨妖魔。


三、为什么会出现内存对齐?

1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数
据,否则抛出硬件异常。

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

在神念作用到你身上的时候,你也能看见那界主的记忆,原来界主生前极其痛恨妖魔,虽然自身强大,但是终究敌不过妖魔数量众多,只好究其毕生所学,创造如此法则,存于小世界中,但是为何小世界会出现在这里,你就不知晓了。

在你到尽头之后又出现一块石碑,上面写着:修仙之路漫长遥远,这内存对齐造化也只是冰山一角,如果想要得道升仙,还需提升自己的实力,以及看自己的机缘,如果道友能将三连留下,必定可得大机缘,得道成仙!(写这些想告诉大家,学习语言知识也可以是很有趣的,如果不喜欢,望轻点喷,如果有错误的地方还望各位大佬帮忙指正!)

相关文章
|
1月前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
48 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
2月前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
54 6
|
2月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
159 13
|
2月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
63 11
|
2月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
65 1
|
2月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
3月前
|
存储 程序员 编译器
C语言——动态内存管理与内存操作函数
C语言——动态内存管理与内存操作函数
|
3月前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
43 0
|
3月前
|
编译器 C语言 C++
【C语言】精妙运用内存函数:深入底层逻辑的探索
【C语言】精妙运用内存函数:深入底层逻辑的探索

热门文章

最新文章