目录
一. 内存的模型
前言:
c语言不能直接操作物理内存,程序中使用的内存都是虚拟内存。
对于C程序而言,内存空间主要由五个部分组成:
数据段(data)、未初始化数据段(bss),代码段(text)、堆(heap) 和 栈(stack) 组成,
其中数据段和BSS段,代码段,是编译的时候由编译器分配的,而堆和栈是程序运行的时候由系统分配的。布局如下:
二. 栈(STACK)
2.1 栈是什么?
栈(Stack)通常指的是程序运行时使用的一片临时内存区域,用于存储函数调用信息和局部变量。它的数据结构类似于一个“后进先出”的容器,新的元素被压入栈顶,最后压入的元素先被取出,因此也被称为 “后进先出” 的数据结构。
由于栈有 后进先出 特点,所以栈特别方便用来 保存/恢复 调用现场。
2.2 栈的特点
后进先出(LIFO):
栈是一种后进先出的数据结构,最后压入的元素会最先弹出。新的元素会被添加到栈顶,而只能从栈顶移除元素。
自动内存管理:
栈的内存分配和释放是由编译器自动完成的,无需手动管理。当函数被调用时,函数的参数和局部变量会被分配在栈上;函数返回时,这些内存空间会被自动释放。
有限大小:
栈的大小是有限的,由系统或编译器所定义。 栈的大小通常比堆小得多,因为栈的内存空间是连续分配的,并且需要保证栈的操作高效执行。
快速访问:
由于栈的实现方式是连续的内存区域,栈上的数据访问非常快速,只需要对栈顶指针进行简单的加减操作即可。
局部性原理:
栈上的数据具有局部性,即相邻的栈帧通常包含相关的数据。这种局部性原理可以提高缓存的利用效率,并优化程序的性能。
函数调用和返回:
栈在函数调用和返回过程中起到重要作用,存储了函数的返回地址、参数值和局部变量等信息。函数调用时会将相关信息压入栈中,函数返回时会从栈中弹出这些信息,以恢复到调用前的状态。
注意:
栈的大小有限,过度使用栈可能导致栈溢出问题,因此在编程中需要合理控制栈的使用。
//栈溢出 void stack_overflow(void) { int a[100000000]={0}; a[100000000-1]=12; } int main(void) { stack_overflow(); } //程序报错:error:Segmentation fault(core dumped)
三. 堆(HEAP)
3.1 堆是什么?
堆(Heap)是一段动态分配的内存空间,用于存储程序运行时需要动态申请的数据。它通常指的是操作系统或者运行时库提供的一个内存池,可以通过调用相关的内存管理函数例来对其进行动态内存的分配和释放。
堆的数据结构类似于一个无序的容器,程序可以在其中任意插入、删除和查找元素。不同于栈,堆并没有固定的访问顺序,每个元素的存储地址都是动态分配的,因此从堆中获取数据时需要通过指针进行访问。
3.2 堆的特点
动态内存分配:
堆是一段动态分配的内存空间,程序可以在运行时根据需要动态地申请和释放内存。通过使用堆,程序可以根据实际需求来分配所需大小的内存空间,而不需要在编译时确定固定的大小。
随机访问:
堆的数据存储是无序的,每个元素的存储地址都是动态分配的。因此,程序可以随机地访问堆中的元素,而不需要遵循固定的访问顺序。
大小可变:
堆的大小可以在运行时动态变化。程序可以根据需要动态地增加或减少堆的大小,从而适应不同的数据存储需求。
手动内存管理:
在堆中分配内存需要程序员手动管理内存的申请和释放过程。程序员需要使用内存管理函数,如
malloc()
、calloc()
来申请内存,并使用free()
函数来显式释放不再需要的内存空间。这样可以确保及时释放内存,避免内存泄漏和资源浪费。
内存碎片化:
由于堆的动态内存分配,可能会出现内存碎片化的情况。即分配的内存空间可能是不连续的,这可能会导致一些内存浪费和额外的性能开销。因此,需要进行合理的内存管理和优化,以减少内存碎片化的影响。
注意:
在使用堆时,需要谨慎地管理内存,确保内存的正确申请和释放,避免出现内存泄漏、野指针等问题,以保证程序的正确性和稳定性。
3.3 堆内存的相关函数
3.3.1 malloc()
函数 void *malloc(unsigned int size) 功能 动态地分配一块指定大小的内存空间 参数 size:申请空间的大小 返回 成功 - 分配空间的起始地址,失败 - NULL 注意:
内存泄漏:
每次调用
malloc()
分配内存后,都需要相应地调用free()
函数来释放已经使用完毕的内存块,避免内存泄漏。未释放的内存会导致程序运行过程中不断占用内存空间,最终可能导致内存耗尽的问题。
初始化内存:
malloc()
分配的内存块通常是未初始化的,其中的内容是不确定的。如果需要将内存块初始化为特定的值,可以使用calloc()
函数进行分配,它会自动将内存块清零。
检查分配是否成功:
在调用
malloc()
分配内存后,需要检查返回的指针是否为NULL
,判断内存分配是否成功。如果返回的指针为NULL
,表示内存分配失败,可能是因为内存不足或其他原因导致的。
内存越界访问:
使用
malloc()
分配的内存块,其大小是由参数指定的。在访问该内存块时,需要确保不超出其分配的范围,即避免发生内存越界访问。越界访问可能导致数据损坏、程序崩溃等问题。
释放已释放内存:
一旦调用
free()
函数释放了一块内存块,就不应再次访问或释放该内存块。这样的行为是未定义的,可能导致程序出现错误或崩溃。确保只释放一次已分配的内存块,避免重复释放引起的问题。
大内存分配:
如果需要分配较大的内存块(如几十兆字节以上),需要注意系统的可用内存资源和操作系统对单个进程可用内存的限制。过度分配大内存块可能导致性能问题、系统资源耗尽或申请失败。
3.3.2 realloc()
函数 void *realloc(void *s, unsigned int newsize) 功能 动态地分配一块指定大小的内存空间 参数 s:原本开辟好的空间的首地址
newsize:重新开辟的空间的大小返回 成功 -新申请的内存的首地址,失败 - NULL 注意:
检查返回值:
realloc()
函数可能会返回NULL
,表示重新分配内存失败。因此,在使用realloc()
分配内存后,应该始终检查返回值,确保分配成功。
更新指针:
如果
realloc()
成功分配了新的内存块,返回的指针可能与原来的指针不同。因此,在使用realloc()
后,应该将返回的指针赋给相关的指针变量,更新指针的值。
避免频繁地分配和释放:
频繁地对小块内存进行
realloc()
操作可能会导致效率低下。因为realloc()
可能需要在内存中找到连续的空闲内存块,并将数据复制到新位置。
注意处理被截断的数据:
如果新分配的内存大小小于原来的大小,超出新大小的部分数据会被截断,即丢失。因此,在调整内存块大小时,要确保不会丢失关键的数据。
3.3.3 free()
函数 void *free(void *ptr) 功能 将内存块返回给系统的内存池,使其可供其他部分使用 参数 ptr:开辟后使用完毕的堆区空间的首地址 返回 无 注意:
只能释放通过
malloc()
、calloc()
、realloc()
分配的内存:
free()
函数只能用于释放使用这些函数分配的内存块。不要尝试释放静态分配的内存、栈上的变量或常量字符串,否则会导致未定义的行为。
一次释放一块内存:
每次调用
free()
只能释放一个内存块。如果你的程序中使用了多个malloc()
或calloc()
来分配内存,需要对每个内存块分别调用free()
进行释放。
不要尝试多次释放同一块内存:
尝试释放已经被释放的内存块会导致未定义的行为,包括程序崩溃等问题。因此,在使用
free()
释放内存后,应该立即将指针设置为NULL
,以避免误用。
不要依赖释放后的内存:
一旦调用
free()
释放了内存块,就不再拥有该内存块。访问已经释放的内存是未定义行为,可能导致程序崩溃或产生不可预测的结果。
注意释放数组和结构体的内存:
如果分配了一个数组或结构体的内存,你应该使用
free()
释放整个数组或结构体,而不仅仅是首个元素的内存块。
四. 未初始化数据区(BSS)
4.1 未初始化数据段是什么?
未初始化数据段(BSS)是指程序运行时使用的一片内存区域,用于存储未初始化的全局变量和静态变量。BSS段在程序加载时会被系统初始化为0或空值。
4.2 未初始化数据段的特点
存储未初始化的全局变量和静态变量:
BSS段用于存储未初始化的全局变量和静态变量。这些变量在程序执行之前就已经分配了内存空间,但由于未进行初始化,所以其初始值是未定义的。
自动初始化为零或空值:
BSS段在程序加载时会被系统自动初始化为零值或空值。这意味着在程序启动时,BSS段的内存空间会被清零,将其中的全局变量和静态变量都设置为零或空。
仅保留变量的存储空间大小:
BSS段在可执行文件中仅仅定义了变量的存储空间大小,没有实际的数据存储。这样可以减小可执行文件的大小。
不占用磁盘空间:
由于BSS段只定义了变量的存储空间大小,没有实际的数据存储,因此在可执行文件中不占用磁盘空间。当程序加载到内存时,操作系统为BSS段分配相应大小的内存。
可读写访问:
BSS段中存储的变量是可读写访问的,可以通过变量名直接进行访问和修改。
注意:
在C语言中,全局变量和静态变量如果没有被程序员显示地初始化,它们会被自动初始化为0。这种情况下,我们说这些变量是未初始化的。
一般的书籍会说全局变量和静态变量是会自动初始化的,是指它们会被编译器自动初始化为0,但如果程序员自己显式地给这些变量指定了初始值,那么编译器会使用程序员指定的初始值来初始化这些变量。
示例代码:
/****第一个代码*****/ int arr[40000]; void main() { ...... }
/****第二个代码*****/ int arr[40000] = {1, 2, 3, 4, 5, 6 }; void main() { ...... }
以上两个程序相比之下,程序2的可执行文件 比 程序1大。
原因:
程序1全局的未初始化变量存在于.bss段中; 程序2全局的已初始化变量存于.data段中。
.bss是不占用可执行文件空间的,其内容由操作系统初始化;
.data却需要占用,其内容由程序初始化。
五. 初始化数据区(Data Segment)
5.1 已初始化数据段是什么?
已初始化数据段(Data Segment)是程序运行时使用的一片内存区域,用于存储已经初始化的全局变量和静态变量。在编写C语言程序时,如果为全局变量或静态变量显式地指定了初始值,那么这些变量的内存空间就会被分配到数据段。
数据段又可以分为只读数据段和读写数据段
只读数据段主要用于存储不可修改的数据,例如字符串常量。这些数据在程序加载时就已经被赋予了初始值,并且在程序运行期间不能进行修改。因此,它们被放在只读数据段中,以确保其内容的安全性和不可变性。
读写数据段用于存储可以被程序读取和修改的数据,例如已初始化的全局变量和静态变量。这些变量在程序加载时也会被赋予初始值,但在程序运行过程中,它们的值可以被修改。
5.2 已初始化数据段的特点
存储已初始化的全局变量和静态变量:
已初始化数据段用于存储那些在程序中被明确初始化了初始值的全局变量和静态变量。
静态内存分配:
数据段在编译阶段就被静态地分配了一定大小的内存空间,供程序运行时使用。这意味着数据段中的变量在程序运行期间都存在,并且可以被多个函数和模块共享访问。
可读写访问:
已初始化数据段中的变量可以被程序读取和写入。这意味着我们可以在程序中读取和修改这些变量的值。
初始值保留:
已初始化数据段中的变量在程序加载时会被系统自动初始化,按照程序员指定的初始值进行赋值。因此,这些变量在程序开始执行之前就已经具有了初始值。
磁盘空间占用:
数据段中的变量的初始值在程序的可执行文件中也要保存相应的数据值,所以会占用磁盘空间。
存储位置固定:
已初始化数据段中的变量在程序运行期间的存储位置是固定的,不会随着函数调用或局部变量的创建而改变。
六. 代码段(Code Segment)
6.1 代码段是什么?
代码段(Code Segment)是指存储程序执行代码的内存区域,代码段一般是只读的,且存储了程序可执行文件中的机器指令和常量数据。在C语言程序中,在编写函数、方法和全局变量等时,相应的代码会被编译成机器指令并存储在代码段中。
与已初始化数据段类似,代码段的内存空间也是在程序加载时静态地分配的。它通常包含程序的主要逻辑,由操作系统在程序运行时将代码段映射到进程的虚拟地址空间中,以供程序执行时调用。
需要注意的是,由于代码段通常是只读的,所以在程序运行时无法修改其中的内容。如果程序需要在运行时动态地生成新的机器指令,就需要使用其他可读写的内存段,如堆或栈。
6.2 代码段的特点
存储可执行代码:
代码段是用来存储程序的可执行代码的内存区域。它包含了程序中定义的函数、方法和其他执行逻辑的机器指令。
只读访问:
代码段通常是只读的,意味着程序在执行过程中无法修改代码段中的指令。这是为了保护代码的完整性和安全性。
固定大小:
代码段在编译时就被静态地分配了固定大小的内存空间。这意味着在程序运行期间,代码段的大小不会改变。
程序共享:
代码段通常是可共享的。多个相同的程序实例可以共享同一个代码段,这样可以节省内存空间。
程序加载时映射:
在程序加载时,操作系统将代码段映射到进程的虚拟地址空间中。这样,程序在执行时就可以直接访问代码段中的指令。