深入理解C语言内存对齐

一.内存对齐的初步讲解

内存对齐可以用一句话来概括:

“数据项只能存储在地址是数据项大小的整数倍的内存位置上”

例如int类型占用4个字节,地址只能在0,4,8等位置上。

例1:


代码如下:

#include <stdio.h>
struct xx{
        char b;
        int a;
        int c;
        char d;
};

int main()
{
        struct xx bb;
        printf("&a = %p/n", &bb.a);
        printf("&b = %p/n", &bb.b);
        printf("&c = %p/n", &bb.c);
        printf("&d = %p/n", &bb.d);
        printf("sizeof(xx) = %d/n", sizeof(struct xx));

return 0;
}

执行结果如下:


代码如下:

&a = ffbff5ec
&b = ffbff5e8
&c = ffbff5f0
&d = ffbff5f4
sizeof(xx) = 16

会发现b与a之间空出了3个字节,也就是说在b之后的0xffbff5e9,0xffbff5ea,0xffbff5eb空了出来,a直接存储在了0xffbff5ec, 因为a的大小是4,只能存储在4个整数倍的位置上。打印xx的大小会发现,是16,有些人可能要问,b之后空出了3个字节,那也应该是13啊?其余的3个 呢?这个往后阅读本文会理解的更深入一点,这里简单说一下就是d后边的3个字节,也会浪费掉,也就是说,这3个字节也被这个结构体占用了.

可以简单的修改结构体的结构,来降低内存的使用,例如可以将结构体定义为:


代码如下:

struct xx{
        char b;
        char d;
        int a;         
        int c;                 
};

这样打印这个结构体的大小就是12,省了很多空间,可以看出,在定义结构体的时候,一定要考虑要内存对齐的影响,这样能使我们的程序占用更小的内存。

二.操作系统的默认对齐系数

每 个操作系统都有自己的默认内存对齐系数,如果是新版本的操作系统,默认对齐系数一般都是8,因为操作系统定义的最大类型存储单元就是8个字节,例如 long long(为什么一定要这样,在第三节会讲解),不存在超过8个字节的类型(例如int是4,char是1,long在32位编译时是4,64位编译时是 8)。当操作系统的默认对齐系数与第一节所讲的内存对齐的理论产生冲突时,以操作系统的对齐系数为基准。

例如:

假设操作系统的默认对齐系数是4,那么对与long long这个类型的变量就不满足第一节所说的,也就是说long long这种结构,可以存储在被4整除的位置上,也可以存储在被8整除的位置上。

可以通过#pragma pack()语句修改操作系统的默认对齐系数,编写程序的时候不建议修改默认对齐系数,在第三节会讲解原因

例2:


代码如下:

#include <stdio.h>
#pragma pack(4)
struct xx{
        char b;
        long long a;
        int c;
        char d;
};
#pragma pack()

int main()
{
        struct xx bb;
        printf("&a = %p/n", &bb.a);
        printf("&b = %p/n", &bb.b);
        printf("&c = %p/n", &bb.c);
        printf("&d = %p/n", &bb.d);
        printf("sizeof(xx) = %d/n", sizeof(struct xx));

return 0;
}

打印结果为:


代码如下:

&a = ffbff5e4
&b = ffbff5e0
&c = ffbff5ec
&d = ffbff5f0
sizeof(xx) = 20

发现占用8个字节的a,存储在了不能被8整除的位置上,存储在了被4整除的位置上,采取了操作系统的默认对齐系数。

三.内存对齐产生的原因

内存对齐是操作系统为了快速访问内存而采取的一种策略,简单来说,就是为了放置变量的二次访问。操作系统在访问内存 时,每次读取一定的长度(这个长度就是操作系统的默认对齐系数,或者是默认对齐系数的整数倍)。如果没有内存对齐时,为了读取一个变量是,会产生总线的二 次访问。

例如假设没有内存对齐,结构体xx的变量位置会出现如下情况:


代码如下:

struct xx{
        char b;         //0xffbff5e8
        int a;            //0xffbff5e9      
        int c;             //0xffbff5ed     
        char d;         //0xffbff5f1
};

操作系统先读取0xffbff5e8-0xffbff5ef的内存,然后在读取0xffbff5f0-0xffbff5f8的内存,为了获得值c,就需要将两组内存合并,进行整合,这样严重降低了内存的访问效率。(这就涉及到了老生常谈的问题,空间和效率哪个更重要?这里不做讨论)。

这样大家就能理解为什么结构体的第一个变量,不管类型如何,都是能被8整除的吧(因为访问内存是从8的整数倍开始的,为了增加读取的效率)!

内存对齐的问题主要存在于理解struct等复合结构在内存中的分布。

首先要明白内存对齐的概念。
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。

这个k在不同的cpu平台下,不同的编译器下表现也有所不同。比如32位字长的计算机与16位字长的计算机。这个离我们有些远了。我们的开发主要涉及两大平台,windows和linux(unix),涉及的编译器也主要是microsoft编译器(如cl),和gcc。

内存对齐的目的是使各个基本数据类型的首地址为对应k的倍数,这是理解内存对齐方式的终极法宝。另外还要区分编译器的分别。明白了这两点基本上就能搞定所有内存对齐方面的问题。

不同编译器中的k:
1、对于microsoft的编译器,每种基本类型的大小即为这个k。大体上char类型为8,int为32,long为32,double为64。
2、对于linux下的gcc编译器,规定大小小于等于2的,k值为其大小,大于等于4的为4。

明白了以上的说明对struct等复合结构的内存分布就应该很清楚了。

下面看一下最简单的一个类型:struct中成员都为基本数据类型,例如:


代码如下:

struct test1
{
char a;
short b;
int c;
long d;
double e;
};

在windows平台,microsoft编译器下:

假设从0地址开始,首先a的k值为1,它的首地址可以使任意位置,所以a占用第一个字节,即地址0;然后b的k值为2,他的首地址必须是2的倍数,不能是1,所以地址1那个字节被填充,b首地址为地址2,占用地址2,3;然后到c,c的k值为4,他的首地址为4的倍数,所以首地址为4,占用地址4,5,6,7;再然后到d,d的k值也为4,所以他的首地址为8,占用地址8,9,10,11。最后到e,他的k值为8,首地址为8的倍数,所以地址12,13,14,15被填充,他的首地址应为16,占用地址16-23。显然其大小为24。

这就是 test1在内存中的分布情况。我们建立一个test1类型的变量,a、b、c、d、e分别赋值2、4、8、16、32。然后从低地址依次打印出内存中每个字节对应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 0 0 0 0 40 40

验证:
显然推断是正确的。

在linux平台,gcc编译器下:
假设从0地址开始,首先a的k值为1,它的首地址可以使任意位置,所以a占用第一个字节,即地址0;然后b的k值为2,他的首地址必须是2的倍数,不能是1,所以地址1那个字节被填充,b首地址为地址2,占用地址2,3;然后到c,c的k值为4,他的首地址为4的倍数,所以首地址为4,占用地址4,5,6,7;再然后到d,d的k值也为4,所以他的首地址为8,占用地址8,9,10,11。最后到e,从这里开始与microsoft的编译器开始有所差异,他的k值为不是8,仍然是4,所以其首地址是12,占用地址12-19。显然其大小为20。

验证:
我们建立一个test1类型的变量,a、b、c、d、e分别赋值2、4、8、16、32。然后从低地址依次打印出内存中每个字节对应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 40 40

显然推断也是正确的。

接下来,看一看几类特殊的情况,为了避免麻烦,不再描述内存分布,只计算结构大小。

第一种:嵌套的结构


代码如下:

struct test2
{
char f;
struct test1 g;
};

在windows平台,microsoft编译器下:

这种情况下如果把test2的第二个成员拆开来,研究内存分布,那么可以知道,test2的成员f占用地址0,g.a占用地址1,以后的内存分布不变,仍然满足所有基本数据成员的首地址都为其对应k的倍数这一原则,那么test2的大小就还是24了。但是实际上test2的大小为32,这是因为:不能因为test2的结构而改变test1的内存分布情况,所以为了使test1种各个成员仍然满足对齐的要求,f成员后面需要填充一定数量的字节,不难发现,这个数量应为7个,才能保证test1的对齐。所以test2相对于test1来说增加了8个字节,所以test2的大小为32。

在linux平台,gcc编译器下:

同样,这种情况下如果把test2的第二个成员拆开来,研究内存分布,那么可以知道,test2的成员f占用地址0,g.a占用地址1,以后的内存分布不变,仍然满足所有基本数据成员的首地址都为其对应k的倍数这一原则,那么test2的大小就还是20了。但是实际上test2的大小为24,同样这是因为:不能因为test2的结构而改变test1的内存分布情况,所以为了使test1种各个成员仍然满足对齐的要求,f成员后面需要填充一定数量的字节,不难发现,这个数量应为3个,才能保证test1的对齐。所以test2相对于test1来说增加了4个字节,所以test2的大小为24。

第二种:位段对齐


代码如下:

struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};

或者


代码如下:

struct test3
{
unsigned int a:4;
int b:4;
char c;
};

在windows平台,microsoft编译器下:

相邻的多个同类型的数(带符号的与不带符号的,只要基本类型相同,也为相同的数),如果他们占用的位数不超过基本类型的大小,那么他们可作为一个整体来看待。不同类型的数要遵循各自的对齐方式。
如:test3中,a、b可作为一个整体,他们作为一个int型数据来看待,所以test3的大小为8字节。并且a与b的值在内存中从低位开始依次排列,位于4字节区域中的前0-3位和4-7位

如果test4位以下格式


代码如下:

struct test4
{
unsigned int a:30;
unsigned int b:4;
char c;
};

那么test4的大小就为12个字节,并且a与b的值分别分布在第一个4字节的前30位,和第二个4字节的前4位。

如过test5是以下形式


代码如下:

struct test5
{
unsigned int a:4;
unsigned char b:4;
char c;
};

那么由于int和char不同类型,他们分别以各自的方式对齐,所以test5的大小应为8字节,a与b的值分别位于第一个4字节的前4位和第5个字节的前4位。

在linux平台,gcc编译器下:


代码如下:

struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};

gcc下,相邻各成员,不管类型是否相同,占的位数之和超过这些成员中第一个的大小的时候,在结构中以k值为1对齐,在结构外k值为其基本类型的值。不超过的情况下在内存中依次排列。
如test3,其大小为4。a,b的值在内存中依次排列分别为第一个四字节中的0-3和4-7位。

如果test4位以下格式


代码如下:

struct test4
{
unsigned int a:20;
unsigned char b:4;
char c;
};

test4的大小为4个字节,并且a与b的值分别分布在第一个4字节的0-19位,和20-23位,c存放在第4个字节中。
如过test5是以下形式


代码如下:

struct test5
{
unsigned int a:10;
unsigned char b:4;
short c;
};

那么test5的大小应为4字节,a,b的值为0-9位和10-13位。c存放在后两个字节中。如果a的大小变成了20
那么test5的大小应为8字节。即


代码如下:

struct test6
{
unsigned int a:20;
unsigned char b:4;
short c;
};

此时,test6的a、b共占用0,1,2共3字节,c的k值为2,其实可以4位首位置,但是在结构外,a要以int的方式对齐。也就是说连续两个test6对象在内存中存放的话,a的首位置要保证为4的倍数,那么c后面必须多填充2位。所以test6的大小为8个字节。

关于位段结构的部分是比较复杂的。暂时我就知道这么多。

(0)

相关推荐

  • 浅析c语言中的内存

    1.栈(stack):存局部变量.函数,调用函数时会开辟栈区,函数结束时就自动回收,遵循后进先出的原则,从高地址向低地址增长. 2.堆(heap):malloc.realloc.calloc等开辟的内存就在堆,从低地址向高地址增长,由程序员分配和释放,系统不自动回收,所以一定要记得申请了就要释放,以免溢出. 3.数据段(初始化数据段)(data):存放初始化的全局变量.static修饰的已初始化的变量. 4.未初始化数据段(bss段):存放未初始化的全局变量和static修饰的未初始化的变量.

  • 深入解析C语言中的内存分配相关问题

    C内存分配区域 程序代码区 存放函数体的二进制代码 全局数据区 全局变量和静态变量的存储是放在一起的.初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域.常量数据存放在另一个区域里.这些数据在程序结束后由系统释放.我们所说的BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域.BSS是英文Block Started by Symbol的简称 栈区 由编译器自动分配释放,存放函数的参数值,局部变量的值等.其操作方式类似

  • 深入探讨C语言中局部变量与全局变量在内存中的存放位置

    C语言中局部变量和全局变量变量的存储类别(static,extern,auto,register) 1.局部变量和全局变量在讨论函数的形参变量时曾经提到,形参变量只在被调用期间才分配内存单元,调用结束立即释放.这一点表明形参变量只有在函数内才是有效的,离开该函数就不能再使用了.这种变量有效性的范围称变量的作用域.不仅对于形参变量,C语言中所有的量都有自己的作用域.变量说明的方式不同,其作用域也不同.C语言中的变量,按作用域范围可分为两种,即局部变量和全局变量.1.1局部变量局部变量也称为内部变量

  • 深入C语言内存区域分配(进程的各个段)详解

    C语言可执行代码结构 名称 内容 代码段  可执行代码.字符串常量 数据段  已初始化全局变量.已初始化全局静态变量.局部静态变量.常量数据 BSS段  未初始化全局变量,未初始化全局静态变量 栈  局部变量.函数参数 堆  动态内存分配 (1)代码段(text segment):存放CPU执行的机器指令.通常代码段是可共享的,这使得需要频繁被执行的程序只需要在内存中拥有一份拷贝即可.代码段也通常是只读的,这样可以防止其他程序意外地修改其指令.另外,代码段还规划了局部数据所申请的内存空间信息.

  • C语言中变量与其内存地址对应的入门知识简单讲解

    先来理解理解内存空间吧.请看下图: 如上图所示,内存只不过是一个存放数据的空间,就好像我的看电影时的电影院中的座位一样.电影院中的每个座位都要编号,而我们的内存要存放各种各样的数据,当然我们要知道我们的这些数据存放在什么位置吧.所以内存也要象座位一样进行编号了,这就是我们所说的内存编址.座位可以是遵循"一个座位对应一个号码"的原则,从"第1号"开始编号.而内存则是按一个字节接着一个字节的次序进行编址,如上图所示.每个字节都有个编号,我们称之为内存地址.好了,我说了这

  • c语言内存泄露示例解析

    正确的内存管理的重要性存在内存错误的 C 和 C++ 程序会导致各种问题.如果它们泄漏内存,则运行速度会逐渐变慢,并最终停止运行:如果覆盖内存,则会变得非常脆弱,很容易受到恶意用户的攻击.从 1988 年著名的莫里斯蠕虫 攻击到有关 Flash Player 和其他关键的零售级程序的最新安全警报都与缓冲区溢出有关:"大多数计算机安全漏洞都是缓冲区溢出",Rodney Bates 在 2004 年写道. 在可以使用 C 或 C++ 的地方,也广泛支持使用其他许多通用语言(如 Java™.

  • C语言中多维数组的内存分配和释放(malloc与free)的方法

    如果要给二维数组(m*n)分配空间,代码可以写成下面: 复制代码 代码如下: char **a, i; // 先分配m个指针单元,注意是指针单元 // 所以每个单元的大小是sizeof(char *) a = (char **) malloc(m * sizeof(char * )); // 再分配n个字符单元, // 上面的m个指针单元指向这n个字符单元首地址 for(i = 0; i < m; i++) a[i] = (char * )malloc(n * sizeof(char )); 释

  • C语言中的内存泄露 怎样避免与检测

    有些程序并不需要管理它们的动态内存的使用.当需要内存时,它们简单地通过分配来获得,从来不用担心如何释放它.这类程序包括编译器和其他一些运行一段固定的(或有限的)时间然后终止的程序.当这种类型的程序终止时,所有内存会被自动回收.细心查验每块内存是否需要回收纯属浪费时间,因为它们不会再被使用. 其他程序的生存时间要长一点.有些工具如日历管理器.邮件工具以及操作系统本事经常需要数日及至数周连续运行,并需要管理动态内存的分配和回收.由于C语言通常并不使用垃圾回收器(自动确认并回收不再使用的内存块),这些

  • 浅析C语言中的内存布局

    本节注重分清几个概念:.text .data .bss   堆   栈    静态存储区    只读存储区等 从程序到a.out 把程序变成.text  .data  .bss  是编译原理完成的过程 从a.out把程序映射到对应的内存地址空间是操作系统完成的,也就是在操作系统创建进程的时候完成的,在描述进程的那个结构体中. 我们常说的堆是为了申请动态内存的时候使用的,malloc. 栈是为了在函数中切换使用的,即存放函数中的局部变量.(堆和栈是操作系统分配的,所有不在a.out中) 静态存储区

  • 深入理解C语言内存对齐

    一.内存对齐的初步讲解 内存对齐可以用一句话来概括: "数据项只能存储在地址是数据项大小的整数倍的内存位置上" 例如int类型占用4个字节,地址只能在0,4,8等位置上. 例1: 复制代码 代码如下: #include <stdio.h>struct xx{        char b;        int a;        int c;        char d;}; int main(){        struct xx bb;        printf(&q

  • C语言内存对齐实例详解

    本文详细讲述了C语言程序设计中内存对其的概念与用法.分享给大家供大家参考之用.具体如下: 一.字节对齐基本概念 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐. 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同.一些平台对某些特定类型的数据只能从某些特定地址开始存取.比如有些架构的C

  • 深入理解c/c++ 内存对齐

    内存对齐,memory alignment.为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐.原因在于,为了访问未对齐的内存,处理器需要作两次内存访问:然而,对齐的内存访问仅需要一次访问.内存对齐一般讲就是cpu access memory的效率(提高运行速度)和准确性(在一些条件下,如果没有对齐会导致数据不同步现象).依赖cpu,平台和编译器的不同.一些cpu要求较高(这句话说的不准确,但是确实依赖cpu的不同),而有些平台已经优化内存对齐问题,不同编译器的对齐模数不同.总

  • 一篇文章带你了解C语言内存对齐

    目录 内存对齐 三.在内存对齐话题下的sizeof与offsetof宏 3.1.sizeof 3.2.offsetof宏 3.3.Debug 总结 内存对齐 先看如下代码: 结构体Test1占用了多少字节?如果事先不知道内存对齐的话,答案肯定是:1个字节(char)+ 4个字节(int)+ 1个字节(char) = 6个字节. 事实上,Test1结构体占用了12个字节,从DEBUG模式下Watch1观察: OK,不就猜少了6个字节吗?有什么影响吗?先不说影响吧,咱们先来看看单片机内存里的实际情况

  • 一篇文章带你了解C语言内存对齐解决的问题

    目录 一.内存对齐为4个字节的好处 二.内存对齐的目的是以空间换取速度 2.1.内存对齐为4的例子 2.2.内存没有使用内存对齐的例子 CPU读取数据的过程: 三.掌握内存对齐的必要性 总结 一.内存对齐为4个字节的好处 首先,了解一下CPU从内存里读取数据的流程: 第一步,CPU通过地址总线,找到该数据的位置. 第二步,通过控制总线,发送读取数据的指令. 第三步,通过数据总线,从内存里获取该数据的内容. 内存对齐使用4个字节的原因有: 1.STM32单片机的数据总线与地址总线都是32bit(4

  • 一篇文章带你了解C语言内存对齐公式

    目录 一.前言 二.公式 2.1.例子一 2.2.例子二 2.3.例子三 总结 一.前言 每一个特定平台上的编译器都有自己的默认"对齐系数"(也叫对齐模数).GCC中默认#program pack(4),即4个字节的内存对齐.Keil也是采用4字节对齐的.也可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数,一般情况下尽量使用自然对齐系数,不要修改它. STM32单片机上各个变量占用的字节数: 二.公式 公式一.结构体变量里,成员的起始地址必须

  • C语言结构体中内存对齐的问题理解

    目录 前言 思考 结构体在内存中开辟空间时内存对齐的规则 为什么存在内存对齐 1.平台的原因 2.性能的原因 前言 学C的同学应该知道~ 想精通C语言就不得不面对—指针与内存 续上次指针的进阶,这一章我来聊一聊C语言内存对齐的问题 学习结构体的你有没有注意过结构体向系统申请的内存为多少呢的 思考 #include<stdio.h> typedef struct s1 { char a; char b; int c; }s1; typedef struct s2 { char a; int c;

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

    这也可以? 复制代码 代码如下: #include <iostream> 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() {

  • C语言重难点之内存对齐和位段

    一:结构体内存对齐 (1)为什么要存在内存对齐 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的:某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常. 比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据. 性能原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐.原因在于,为了访问未对齐内存,处理器需要作两次内存访问:而对齐的内存访问仅需一次. 核心思想就是:以空间换取时间 举个例子:对于有

  • C语言 详细分析结构体的内存对齐

    目录 一.结构体 二.结构体内存对齐 1.非嵌套结构体的大小 2.含嵌套结构体的大小 三.为什么要内存对齐 1.平台原因(移植原因) 2.性能原因 一.结构体 结构体 (struct)是一种数据结构,可以包含很多数据类型,可以实现比较复杂的数据结构. 常见的int,char类型变量,我们可以一眼看出占多少字节,但对于结构体,可就有点难度了. 让我们来猜猜以下程序的输出 struct S1 { char c1; int i; char c2; }; struct S2 { char c1; cha

随机推荐