热门标签 | HotTags
当前位置:  开发笔记 > IOS > 正文

C语言、C++内存对齐问题详解

这篇文章主要介绍了C语言、C++内存对齐问题详解,内存对齐的问题主要存在于理解struct和union等复合结构在内存中的分布,需要的朋友可以参考下

这也可以?

代码如下:

#include
using namespace std;
 
struct Test_A
{
     char a;
     char b;
     int c;
};
 
struct Test_B
{
     char a;
     int c;
     char b;
};
 
struct Test_C
{
     int c;
     char a;
     char b;
};
 
int main()
{
     struct Test_A a;
     memset(&a, 0, sizeof(a));
 
     struct Test_B b;
     memset(&b, 0, sizeof(b));
 
     struct Test_C c;
     memset(&c, 0, sizeof(c));
 
     // Print the memory size of the struct
     cout<      cout<      cout<  
     return 0;
}

好了,一段简单的程序,上面的这段程序输出是什么?如果你很懂,也就会知道我接下来要讲什么了,可以略过了;如果,你不知道,或者还很模糊,请继续阅读。

这是为什么?

上面这段程序的输出结果如下(windows 8.1 + visual studio 2012 update3下运行):

代码如下:

// Print the memory size of the struct
cout< cout< cout<

很奇怪么?定义的三个结构体,只是换了一下结构体中定义的成员的先后顺序,怎么最终得到的结构体所占用的内存大小却不一样呢?很诡异么?好了,这就是我这里要总结的内存对齐概念了。

内存对齐

内存对齐的问题主要存在于理解struct和union等复合结构在内存中的分布。许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。这个值k在不同的CPU平台下,不同的编译器下表现也有所不同,现在我们涉及的主流的编译器是Microsoft的编译器和GCC。

对于我们这种做上层应用的程序员来说,真的是很少考虑内存对齐这个问题的,内存对齐对于上层程序员来说,是“透明的”。内存对齐,可以说是编译器做的工作,编译器为程序中的每个数据块安排在适当的内存位置上。很多时候,我们要写出效率更高的代码,此时我们就需要去了解这种内存对齐的概念,以及编译器在后面到底偷偷摸摸干了点什么。特别是对于C和C++程序员来说,理解和掌握内存对齐更是重要的。

为什么要有内存对齐呢?该占用多大的内存,那就开辟对应大小的内存就好了,好比上面的结构体,两个char类型和一个int类型,大小应该是6bytes才对啊,怎么又是8bytes,又是12bytes的啊?对于内存对齐,主要是为了提高程序的性能,数据结构,特别是栈,应该尽可能地在自然边界上对齐。原因在于,为了访问未对其的内存,处理器需要做两次内存访问;然而,对齐的内存访问仅仅需要一次内存访问。

在计算机中,字、双字和四字在自然边界上不需要在内存中对齐(对字、双字和四字来说,自然边界分别是偶数地址,可以被4整除的地址和可以被8整除的地址)。如果一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,就被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数,但却没有跨越字边界,就被认为是对齐的,能够在一个总线周期中被访问。综上所述,内存对齐可以用一句话来概括——数据项只能存储在地址是数据项大小的整数倍的内存位置上。

我们再来看看一个简答的例子:

代码如下:

#include
 
struct Test
{
     char a;
     int b;
     int c;
     char d;
};
 
int main()
{
     struct Test structTest;
     printf("&a=%p\n", &structTest.a);
     printf("&b=%p\n", &structTest.b);
     printf("&c=%p\n", &structTest.c);
     printf("&d=%p\n", &structTest.d);
 
     printf("sizeof(Test)=%d\n", sizeof(structTest));
     return 0;
}

输出结果如下:

代码如下:

&a=00C7FA44
&b=00C7FA48
&c=00C7FA4C
&d=00C7FA50
sizeof(Test)=16

结构体Test的成员变量b占用字节数为4bytes,所以只能存储在4的整数倍的位置上,由于a只占用1一个字节,而a的地址00C7FA44和b的地址00C7FA48之间相差4bytes,这就说明,a其实也占用了4个字节,这样才能保证b的起始地址是4的整数倍。这就是内存对齐。如果没有内存对齐,我们再拿上面的代码作为例子,则可能输出结果如下:

代码如下:

&a=ffbff5e8
&b=ffbff5e9
&c=ffbff5ed
&d=ffbff5f1
sizeof(Test)=10

可以看到,a占用了一个字节,紧接着a之后就是b;之前也说了,内存对齐是操作系统为了快速访问内存而采用的一种策略,简单来说,就是为了防止变量的二次访问。操作系统在访问内存时,每次读取一定的长度(这个长度就是操作系统的默认对齐系数,或者是默认对齐系数的整数倍)。没有了内存对齐,当我们读取变量c时,第一次读取0xffbff5e8~0xffbff5ef的内存,第二次读取0xffbff5f0~0xffbff5f8的内存,由于变量c所占用的内存跨越了两片地址区域,为了正确得到变量c的值,就需要读取两次,将两次内存合并进行整合,这样就降低了内存的访问效率。

我在这里说了这么多,也挺绕口,这就是内存对齐的规则。在C++中,每个特定平台上的编译器都有自己的内存对齐规则,下面我们就内存对齐的规则进行总结。

内存对齐规则

每个特定平台上的编译器都有自己的默认“对齐系数”。我们可以通过预编译命令#pragma pack(k),k=1,2,4,8,16来改变这个系数,其中k就是需要指定的“对齐系数”;也可以使用#pragma pack()取消自定义字节对齐方式。具体的对齐规则如下:

规则1:struct或者union的数据成员对齐规则:第一个数据成员放在offset为0的地方,对齐按照#pragma pack指定的数值和自身占用字节数中,二者比较小的那个进行对齐;比如;

代码如下:

#pragma pack(4) // 指定对齐系数为4,当占用字节数大于等于4时,就按照4进行对齐
struct Test
{
     char x1;
     short x2;
     float x3;
     char x4;
};

x1占用字节数为1,1 <4,按照对齐系数1进行对齐,所以x1放置在offset为0的位置;
x2占用字节数为2,2 <4,按照对齐系数2进行对齐,所以x2放置在offset为2,3的位置;
x3占用字节数为4,4 = 4,按照对齐系数4进行对齐,所以x3放置在offset为4,5,6,7的位置;
x4占用字节数为1,1 <4,按照对齐系数1进行对齐,所以x4放置在offset为8的位置;
现在已经占了9bytes的内存空间了,但是实际在visual studio 2012中实测为12bytes,为什么呢?看下一条规则。

规则2:struct或者union的整体对齐规则:在数据成员完成各自对齐以后,struct或者union本身也要进行对齐,对齐将按照#pragma pack指定的数值和struct或者union中最大数据成员长度中比较小的那个进行;

继续使用规则1种的例子进行解释,按照规则1的理解,struct Test已经占用了9bytes,实际为什么是12bytes呢?根据规则2,在所有成员对齐完成以后,struct或者union自身也要进行对齐;我们设定的对齐系数为4,而struct Test中占用字节数最大的是float类型的x3,由于x3占用字节数小于或等于设定的对齐系数4,所以struct或者union整体需要按照4bytes进行对齐,也就是说,struct或者union占用的字节数必须能够被4整除,好了。struct Test已经占用了9bytes了,10bytes不能被4整除,11bytes也不能,12bytes正好;所以,struct Test最终占用的字节数为12bytes。

上述两条规则就是内存对齐的基本规则,先局部对齐,后整体对齐。

实例分析

总结了那么多的规则,不来点实际的code,总觉的少点什么,好吧。以下就按照上述总结的内存对齐规则,来进行一些实际的代码分析(注:测试环境Windows 8.1 + Visual Studio 2012 update 3)。

测试代码如下,先确认测试环境:

代码如下:

#include
using namespace std;
 
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};
 
int main()
{
     cout<<"sizeof(char)"<      cout<<"sizeof(short)"<      cout<<"sizeof(int)"<      cout<<"sizeof(double)"<      return 0;
}

我分别设置#pragma pack(k),k=1,2,4,8,16进行测试。

代码如下:

#pragma pack(1) // 设定对齐系数为1
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:
x1 <= 1,按照1进行对齐,x1占用0;
x2 > 1,按照1进行对齐,x2占用1,2,3,4,5,6,7,8;
x3 > 1,按照1进行对齐,x3占用9,10;
x4 > 1,按照1进行对齐,x4占用11,12,13,14;
x5 > 1,按照1进行对齐,x5占用15;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes > 1byte,所以整体按照1进行对齐;16%1=0。
所以,在#pragma pack(1) 的情况下,struct Test占用内存为16bytes;内存占用如下图所示:

代码如下:

#pragma pack(2) // 设定对齐系数为2
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:
x1 <= 2,按照1进行对齐,x1占用0;
x2 > 2,按照2进行对齐,x2占用2,3,4,5,6,7,8,9;
x3 >= 2,按照2进行对齐,x3占用10,11;
x4 > 2,按照2进行对齐,x4占用12,13,14,15;
x5 <2,按照1进行对齐,x5占用16;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes > 2byte,所以整体按照2进行对齐;17%2!=0
所以,在#pragma pack(2) 的情况下,struct Test占用内存为18bytes;内存占用如下图所示:

代码如下:

#pragma pack(4) // 设定对齐系数为4
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:
x1 <= 4,按照1进行对齐,x1占用0;
x2 > 4,按照4进行对齐,x2占用4,5,6,7,8,9,10,11;
x3 <4,按照2进行对齐,x3占用12,13;
x4 >= 4,按照4进行对齐,x4占用16,17,18,19;
x5 <4,按照1进行对齐,x5占用20;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes > 4byte,所以整体按照4进行对齐;21%4!=0
所以,在#pragma pack(4) 的情况下,struct Test占用内存为24bytes;内存占用如下图所示:

代码如下:

#pragma pack(8) // 设定对齐系数为8
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:
x1 <= 8,按照1进行对齐,x1占用0;
x2 >= 8,按照8进行对齐,x2占用8,9,10,11,12,13,14,15;
x3 <8,按照2进行对齐,x3占用16,17;
x4 <= 8,按照4进行对齐,x4占用20,21,22,23;
x5 <8,按照1进行对齐,x5占用24;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,8bytes >= 8byte,所以整体按照8进行对齐;25%8!=0
所以,在#pragma pack(8) 的情况下,struct Test占用内存为32bytes;内存占用如下图所示:

代码如下:

#pragma pack(16) // 设定对齐系数为16
struct Test
{
     char x1;
     double x2;
     short x3;
     float x4;
     char x5;
};

首先使用规则1,对成员变量进行对齐:
x1 <16,按照1进行对齐,x1占用0;
x2 <16,按照8进行对齐,x2占用8,9,10,11,12,13,14,15;
x3 <16,按照2进行对齐,x3占用16,17;
x4 <16,按照4进行对齐,x4占用20,21,22,23;
x5 <16,按照1进行对齐,x5占用24;
最后使用规则2,对struct整体进行对齐:
x2占用内存最大,为8bytes,16bytes >= 8byte,所以整体按照8进行对齐;25%8!=0
所以,在#pragma pack(16) 的情况下,struct Test占用内存为32bytes;内存占用如下图所示:

总结

经过上面的实例分析,我对内存对齐有了全面的认识和了解。现在再回过来看看文章开头的那段代码,问题就迎刃而解了,同时经过这段代码,让我们认识到定义struct或者union时,也是有讲解的。在以后的编码生涯时,是不是又要多考虑一些呢?纠结~


推荐阅读
  • 探索新一代API文档工具,告别Swagger的繁琐
    对于后端开发者而言,编写和维护API文档既繁琐又不可或缺。本文将介绍一款全新的API文档工具,帮助团队更高效地协作,简化API文档生成流程。 ... [详细]
  • 本问题探讨了在特定条件下排列儿童队伍的方法数量。题目要求计算满足条件的队伍排列总数,并使用递推算法和大数处理技术来解决这一问题。 ... [详细]
  • Appium + Java 自动化测试中处理页面空白区域点击问题
    在进行移动应用自动化测试时,有时会遇到某些页面没有返回按钮,只能通过点击空白区域返回的情况。本文将探讨如何在Appium + Java环境中有效解决此类问题,并提供详细的解决方案。 ... [详细]
  • 如何清除Chrome浏览器地址栏的特定历史记录
    在使用Chrome浏览器时,你可能会发现地址栏保存了大量浏览记录。有时你可能希望删除某些特定的历史记录而不影响其他数据。本文将详细介绍如何单独删除地址栏中的特定记录以及批量清除所有历史记录的方法。 ... [详细]
  • 利用Selenium与ChromeDriver实现豆瓣网页全屏截图
    本文介绍了一种使用Selenium和ChromeDriver结合Python代码,轻松实现对豆瓣网站进行完整页面截图的方法。该方法不仅简单易行,而且解决了新版Selenium不再支持PhantomJS的问题。 ... [详细]
  • 本文探讨了在构建应用程序时,如何对不同类型的数据进行结构化设计。主要分为三类:全局配置、用户个人设置和用户关系链。每种类型的数据都有其独特的用途和应用场景,合理规划这些数据结构有助于提升用户体验和系统的可维护性。 ... [详细]
  • 气象对比分析
    本文探讨了不同地区和时间段的天气模式,通过详细的图表和数据分析,揭示了气候变化的趋势及其对环境和社会的影响。 ... [详细]
  • 通常情况下,修改my.cnf配置文件后需要重启MySQL服务才能使新参数生效。然而,通过特定命令可以在不重启服务的情况下实现配置的即时更新。本文将详细介绍如何在线调整MySQL配置,并验证其有效性。 ... [详细]
  • Python自动化测试入门:Selenium环境搭建
    本文详细介绍如何在Python环境中安装和配置Selenium,包括开发工具PyCharm的安装、Python环境的设置以及Selenium包的安装方法。此外,还提供了编写和运行第一个自动化测试脚本的步骤。 ... [详细]
  • 本文详细介绍如何在 iOS 7 环境下申请苹果开发者账号,涵盖从访问开发者网站到最终激活账号的完整流程。包括选择个人或企业账号类型、付款方式及注意事项等。 ... [详细]
  • 为了使您的电脑更加个性化,许多用户希望为Windows 10桌面设置自己喜欢的多张图片。本文将详细介绍如何轻松实现这一目标,并提供一些专业建议,确保您能够充分利用系统功能。 ... [详细]
  • 如何在电脑上同时登录多个微信账号?实用技巧全解析
    本文详细介绍了如何在电脑上同时登录多个微信账号的方法,并分享了一些微信的隐藏小技巧,帮助用户更高效地使用微信。 ... [详细]
  • Linux环境下进程间通信:深入解析信号机制
    本文详细探讨了Linux系统中信号的生命周期,从信号生成到处理函数执行完毕的全过程,并介绍了信号编程中的注意事项和常见应用实例。通过分析信号在进程中的注册、注销及处理过程,帮助读者理解如何高效利用信号进行进程间通信。 ... [详细]
  • 2004年春节,作者与父亲讨论了未来的职业规划,并决定尝试创业开设家教培训班。然而,创业过程中的种种困难和挑战最终导致了项目的失败。 ... [详细]
  • 设计模式在软件开发中被广泛应用,但如果不当使用,可能会导致系统复杂性增加。例如,过度添加类可能导致类图难以理解,代码跟踪变得复杂。本文探讨如何在使用设计模式时保持系统的简洁和高效。 ... [详细]
author-avatar
海带木耳求_529
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有