C 内存管理那些事儿


一、内存介绍

本文主要介绍C内存管理基本概念,以及C语言编译后的可执行程序的存储结构和运行结构。

在用户存储空间,一个C程序的在内存中的分配分类5大部分:代码段、全局已初始化数据段、bss段、堆和栈。其中各部分具体所指:

1、代码段(text segment)
   存放CPU执行的机器指令(machine instructions)。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。

2、全局已初始化数据段    存入的是初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

3、bss段 / 全局未初始化数据区
   存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。

4、堆 heap
  用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。

5、栈 stack
  由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。

之所以分成这么多个区域,主要基于以下考虑:

一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。

临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。

全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。

堆区由用户自由分配,以便管理。


例子:


二、内存分配方式

在C语言中,对象可以使用静态或动态的方式分配内存空间。

静态分配:编译器在处理程序源代码时分配。

动态分配:程序在执行时调用malloc库函数申请分配。

静态内存分配是在程序执行之前进行的因而效率比较高,而动态内存分配则可以灵活的处理未知数目的。

静态与动态内存分配的主要区别如下:

对象是没有名字的变量,需要通过指针间接地对它进行操作。

静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc()和free两个函数(C++中为new和delete运算符)来完成。

以下是采用静态分配方式的例子。

int a=100;

此行代码指示编译器分配足够的存储区以存放一个整型值,该存储区与名字a相关联,并用数值100初始化该存储区。

以下是采用动态分配方式的例子。

p1 = (char *)malloc(10*sizeof(int));//分配得来得10*4字节的区域在堆区

此行代码分配了10个int类型的对象,然后返回对象在内存中的地址,接着这个地址被用来初始化指针对象p1,对于动态分配的内存唯一的访问方式是通过指针间接地访问,其释放方法为:

free(p1);

三、堆栈那些事儿

  • 栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。
  • 堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成(C++中为delete)。栈和堆的主要区别有以下几点:

(1)管理方式不同。

栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。

(2)空间大小不同。

栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。

(3)是否产生碎片。

对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

(4)增长方向不同。

堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

(5)分配方式不同。

堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。

(6)分配效率不同。

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。


四、内存管理函数

  • malloc/free函数

Malloc()函数用来在堆中申请内存空间,free()函数释放原先申请的内存空间。Malloc()函数是在内存的动态存储区中分配一个长度为size字节的连续空间。其参数是一个无符号整型数,返回一个指向所分配的连续存储域的起始地址的指针。当函数未能成功分配存储空间时(如内存不足)则返回一个NULL指针。

由于内存区域总是有限的,不能无限制地分配下去,而且程序应尽量节省资源,所以当分配的内存区域不用时,则要释放它,以便其他的变量或程序使用。

需要特别注意下面几点:

(1)调用free()释放内存后,不能再去访问被释放的内存空间。内存被释放后,很有可能该指针仍然指向该内存单元,但这块内存已经不再属于原来的应用程序,此时的指针为悬挂指针(可以赋值为NULL)。

(2)不能两次释放相同的指针。因为释放内存空间后,该空间就交给了内存分配子程序,再次释放内存空间会导致错误。也不能用free来释放非malloc()、calloc()和realloc()函数创建的指针空间,在编程时,也不要将指针进行自加操作,使其指向动态分配的内存空间中间的某个位置,然后直接释放,这样也有可能引起错误。

(3)在进行C语言程序开发中,malloc/free是配套使用的,即不需要的内存空间都需要释放回收。

  • realloc--更改已经配置的内存空间

realloc()函数用来从堆上分配内存,当需要扩大一块内存空间时,realloc()试图直接从堆上当前内存段后面的字节中获得更多的内存空间,如果能够满足,则返回原指针;如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉。如果内存不足,重新申请空间失败,则返回NULL。此函数定义如下:

void *realloc(void *ptr,size_t size)

参数ptr为先前由malloc、calloc和realloc所返回的内存指针,而参数size为新配置的内存大小

当调用realloc()函数重新分配内存时,如果申请失败,将返回NULL,此时原来指针仍然有效,因此在程序编写时需要进行判断,如果调用成功,realloc()函数会重新分配一块新内存,并将原来的数据拷贝到新位置,返回新内存的指针,而释放掉原来指针(realloc()函数的参数指针)指向的空间,原来的指针变为不可用(即不需要再释放,也不能再释放),因此,一般不使用以下语句:

ptr=realloc(ptr,new_amount)

如果内存减少,malloc仅仅改变索引信息,但并不代表被减少的部分还可以访问,这一部分内存将交给系统内存分配子程序。

  • 其他内存管理函数calloc和alloca

calloc是malloc函数的简单包装,它的主要优点是把动态分配的内存进行初始化,全部清零。其操作及语法类似malloc()函数。

alloca()函数用来在栈中分配size个字节的内存空间,因此函数返回时会自动释放掉空间。alloca函数定义及库头文件如下:

返回值:若分配成功返回指针,失败则返回NULL。

它与malloc()函数的区别主要在于:

alloca是向栈申请内存,无需释放,malloc申请的内存位于堆中,最终需要函数free来释放。

malloc函数并没有初始化申请的内存空间,因此调用malloc()函数之后,还需调用函数memset初始化这部分内存空间;alloca则将初始化这部分内存空间为0。

输出为:

时间: 2024-10-30 09:33:13

C 内存管理那些事儿的相关文章

Kernel那些事儿之内存管理(8) --- Slab(中)

上篇讲了Slab中的数据结构,这篇该讲Slab中的操作了. 既然是内存管理,那操作无非就两点:allocate 和 free. 1. 申请一个object 在Slab中,申请一个object是通过函数 kmem_cache_alloc() 来完成的. 3618 void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 3619 { 3620     return __cache_alloc(cachep, flags, __bu

Kernel那些事儿之内存管理(1)

有人的地方就有江湖.要介绍内存管理这个江湖,首先还得从这里面的主要人物讲起. 在NUMA结构中,物理内存首先被分成若干nodes.每一个node进一步被分成若干zones.每一个zone又关联了一个描述page frames的数组,该数组包含了属于该zone的所有page frame的描述符. 不难看出,在这个江湖里主要有三位重要人物:nodes, zones 和 page frames.这三者的关系和地位大体可以用下图来描述(该图取自"Professional Linux Kernel Arc

Kernel那些事儿之内存管理(3) --- 久别重逢

上次我们讲到page frame是物理内存的基本组成单位.那Kernel就必须要有一套机制来管理空闲的page frames.这一点不难理解.每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱. 这个问题看似简单,实则不然.因为这里面有一个外碎片的问题. 在物理内存中,连续的物理内存页有时是很重要的.例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续:再例如,使用连续的物理内存页,可

Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收

很多Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收肯定要涉及到JVM内存管理机制,Java语言的执行效率一直被C.C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确实很低,一方面,Java语言采用面向对象思想,这也决定了其必然是开发效率高,执行效率低.另一方面,Java语言对程序员做了一个美好的承诺:程序员无需去管理内存,因为JVM有垃圾回收(GC),会去自动进行垃圾回收. 其实不然: 1.垃圾回收并不会按照程序员的要求,随时进行GC. 2.垃圾回收并不会及时

Unity 3D中的内存管理

本文欢迎转载,但烦请保留此行出处信息:http://www.onevcat.com/2012/11/memory-in-unity3d/ Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极差的体验.类似这种情况并不少见,但是绝大部分都是可以避免的.虽然理论上Unity的内存管理系统应当为开发者分忧解难,让大家投身到更有意义的事情中去,但是对于Unity对内存的管理方式,官方文档中并没有太多的说明,基本需要依靠

JVM内存管理及垃圾回收

一.JVM内存的构 Java虚拟机会将内存分为几个不同的管理区,这些区域各自有各自的用途,根据不同的特点,承担不同的任务以及在垃圾回收时运用不同的算法.总体分为下面几个部分: 程序计数器(Program Counter Register).JVM虚拟机栈(JVM Stacks).本地方法栈(Native Method Stacks).堆(Heap).方法区(Method Area) 如下图: 1.程序计数器(Program Counter Register) 这 是一块比较小的内存,不在Ram上

Java虚拟机内存管理机制

自动内存管理机制 Java虚拟机(JVM)在执行Java程序过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则是依赖用户线程的启动和结束而建立和销毁.根据<Java虚拟机规范 第2版>规定,运行时数据区包括: 1.程序计数器 一块较小的内存空间,不在Ram上,而是直接划分在CPU上的,程序员无法直接操作它.当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令.每条

JAVA当中内存管理与垃圾回收!

很多Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收肯定要涉及到JVM内存管理机制,Java语言的执行效率一直被C.C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确实很低,一方面,Java语言采用面向对象思想,这也决定了其必然是开发效率高,执行效率低.另一方面,Java语言对程序员做了一个美好的承诺:程序员无需去管理内存,因为JVM有垃圾回收(GC),会去自动进行垃圾回收. 其实不然: 1.垃圾回收并不会按照程序员的要求,随时进行GC. 2.垃圾回收并不会及时

linux内存管理

一.Linux 进程在内存中的数据结构 一个可执行程序在存储(没有调入内存)时分为代码段,数据段,未初始化数据段三部分:    1) 代码段:存放CPU执行的机器指令.通常代码区是共享的,即其它执行程序可调用它.假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段.     2) 数据段:存放已初始化的全局变量.静态变量(包括全局和局部的).常量.static全局变量和static函数只能在当前文件中被调用.     3) 未初始化数据区(uninitializeddata s