
1Win32的堆分配函数
每一个进程都可以使用堆分配函数创建一个私有的堆──调用进程地址空间的一个或者多个页面。DLL创建的私有堆必定在调用DLL的进程的地址空间内,只能被调用进程访问。
HeapCreate用来创建堆;HeapAlloc用来从堆中分配一定数量的空间,HeapAlloc分配的内存是不能移动的;HeapSize可以确定从堆中分配的空间的大小;HeapFree用来释放从堆中分配的空间;HeapDestroy销毁创建的堆。
2Windows传统的全局或者局部内存分配函数
由于Win32采用平面内存结构模式,Win32下的全局和局部内存函数除了名字不同外,其他完全相同。任一函数都可以用来分配任意大小的内存(仅仅受可用物理内存的限制)。用法可以和Win16下基本一样。
Win32下保留这类函数保证了和Win16的兼容,所以不建议再使用了。
3C语言的标准内存分配函数
C语言的标准内存分配函数包括以下函数:。
malloc,calloc,realloc,free,等。
这些函数最后都映射成堆API函数,所以,malloc分配的内存是不能移动的。这些函数的调式版本为malloc_dbg,calloc_dbg,realloc_dbg,free_dbg,等。
4Win32的虚拟内存分配函数
虚拟内存API是其他API的基础。new->malloc->HeapAlloc->VirtualAlloc->驱动程序的_PageAlloc。
虚拟内存API以页为最小分配单位,X86上页长度为4KB,可以用GetSystemInfo函数提取页长度。
虚拟内存分配函数包括以下函数:
LPVOID VirtualAlloc(
LPVOID lpvAddress,
DWORD cbSize,
DWORD fdwAllocationType,
DWORD fdwProtect);
该函数用来分配一定范围的虚拟页。
参数1指定起始地址;
参数2指定分配内存的长度;
参数3指定分配方式,取值MEM_COMMINT或者MEM_RESERVE;
参数4指定控制访问本次分配的内存的标识,取值为PAGE_READONLY、PAGE_READWRITE或者PAGE_NOACCESS。
LPVOID VirtualAllocEx(
HANDLE process,
LPVOID lpvAddress,
DWORD cbSize,
DWORD fdwAllocationType,
DWORD fdwProtect);
该函数功能类似于VirtualAlloc,但是允许指定进程process。VirtaulFree、VirtualProtect、VirtualQuery都有对应的扩展函数。
BOOL VirtualFree(
LPVOID lpvAddress,
DWORD dwSize,
DWORD dwFreeType);
该函数用来回收或者释放分配的虚拟内存。
参数1指定希望回收或者释放内存的基地址;
如果是回收,参数2可以指向虚 拟地址范围内的任何地方,如果是释放,参数2必须是VirtualAlloc返回的地址;
参数3指定是否释放或者回收内存,取值为MEM_DECOMMINT或者MEM_RELEASE。
BOOL VirtualProtect(
LPVOID lpvAddress,
DWORD cbSize,
DWORD fdwNewProtect,
PDWORD pfdwOldProtect);
该函数用来把已经分配的页改变成保护页。
参数1指定分配页的基地址;
参数2指定保护页的长度;
参数3指定页的保护属性,取值PAGE_READ、PAGE_WRITE、PAGE_READWRITE等等;
参数4用来返回原来的保护属性。
DWORD VirtualQuery(
LPCVOID lpAddress,
PMEMORY_BASIC_INFORMATION lpBuffer,
DWORD dwLength
);
该函数用来查询内存中指定页的特性。
参数1指向希望查询的虚拟地址;
参数2是指向内存基本信息结构的指针;
参数3指定查询的长度。
BOOL VirtualLock(
LPVOID lpAddress,
DWORD dwSize);
该函数用来锁定内存,锁定的内存页不能交换到页文件。
参数1指定要锁定内存的起始地址;
参数2指定锁定的长度。
BOOL VirtualUnLock(
LPVOID lpAddress,
DWORD dwSize);
参数1指定要解锁的内存的起始地址;
参数2指定要解锁的内存的长度。
摘自 落日小屋
一个程序内存分配:
下图是APUE中的一个典型C内存空间分布图(虚拟内存)
例如:
int g1=0, g2=0, g3=0;
int max(int i)
{
int m1=0,m2,m3=0, p_max;
static n1_max=0,n2_max,n3_max=0;
p_max = (int )malloc(10);
printf("打印max程序地址\n");
printf("in max: 0xx\n\n",max);
printf("打印max传入参数地址\n");
printf("in max: 0xx\n\n",&i);
printf("打印max函数中静态变量地址\n");
printf("0xx\n",&n1_max); //打印各本地变量的内存地址
printf("0xx\n",&n2_max);
printf("0xx\n\n",&n3_max);
printf("打印max函数中局部变量地址\n");
printf("0xx\n",&m1); //打印各本地变量的内存地址
printf("0xx\n",&m2);
printf("0xx\n\n",&m3);
printf("打印max函数中malloc分配地址\n");
printf("0xx\n\n",p_max); //打印各本地变量的内存地址
if(i) return 1;
else return 0;
}
int main(int argc, char argv)
{
static int s1=0, s2, s3=0;
int v1=0, v2, v3=0;
int p;
p = (int )malloc(10);
printf("打印各全局变量(已初始化)的内存地址\n");
printf("0xx\n",&g1); //打印各全局变量的内存地址
printf("0xx\n",&g2);
printf("0xx\n\n",&g3);
printf("======================\n");
printf("打印程序初始程序main地址\n");
printf("main: 0xx\n\n", main);
printf("打印主参地址\n");
printf("argv: 0xx\n\n",argv);
printf("打印各静态变量的内存地址\n");
printf("0xx\n",&s1); //打印各静态变量的内存地址
printf("0xx\n",&s2);
printf("0xx\n\n",&s3);
printf("打印各局部变量的内存地址\n");
printf("0xx\n",&v1); //打印各本地变量的内存地址
printf("0xx\n",&v2);
printf("0xx\n\n",&v3);
printf("打印malloc分配的堆地址\n");
printf("malloc: 0xx\n\n",p);
printf("======================\n");
max(v1);
printf("======================\n");
printf("打印子函数起始地址\n");
printf("max: 0xx\n\n",max);
return 0;
}
打印结果:
ELF目标文件格式的最前端是 ELF文件头(ELF Header) ,
包含了描述整个文件的基本属性,如ELF版本、目标机器型号、 程序入口地址 等
3 加载:
我认为是这样的,当创建一个Dog对象d时,Dog d = new Dog("Kitty",21);从数据结构上看d是个引用变量被存放在栈内存当中,而其指向一块堆内存中的数据,也就是对象Dog。d中存放的是这个新的Dog对象的地址的引用。实际上new Dog("Kitty",21)就是一块堆内存中的数据块,用d来引用。当new出多个Dog对象时,系统就会为不同的对象的成员变量分配不同的存储空间。
在win32下,堆和栈都是每个程序独立的。
堆的分配的地址是由低到高的,栈的分配的地址是由高往低的。
栈一般是给函数的参数或局部变量使用的
堆一般是动态分配的内存块
由 *** 作系统或系统管理员预先将内存划分成若干个分区。在系统运行过程中,分区的边界不再改变。分配时,找一个空闲且足够大的分区。如没有合适的分区:①让申请者等待。②先换出某分区的内容,再将其分配出去。
为申请者分配指定的分区或任选一个分区。如果没有空闲分区,可将一个分区的内容换出。可能需要重定位。
会出现内部碎片,无法满足大内存的需求。
可减少内部碎片。减少对大内存需求的限制。
①固定分配:只分配某种尺寸的特定分区,如分区已被使用,申请者必须等待。
可能出现不公平等待:虽有更大尺寸的空闲分区,却必须等待。
②最佳适应分配:分配能满足需要的最小尺寸的空闲分区,只有当所有分区都已用完时,申请者才需要等待。灵活,但可能产生较大的内部碎片。
3、静态分区:内存利用率低,产生内部碎片;尺寸和分区数量难以确定。
1、不预先确定分区的大小和数量,将分区工作推迟到实际分配内存时进行。 Lazy
初始情况下,把所有的空闲内存看成一个大分区。分配时,按申请的尺寸,找一块足够大的空闲内存分区,临时从中划出一块构成新分区。新分区的尺寸与申请的大小相等,不会出现内部碎片。回收时,尽可能与邻近的空闲分区合并。在内存紧缺时,可将某个选定的分区换出。
2、会产生外部碎片,如下图(内部碎片是指 eg:要 1M,分了 8M,产生 7M 的碎片):
移动内存中的进程,将碎片集中起来,重新构成大的内存块。需要运行时的动态重定位,费时。
(1)紧缩方向:向一头紧缩,向两头紧缩。
(2)紧缩时机:①在释放分区时,如果不能与空闲分区合并,则立刻进行紧缩。
好处是不存在外部碎片,坏处是费时。
②在内存分配时,如果剩余的空闲空间总量能满足要求但没有一个独立的空闲块能满足要求,则进行紧缩。
好处是减少紧缩次数。Lazy。
①最先适应算法(First fit):从头开始,在满足要求的第一个空闲块中分配。
分区集中在内存的前部,大内存留在后面,便于释放后的合并。
②最佳适应算法(Best fit):遍历空闲块,在满足要求的最小空闲块中分配。
留下的碎片最小,基本无法再用,需要更频繁地紧缩。
③下一个适应算法(Next fit):从上次分配的位置开始,在满足要求的下一个空闲块中分配。
对内存的使用较平均,不容易留下大的空闲块。
④最差适应算法(Worst Fit):遍历空闲块,在满足要求的最大空闲块中分配。
留下的碎片较大,但不会剩余大内存。
最先适应算法较优,最佳适应算法较差。
伙伴算法:将动态分区的大小限定为 2^k 字节,分割方式限定为平分,分区就会变得较为规整,分割与合并会更容易,可以减少一些外部碎片。平分后的两块互称伙伴。
1、
分配时可能要多次平分,释放时可能要多次合并。举例:
记录大小不同的空闲页:
示意图:
2、
伙伴算法是静态分区和动态分区法的折中,比静态分区法灵活,不受分区尺寸及个数的限制;比动态分区法规范,不易出现外部碎片。会产生内部碎片,但比静态分区的小。
Linux、Windows、Ucore等都采用伙伴算法管理物理内存。
一般情况下,将最小尺寸定为 2^12 字节(1页,4K=4096B),最大尺寸定为1024页,11个队列。
Linux、Windows、Ucore 等都将伙伴的最小尺寸限定为1页。
ucore 用 page,在内存初始化函数 page_init 中为系统中的每个物理页建立一个 page 结构。
页块(pageblock)是一组连续的物理页。
5、
(1)判断伙伴关系/寻找伙伴
最后两行是指,B1和B2只有第i位相反。
(2)判断伙伴是否空闲:
ucore 用 free_area[ ]数组定义空闲页块队列。
(3)确定伙伴是否在 order 队列中:
7、
(1)解决内部碎片过大问题(eg:申请5页,分配8页,浪费3页):
ucore 在前部留下需要的页数,释放掉尾部各页。每次释放1页,先划分成页块,再逐个释放。
(2) 解决切分与合并过于频繁的问题:
用得较多的是单个页。位于处理器Cache中页称为热页(hot page),其余页称为冷页(cold page)。处理器对热页的访问速度要快于冷页。
可建一个热页队列(per_cpu_page),暂存刚释放的单个物理页,将合并工作向后推迟 Lazy。总是试图从热页队列中分配单个物理页。分配与释放都在热页队列的队头进行。
(3)解决内存碎化(有足够多的空闲页,但是没有大页块)问题:①将页块从一个物理位置移动到另一个物理位置,并保持移动前后逻辑地址不变(拷贝页块内容);②逻辑内存管理器。
(4)满足大内存的需求:
(5)物理内存空间都耗尽的情况:
在任何情况下,都应该预留一部分空闲的物理内存以备急需。定义两条基准线low和high,当空闲内存量小于low时,应立刻开始回收物理内存,直到空闲内存量大于high。
(6)回收物理内存:
法一:启动一个守护进程,专门用于回收物理内存。周期性启动,也可被唤醒。
法二:申请者自己去回收内存。实际是由内存分配程序回收。回收的方法很多,如释放缓冲区、页面淘汰等。
1、
伙伴算法最小分配内存为页,对于更小的内存的管理 --> Slab 算法
内和运行过程中经常使用小内存(小于1页)eg:建立数据结构、缓冲区
内核对小内存的使用极为频繁、种类繁多、时机和数量难以预估。所以难以预先分配,只能动态地创建和撤销
2、
Slab 向伙伴算法申请大页块(批发),将其划分成小对象分配出去(零售);将回收的小对象组合成大页块后还给伙伴算法。
Slab 采用等尺寸静态分区法,将页块预先划分成一组大小相等的小块,称为内存对象。
具有相同属性的多个Slab构成一个Cache,一个Cache管理一种类型(一类应该是指一个大小)的内存对象。当需要小内存时,从预建的Cache中申请内存对象,用完之后再将其还给Cache。当Cache中缺少对象时,追加新的Slab;当物理内存紧缺时,回收完全空闲的Slab。
Slab 算法的管理结构:
① Cache 管理结构:管理Slab,包括Slab的创建和销毁。
② Slab 管理结构:管理内存对象,包括小对象的分配与释放。
(Cache结构和Slab结构合作,共同实现内存对象的管理)
3、
(1)描述各个内存对象的使用情况
可以用位图标识空闲的内存对象。也可以将一个Slab中的空闲内存对象组织成队列,并在slab结构中记录队列的队头。
早期的Linux在每个内存对象的尾部都加入一个指针,用该指针将空闲的内存对象串联成一个真正的队列。(对象变长、不规范,空间浪费)
改进:将指针集中在一个数组中,用数组内部的链表模拟内存对象队列。
再改进:将数组中的指针换成对象序号,利用序号将空闲的内存对象串成队列。序号数组是动态创建的。
序号数组可以位于 Slab 内部,也可以位于 Slab 外部
(2)一个Cache会管理多个Slab,可以将所有Slab放在一个队列中。
Ucore为每个Cache准备了两个slab结构队列:全满的和不满的。Linux为每个Cache准备了三个slab结构队列:部分满的、完全满的和完全空闲的。
Linux允许动态创建Cache,Ucore不许。Ucore预定了对象大小,分别是32、64、128、256、512、1K、2K(4K、8K、16K、32K、64K、128K)。为每一种大小的对象预建了Cache。
(3)Slab是动态创建的,当Cache中没有空闲的内存对象时,即为其创建一个新的Slab。
Slab所需要的内存来自伙伴算法,大小是 2^page_order 个连续页。
4、小对象的尺寸
如按处理器一级缓存中缓存行(Cache Line)的大小(16、32字节)取齐,可使对象的开始位置都位于缓存行的边界处。
在将页块划分成内存对象的过程中,通常会剩余一小部分空间,位于所有内存对象之外,称为外部碎片。
Slab算法选用碎片最小的实现方案。
5、
(1)对象分配 kmalloc
① 根据size确定一个Cache。
② 如果Cache的slabs_notfull为空,则为其创建一个新的Slab。
③ 选中slabs_notfull中第一个Slab,将队头的小对象分配出去,并调整队列。
④ 对象的开始地址是:objp = slabp->s_mem + slabp->free cachep->objsize;
(2)对象释放 kfree
① 算出对象所在的页号,找到它的 Page 结构。
② 根据 Page 找到所属的 Cache 和 Slab。
③ 算出对象序号:objnr = (objp - slabp->s_mem) / cachep->objsize;
④将序号插入Slab的free队列。
⑤整Slab所属队列。
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。 当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存 *** 作。否则,我们就可能需要两次内存 *** 作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。
Win32平台下的微软C编译器(clexe for 80x86)在默认情况下采用如下的对齐规则:任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如Long和double)都以4为对齐模数。
现在回到我们关心的struct上来。ANSIC规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。填充区就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。那么结构体本身也有对齐要求,ANSIC标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格(但此非强制要求,VC71就仅仅是让它们一样严格)。我们来看一个例子(以下所有试验的环境是Intel Celeron 24G + WIN2000 PRO +vc71,内存对齐编译选项是"默认",即不指定/Zp与/pack选项):
typedef struct ms1 { char a; int b; } MS1;
MS1中有最强对齐要求的是b字段(int),所以根据编译器的对齐规则以及ANSIC标准,该结构体的内存布局图如下:
这个方案在a与b之间多分配了3个填充(padding)字节,这样当整个struct对象首地址满足4字节的对齐要求时,b字段也一定能满足int型的4字节对齐规定。那么sizeof(MS1)显然就应该是8,而b字段相对于结构体首地址的偏移就是4。非常好理解,对吗?现在我们把MS1中的字段交换一下顺序:
typedef struct ms2 { int a; char b; } MS2;
或许你认为MS2比MS1的情况要简单,它的布局应该就是 因为MS2对象同样要满足4字节对齐规定,而此时a的地址与结构体的首地址相等,所以它一定也是4字节对齐。可是却不全面。让我们来考虑一下定义一个MS2类型的数组会出现什么问题。C标准保证,任何类型(包括自定义结构类型)的数组所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话说,数组各元素之间不会有空隙。按照上面的方案,一个MS2数组array的布局就是:当数组首地址是4字节对齐时,array[1]a也是4字节对齐,可是array[2]a呢?array[3]a 呢?可见这种方案在定义结构体数组时无法让数组中所有元素的字段都满足对齐规定,必须修改成如下形式:
现在无论是定义一个单独的MS2变量还是MS2数组,均能保证所有元素的所有字段都满足对齐规定。那么sizeof(MS2)仍然是8,而a的偏移为0,b的偏移是4。尝试分析一个稍微复杂点的类型。 typedef struct ms3 { char a; short b; double c; } MS3; 我想你一定能得出如下正确的布局图: sizeof(short)等于2,b字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double)等于8,c字段要从8倍数地址开始,前面的a、b字段加上填充字节已经有4 bytes,所以b后面再填充4个字节就可以保证c字段的对齐要求了。sizeof(MS3)等于16,b的偏移是2,c的偏移是8。接着看看结构体中字段还是结构类型的情况: typedef struct ms4 { char a; MS3 b; } MS4; MS3中内存要求最严格的字段是c,那么MS3类型数据的对齐模数就与double的一致(为8),a字段后面应填充7个字节,因此MS4的布局应该是: 显然,sizeof(MS4)等于24,b的偏移等于8。
在实际开发中,我们可以通过指定/Zp编译选项或者在代码中用#pragma pack指令来更改编译器的对齐规则。比如指定/Zpn(VC71中n可以是1、2、4、8、16)就是告诉编译器最大对齐模数是n。或者定义结构时
#pragma pack(push, n)
typedef struct ms3 { char a; short b; double c; } MS3;
#pragma pack(pop);
在这种情况下,所有小于等于n字节的基本数据类型的对齐规则与默认的一样,但是大于n个字节的数据类型的对齐模数被限制为n。如果n = 1,那么结构体的大小就是各个字段的大小之和,在Moses中定义结构体的地方随处可见,这样做可以减少结构体所占用的内存空间。
VC71的默认对齐选项就相当于/Zp8。仔细看看MSDN对这个选项的描述,会发现它郑重告诫了程序员不要在MIPS和Alpha平台上用/Zp1和/Zp2选项,也不要在16位平台上指定/Zp4和/Zp8(想想为什么?)。 结构体的内存布局依赖于CPU、 *** 作系统、编译器及编译时的对齐选项,而你的程序可能需要运行在多种平台上,你的源代码可能要被不同的人用不同的编译器编译(试想你为别人提供一个开放源码的库),那么除非绝对必需,否则你的程序永远也不要依赖这些诡异的内存布局。顺便说一下,如果一个程序中的两个模块是用不同的对齐选项分别编译的,那么它很可能会产生一些非常微妙的错误。如果你的程序确实有很难理解的行为,不防仔细检查一下各个模块的编译选项。
---------------------------------------------------------------------------------
问题:下面的试验,请问如何解释?
#pragma pack(push, 2)
struct s
{
char a;
};
#pragma pack (pop)
void TestPack()
{
s c[2];
assert(sizeof(s)==1);
assert(sizeof(c)==2);
}
int _tmain(int argc, _TCHAR argv[])
{
TestPack();
return 0;
}
解答:
每一种基本的数据类型都有该数据类型的对齐模数(alignment modulus)。Win32平台下的微软C编译器(clexe for 80x86)在默认
情况下: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。
一组可能的对齐模数数据如下:
数据类型 模数
------------------
char 1
shor 2
int 4
double 8
ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。
注:填充区就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。
产生填充区的条件:
当结构体中的成员一种类型S的对齐模数与另一种类型T的对齐模数不一致的时候,才可能产生填充区。
我们通过编译选项设置/zpn 或#pragma pack(push, n) 来设置内存对齐模数时,当结构体中的某中基本数据类型的对齐模数大于n时才会影响填充区的大小,否则将会按照基本数据类型的对齐模数进行对齐。
例子:
当n = 1时:
#pragma pack(push,1)
typedef struct ms3{char a; short b; double c; } MS3;
#pragma pack(pop)
这时n=1,此结构中基本数据类型short的对齐模数为2,double为8,大于n 所以将会影响这两个变量存储时地址的偏移量,必须是n的整数倍,而char的对齐模数是1,小于等于n,将会按照其自身的对齐模数1进行对齐
因为n=1,所以这三个变量在内存中是连续的而不存在填充区内存布局如下:
___________________________
| a | b | c |
+-------------------------+
Bytes: 1 2 8
sizeof(MS3) = 11
当n = 2时:
#pragma pack(push,2)
typedef struct ms3{char a; short b; double c; } MS3;
#pragma pack(pop)
这时n=2,此结构中基本数据类型double的对齐模数为8,大于n, 所以将会影响这个变量存储时地址的偏移量,必须是n的整数倍,而char 和 short 的对齐模数小于等于n, 将会按照其自身的对齐模数分别是1,2进行对齐内存布局如下:
____________________________
| a |\| b | c |
+---------------------------+
Bytes: 1 1 2 8
此时变量c的存储地址偏移是4,是n=2的整数倍,当然偏移为6,8等等时也满足这个条件,但编译器不至于愚蠢到这种地步白白浪费空间,呵。
sizeof(MS3) = 12
当n = 4时:与n=2时结果是一样的
当n = 8时:
#pragma pack(push,8)
typedef struct ms3{char a; short b; double c; } MS3;
#pragma pack(pop)
这时n=8,此结构中char ,short ,double的对齐模数为都,小于等于n,将会按照其自身的对齐模数分别是1,2,8进行对齐即:short变量存储时地址的偏移量是2的倍数;double变量存储时地址的偏移量是8的倍数
内存布局如下:
_______________________________________
| a |\| b |\padding\| c |
+-------------------------------------+
Bytes: 1 1 2 4 8
此时变量a的存储地址偏移是0,当然也是char型对齐模数1的整数倍了
变量b的存储地址偏移要想是short型对齐模数2的整数倍,因为前面a占了1 个byte ,所以至少在a 与b之间再加上1 个byte的padding才能满足条件。
变量c的存储地址骗移要想是double型对齐模数8的整数倍,因为前面a 和b 加 1个byte 的padding,共4 bytes所以最少还需要4 bytes的padding才能满足条件。
sizeof(MS3) = 16
当n = 16时:与n=8时结果是一样的
====================================================
根据上面的分析,如下定义的结构
#pragma pack(push, 2)
struct s
{
char a;
};
#pragma pack (pop)
因为char 的对齐模数是1,小于n=2,所以将按照自身的对齐模数对齐。根本就不会存在填充区,所以sizeof(s) = 1对于s c[2]; sizeof(c)==2 也是必然的。
再看下面的结构:
#pragma pack(push, n)//n=(1,2,4,8,16)
struct s
{
double a;
double b;
double c;
};
#pragma pack (pop)
对于这样的结构无论pack设置的对齐模数为几都不会影响其大小,即无padding
double 类型的对齐模数为8
当n<8时,虽然满足前面讲的规则:当结构体中的某中基本数据类型的对齐模数大于n时才会影响填充区的大小。但这个时候无论n等于几(1,2,4),double 变量存储时地址的偏移量都是n的整数倍,所以根本不需要填充区。当n>=8时,自然就按照double的对齐模数进行对齐了因为类型都一样所以变量之间在内存中不会存在填充区
---------------------------------------------------------------------------------------------------------------------
补充一点:
如果在定义结构的时候,仔细调整顺序,适当明确填充方式,则最终内存结果可以与编译选项/Zpn 或 pack无关。
举个例子:
typedef struct ms1{ char a; char b; int c; short d; } MS1;
在不同的 /Zpn下,sizeof(MS1)的长度可能不同,也就是内存布局不同。
如果改成
typedef struct ms2{ char a; char b; short d; int c; } MS2;
即便在不同的/Zpn或pack方式下,编译生成的内存布局总是相同的;
再比如:
typedef struct ms3{ char a; char b; int c; } MS3;
可以改写成:
typedef struct ms4{ char a; char b; short padding; int c; } MS4; 显式地写上 padding
(通过源代码本身来消除隐患,要比依赖编译选项更加可靠,并易于移植,优质的代码应该做到这一点)
(减少隐含padding的另外一个好处是少占内存,当结构的实例数量很大时,内存的节省量是非常可观的)
(以上的变量/结构命名没有遵循命名规范,只为说明用,不可模仿)
堆和栈的区别(内存和数据结构)
在计算机领域,堆栈是一个不容忽视的概念,我们编写的C语言程序基本上都要用到。但对于很多的初学着来说,堆栈是一个很模糊的概念。堆栈:一种数据结构、一个在程序运行时用于存放的地方,这可能是很多初学者的认识,因为我曾经就是这么想的和汇编语言中的堆栈一词混为一谈。我身边的一些编程的朋友以及在网上看帖遇到的朋友中有好多也说不清堆栈,所以我想有必要给大家分享一下我对堆栈的看法,有说的不对的地方请朋友们不吝赐教,这对于大家学习会有很大帮助。
数据结构的栈和堆
首先在数据结构上要知道堆栈,尽管我们这么称呼它,但实际上堆栈是两种数据结构:堆和栈。
堆和栈都是一种数据项按序排列的数据结构。
栈就像装数据的桶或箱子
我们先从大家比较熟悉的栈说起吧,它是一种具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。这就如同我们要取出放在箱子里面底下的东西(放入的比较早的物体),我们首先要移开压在它上面的物体(放入的比较晚的物体)。
堆像一棵倒过来的树
而堆就不同了,堆是一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
内存分配中的栈和堆
然而我要说的重点并不在这,我要说的堆和栈并不是数据结构的堆和栈,之所以要说数据结构的堆和栈是为了和后面我要说的堆区和栈区区别开来,请大家一定要注意。
下面就说说C语言程序内存分配中的堆和栈,这里有必要把内存分配也提一下,大家不要嫌我啰嗦,一般情况下程序存放在Rom或Flash中,运行时需要拷到内存中执行,内存会分别存储不同的信息,如下图所示:
内存中的栈区处于相对较高的地址以地址的增长方向为上的话,栈地址是向下增长的。
栈中分配局部变量空间,堆区是向上增长的用于分配程序员申请的内存空间。另外还有静态区是分配静态变量,全局变量空间的;只读区是分配常量和程序代码空间的;以及其他一些分区。
以上就是关于内存分配是什么意思windows中的内存分配方式有哪些全部的内容,包括:内存分配是什么意思windows中的内存分配方式有哪些、C语言中内存分布及程序运行加载过程、Java中的详细内存分配图,按照如下程序等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)