编译器分配内存空间
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
1.栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
2.堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
3.自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)
② 我们经常看到书上面说的 某某变量的内存单元是编译器在编译时候分配的 是什么意思
所谓在编译期间分配空间指的是静态分配空间(相对于用new动态申请空间),如全局变量或静态变量(包括一些复杂类型的常量),它们所需要的空间大小可以
明确计算出来,并且不会再改变,因此它们可以直接存放在可执行文件的特定的节里(而且包含初始化的值),程序运行时也是直接将这个节加载到特定的段中,不
必在程序运行期间用额外的代码来产生这些变量。
其实在运行期间再看“变量”这个概念就不再具备编译期间那么多的属性了(诸如名称,类型,作用
域,生存期等等),对应的只是一块内存(只有首址和大小),
所以在运行期间动态申请的空间,是需要额外的代码维护,以确保不同变量不会混用内存。比如写new表示有一块内存已经被占用了,其它变量就不能再用它了;
写delete表示这块内存自由了,可以被其它变量使用了。(通常我们都是通过变量来使用内存的,就编码而言变量是给内存块起了个名字,用以区分彼此)
内存申请和释放时机很重要,过早会丢失数据,过迟会耗费内存。特定情况下编译器可以帮我们完成这项复杂的工作(增加额外的代码维护内存空间,实
现申请和释 放)。从这个意义上讲,局部自动变量也是由编译器负责分配空间的。进一步讲,内存管理用到了我们常常挂在嘴边的堆和栈这两种数据结构。
最后对于“编译器分配空间”这种不严谨的说法,你可以理解成编译期间它为你规划好了这些变量的内存使用方案,这个方案写到可执行文件里面了(该文件中包含若干并非出自你大脑衍生的代码),直到程序运行时才真正拿出来执行。
③ C语言 + 单片机-内存分布详解
C语言内存分区示意图如下:
1. 代码区
2. 常量区
3. 全局(静态)区
.bss段
.data段
4. 堆区(heap)
调用函数参数size_t是分配的字节大小,返回值是一个void型的指针,该指针指向分配空间的首地址。参数是开辟的内存的首地址。
5. 栈区(stack)
在STM32中,内存分配如下:
1. 随机存储器—RAM
2. 只读存储器—ROM
STM32F103芯片的内部分区如下图所示。
编译程序完成后,可以看到编译后的大小信息,包括Code、RO-data、RW-data和ZI-data的大小。这有助于开发人员和嵌入式系统设计者更好地管理内存资源并确保程序在目标设备上正常运行。
Code:代码段,指程序由编译器生成的可执行的机器指令。
RO-data:数据段,指程序中的只读数据部分,包括常量、字符串、const定义的变量等。
RW-data:数据段,指初始化为“非0值“的可读写数据,程序运行的时候这些数据又会常驻在RAM区,应用程序可以修改其内容。包括初始化为非零的全局变量和静态变量。
ZI-data:数据段,指初始化为0值的可读可写数据,它与RW-data的区别是程序刚运行时这些数据初始值全都为0,程序运行时和RW-data的性质一样,它们也常驻在RAM区,应用程序可以更改其内容。包括未初始化和初始化为零的全局变量和静态变量。
ZI-data 的栈空间(Stack)及堆空间(Heap):在C语言中,函数内部定义的局部变量属于栈空间,而使用malloc动态分配的变量属于堆空间。在程序中的栈空间和堆空间都是属于ZI-data区域的,这些空间都会被初始值化为0值。
关于哪些数据存储在Flash区域,哪些数据存储在SRAM区域,这涉及到程序的存储状态。程序具有静止和运行两种状态,静止态的程序被存储在非易失存储器中,如内部FLASH区域。当程序在运行状态的时候,程序常常需要修改一些暂存数据,这些数据往往存放在Flash中,但需要被复制到RAM中。
程序存储分布如下图展示。当程序存储到芯片的内部FLASH时,它占用的空间为Code+RO-data+RW-data的总和。程序在执行的时候,需要占用内部SRAM空间,占用的空间为RW-data+ZI-data之和。
结论,想要让一个程序正常运行,必须满足以下两个条件:程序编译后打开工程的map文件,在map文件的最后一段也可以看到ROM的总大小。
划分依据及好处:首先区分代码段和数据段。程序源代码编译后的机器指令放在代码段;数据段包括" .data "、" .bss "、" .rodata ",将程序中定义的全局变量和局部变量都称为数据段。
把程序的“代码段”和“数据段”分开存放,数据和指令分别被映射到两个虚拟内存区域,数据段对进程是可读写的,而代码段对进程是只读的。现代CPU缓存设计成数据缓存和指令缓存分离,程序的指令和数据分开存放提高缓存命中率。
数据段还需要分 ".data"、".bss"、".rodata",主要根据是否占内存空间、读写权限进行区分。".data"段和".bss"段都是可读写的数据段,而".rodata"存放的是只读数据,主要是一些const变量和字符串常量。".rodata"段单独设立的好处是可以将属性映射成只读,减少修改操作,还可以将".rodata"段存放在只读存储器中。
全局变量也有细分初始化和未初始化,把全局变量分开存放,初始化为0和未初始化的全局变量放在BSS区,初始化不为0的全局变量存放在数据区。程序有两个存储状态,静止状态的程序被存储在非易失存储器中,运行状态的程序需要修改一些暂存数据存放在内存中,减少从ROM读数据的次数提高效率。
④ C语言内存管理机制--malloc/calloc/free原理与实现
一、C程序的存储空间布局
在C程序中,存储空间布局通常分为栈和堆两种类型。栈用于函数调用时的局部变量存储,其大小由编译器自动管理,遵循后进先出(LIFO)原则。堆用于动态内存分配,可以由程序在运行时动态地请求和释放内存。
二、Heap内存模型
在堆内存中,malloc所申请的内存主要从堆区域分配。Linux内核通过维护一个break指针来管理堆空间。这个指针指向堆空间的某个地址,从堆起始地址(Heap’s Start)到break之间的地址空间为映射好的(虚拟地址与物理地址的映射,通过MMU实现),可以供进程访问。从break向上,是未映射的地址空间,访问这些空间会导致程序报错。
三、调整break:brk()和sbrk()
break指针最初位于bss段的末尾之后,当break指针升高时,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配,内存会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页。
Linux通过brk和sbrk系统调用操作break指针。brk()将break指针设置为指定位置,地址四舍五入到下一个内存页的边界处。sbrk()将break指针在原有地址基础上增加指定的大小。sbrk(0)返回当前break指针的位置。系统对进程所分配的资源有限,包括映射的内存空间。
四、malloc
malloc函数用于在系统中动态分配连续的可用内存。它要求内存大小至少为指定的字节数,返回指向内存块起始地址的指针,多次调用不重叠分配地址,实现内存分配和释放。malloc函数的返回值总是字节对齐,适合高效访问C语言数据结构。
五、初探实现malloc
一个简单实现的malloc函数直接从未映射区域划出内存,但忽略了记录分配的内存块信息,导致内存释放时无法确定释放的大小,需要额外数据结构记录块信息。
六、正式实现malloc
实现一个完整的malloc需要一个数据结构组织堆内存,每个内存块包含元信息(大小、空闲状态、指针)和实际数据区域。查找合适的内存块、分配新的块、分裂块等操作需实现相应函数。
七、calloc的实现
calloc函数用于给一组相同对象分配内存,并初始化它们。实现只需两次调用malloc,一次分配内存,另一次初始化。
八、free的实现
free函数需要验证地址的有效性,并解决碎片问题。实现策略包括合并相邻空闲内存块,确保释放的地址与未映射区域之间是空闲的。
九、realloc的实现
realloc函数调整已分配内存的大小。实现包括复制现有内存、调整大小、释放旧内存等操作。
十、总结
通过上述机制,C语言提供内存管理功能,允许程序动态分配和释放内存。优化空间和实际应用的内存管理策略如Linux内核伙伴算法、STL空间配置器等提供了更高效的实现。