C语言内存管理

时间:2022-09-09 20:01:15
1、内存管理
  内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。
 
2、变量的分类及作用域
1) 普通局部变量
  属于某个{},在{}外不能使用此变量,在{}内部是可以使用的。代码执行到普通局部变量定义语句时,才会分配空间,离开{}后,自动释放。普通局部变量不初始化时,默认值为随机数。
2) static修饰的局部变量
  属于某个{},在{}外部不能使用此变量,在{}内部可以使用。在编译阶段就已经分配了空间,初始化只能使用常量。static局部变量不初始化时,默认值为0。离开{},static局部变量不会释放,只有整个程序结束才会释放。
  注意:static局部变量的作用域属于某个{},但它的生命周期却是从编译阶段到整个程序结束。
3) 普通全局变量
  在编译阶段分配内存,只有整个程序执行结束才释放。普通全局变量只要定义了,任何地方都能使用,使用前需要申明所有的.c文件,只能定义一次全局变量,但是可以声明多次(外部链接)。
  注意:全局变量的作用域时全局范围,但是在某个文件使用时不需先声明。
4) static全局变量
  在编译阶段分配内存,只有整个程序执行结束才释放。static全局变量只有在定义所在的的文件使用此变量(内部链接)。不同的.c文件,可以定义一次static全局变量。
 
3、内存分区
  C源代码经过预编译、编译、汇编和链接四个步骤,生成一个可执行程序。程序在没有运行之前,也就是说程序在没有被加载到内存之前,可执行程序的内部已经分好了3段信息:代码区(text)、数据区(data)和未初始化数据区(BSS)。有的人直接把数据区和未初始化的数据区合起来叫静态区和全局区
  运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区、数据区和未初始化的数据区以外,还外加两个分区:堆区和栈区。
1)代码区(text)
  代码区存放CPU执行的机器指令。通常代码区是可共享的(即另外的程序也可以调用它)。因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,为了防止程序意外的修改了它的指令。
2)数据区(data)(全局初始化数据区/静态数据区)
  该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
3)未初始化的数据区(bss)
  存入的是全局未初始化的变量和未初始化的静态变量。未初始化数据区的数据在程序开始之前被内核初始化为0或NULL。
4)栈区(stack)
  栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数、返回值、局部变量等。在程序运行中实时加载和释放,因此,局部变量的生命周期为申请到释放该段栈空间。不同的操作系统分配给每一个程序的栈区大小不同:一般Windows是1M~8M不等,Linux是1M~16M不等。
5)堆区(heap)
  堆是一个大容器,它的容量远大于栈,用于动态内存分配,对在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若不主动释放,程序结束时,由操作系统回收。堆区通常加载音频文件、视频文件、图像文件、文本文件以及大小超过栈大小的由程序员主动申请分配的内存的大数组等。
  注意:
  1) 所有未初始化的静态变量和全局变量,编译器会默认赋初值0.
  2)程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。
  3)data段和bss区中的数据的生存周期为整个程序运行过程。
  4)data段、text区和bss区是由编译器在编译时分配的,堆和栈是由系统在运行时分配的。
 
4、内存的分配方式
在C语言中,对象可以使用静态或动态的方式分配内存空间。
  静态分配:编译器在处理程序源代码的时候分配
  动态分配:程序在执行时调用malloc等库函数分配
  静态内存分配是在程序执行之前进行的,因而效率比较高;而动态内存分配则可以灵活的处理未知数目的内存分配,灵活性较强。
静态与动态内存分配的主要区别如下
1)静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的一段地址,需要通过指针间接地对它尽心操作。
2)静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc和free两个函数来完成。
 
5、堆和栈的区别
1)管理方式不同
  栈是编译器自动管理,无须程序员手工控制;而堆空间的申请释放工作则由程序员控制,这就很容易产生内存泄露(Memory leak)。
2)空间大小不同
  栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的最大剩余空间的时候,将提示溢出。用户能从栈获得的空间较小
  堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址遍历。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中弹出的情况。
3)是否产生碎片
  对于堆来讲,频繁的调用malloc/free、new/delete等库函数申请内存空间,势必会造成内存空间的不连续,从而造成大量的内存碎片,使程序效率降低(虽然程序在退出后操作系统会对内存空间进行回收管理)。对于栈来讲,则不会存在这个问题。
4)内存增长方向不同
  堆的增长方向是向上的,即向着内存地址增长的方向增;而栈的增长方向是向下的,即向着内存地址减小的方向增长。
5)分配方式不同
  堆都是程序通过使用库函数malloc()动态申请分配并由free函数释放的;栈的申请和释放是由编译器自动完成的;栈的动态内存分配由函数alloca()完成,用完马上释放。栈的动态内存分配和堆完全不同,它的动态分配是由编译器申请释放的,无须程序员手工操作。
6)分配效率不同
  栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都由专门的指令执行。堆则是C语言库函数支持的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的内存空间,如果没有足够大的内存空间(可能由于内存碎片太多),就需要操作系统来重新整理内存空间了,这样就有机会分配到足够大小的内存空间,然后返回该内存空间的首地址。显然,堆分配内存的效率比栈分配内存的效率要低的多。
  栈是由系统自动分配内存,速度较快,效率较高,但是程序员无法控制。
  堆是由库函数malloc()分配,一般速度较慢,且易产生内存碎片,分配效率也较低,不过用起来也方便。
7)申请后系统的响应
  栈:只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常,提示栈溢出。
  堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
8)堆和栈中的存储内容
  栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。栈中的内存是在程序编译完成以后就可以确定的,不论占用空间大小,还是每个变量的类型。
  堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
9)存取效率对比
  char s1[] = "a";
  char *s2 = "b";
  a是在运行时刻赋值的;而b是在编译时就确定的,但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
10)防止越界发生
  无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生意想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的。
 
6、学习内存管理的目的
  学习内存管理就是为了知道日后怎么样在合适的时候管理我们的内存。那么问题来了?什么时候用堆什么时候用栈呢?一般遵循以下三个原则:
  1)如果明确知道数据占用多少内存,那么数据量较小时用栈,较大时用堆;
  2)如果不知道数据量大小(可能需要占用较大内存),最好用堆(因为这样保险些);
  3)如果需要动态创建数组,则用堆。