【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )

相关文章链接 :
1.【嵌入式开发】C语言 指针数组 多维数组
2.【嵌入式开发】C语言 命令行参数 函数指针 gdb调试
3.【嵌入式开发】C语言 结构体相关 的 函数 指针 数组
4.【嵌入式开发】gcc 学习笔记(一) - 编译C程序 及 编译过程
5.【C语言】 C 语言 关键字分析 ( 属性关键字 | 常量关键字 | 结构体关键字 | 联合体关键字 | 枚举关键字 | 命名关键字 | 杂项关键字)
6.【C 语言】编译过程 分析 ( 预处理 | 编译 | 汇编 | 链接 | 宏定义 | 条件编译 | 编译器指示字 )
7.【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)


文章目录

  • 一. 动态内存分配
    • 1. 动态内存分配相关概念
      • ( 1 ) 动态内存分配 ( ① 变量 数组 -> 内存别名 | ② 变量 在 编译阶段 分配内存 | ③ 除了编译器分配的内存 还需额外内存 -> 动态内存 )
    • 2. 动态内存分配 相关方法
      • ( 1 ) 相关 方法简介 ( ① malloc calloc realloc 申请内存 | ② free 归还内存 | ③ malloc 申请内存 , 不初始化值 | ④ calloc 申请内存 并 初始化 0 | ⑤ realloc 重置已经申请的内存 )
      • ( 2 ) malloc 函数 ( ① void *malloc(size_t size) ; size 字节大小 | ② 返回值 void* 需要强转为指定类型 | ③ 系统实际分配内存比 malloc 稍大 | ④ 如果内存用完会返回 NULL )
      • ( 3 ) free 函数 ( ① void free(void *ptr) | ② 作用 : 释放 malloc 申请的动态空间 | ③ 参数 : void *ptr 指针指向要释放的内存首地址 | ④ 返回值 : 没有返回值 )
      • ( 4 ) calloc 函数 ( ① void *calloc(size_t nmemb, size_t size) | ② 作用 : 申请 指定元素个数 指定元素大小 的内存 , 并将每个元素初始化成 0 | ③ size_t nmemb 参数 : 元素个数 | ④ size_t size 参数 : 元素大小 )
      • ( 5 ) realloc 函数 ( ① void *realloc(void *ptr, size_t size) | ② 作用 : 重新分配一个已经分配并且未释放的动态内存的大小 | ③ void *ptr 参数 : 指向 一块已经存在的动态内存空间的首地址 | ④ size_t size 参数 : 需要重新分配内存大小 | ⑤ ptr 参数为 NULL , 函数与 malloc 作用一样 | ⑥ 要使用新地址 旧地址 ptr 不能继续使用了 )
      • ( 6 ) 代码示例 ( 动态内存分配简单示例)
  • 二. 栈 堆 静态存储区
    • 1. 栈
      • ( 1 ) 栈 相关概念
      • (2) 代码示例 ( 简单的函数调用的栈内存分析 )
      • ( 3 ) 栈内存行为分析 ( 图文分析版本 )
    • 2. 堆
      • ( 1 ) 标题3
    • 3. 静态存储区
      • ( 1 ) 标题3
  • 三. 程序内存布局
    • 1. 程序运行前的程序文件的布局 ( 代码段 | 数据段 | bss段 )
      • (1) 相关概念简介
      • ( 2 ) 分析程序文件的内存布局
    • 2. 程序运行后的内存布局 ( 栈 | 堆 | 映射文件数据 [ bss段 | data段 | text段 ] )
      • ( 1 ) 相关概念简介
    • 3. 总结
  • 四. 野指针 ( 程序BUG根源 )
    • 1. 野指针相关概念
      • ( 1 ) 野指针简介
      • ( 2 ) 野指针的三大来源
    • 2. 经典指针错误分析 (**本节所有代码都是错误示例**)
      • ( 1 ) 非法内存操作
      • ( 2 ) 内存申请成功后未初始化
      • ( 3 ) 内存越界
      • ( 4 ) 内存泄露
      • ( 5 ) 指针多次释放 (***谁申请谁释放***)
      • ( 6 ) 使用已经释放的指针
    • 3. C语言中避免指针错误的编程规范
      • ( 1 ) 申请内存后先判空
      • ( 2 ) 避免数组越界 注意数组长度
      • ( 3 ) 动态内存 谁申请 谁释放
      • ( 4 ) 释放后立即置NULL






一. 动态内存分配




1. 动态内存分配相关概念



( 1 ) 动态内存分配 ( ① 变量 数组 -> 内存别名 | ② 变量 在 编译阶段 分配内存 | ③ 除了编译器分配的内存 还需额外内存 -> 动态内存 )


动态内存分配 :

  • 1.C语言操作与内存关系密切 : C 语言中的所有操作都与内存相关 ;
  • 2.内存别名 : 变量 ( 指针变量 | 普通变量 ) 和 数组 都是在 内存中的别名 ;
    • ( 1 ) 分配内存的时机 : 在编译阶段, 分配内存 ;
    • ( 2 ) 谁来分配内存 : 由 编译器来进行分配 ;
    • ( 3 ) 示例 : 如 定义数组时必须指定数组长度, 数组长度在编译的阶段就必须指定 ;
  • 3.动态内存分配的由来 : 在程序运行时, 除了编译器给分配的一些内存之外, 可能 还需要一些额外内存才能实现程序的逻辑, 因此在程序中可以动态的分配内存 ;



2. 动态内存分配 相关方法


( 1 ) 相关 方法简介 ( ① malloc calloc realloc 申请内存 | ② free 归还内存 | ③ malloc 申请内存 , 不初始化值 | ④ calloc 申请内存 并 初始化 0 | ⑤ realloc 重置已经申请的内存 )


动态内存分配方法 :

  • 1.申请内存 : 使用 malloc 或 calloc 或 realloc 申请内存;

  • 2.归还内存 : 使用 free 归还 申请的内存 ;

  • 3.内存来源 : 系统专门预留一块内存, 用来响应程序的动态内存分配请求 ;

  • 4.内存分配相关函数 :

    • ( 1 ) malloc : 单纯的申请指定字节大小的动态内存, 内存中的值不管 ;
    • ( 2 ) calloc : 申请 指定元素大小 和 元素个数的 内存, 并将每个元素初始化为 0 ;
    • ( 3 ) realloc : 可以重置已经申请的内存大小 ;
       #include 

       void *malloc(size_t size);
       void free(void *ptr);
       void *calloc(size_t nmemb, size_t size);
       void *realloc(void *ptr, size_t size);



( 2 ) malloc 函数 ( ① void malloc(size_t size) ; size 字节大小 | ② 返回值 void 需要强转为指定类型 | ③ 系统实际分配内存比 malloc 稍大 | ④ 如果内存用完会返回 NULL )


malloc 函数简介 :

void *malloc(size_t size);
  • 1.作用 : 分配一块连续的内存 , 单位 字节, 该内存没有具体的类型信息 ;
  • 2.函数解析 :
    • ( 1 ) size_t size 参数 : 传入一个字节大小参数 , size 是要分配的内存的大小 ;
    • ( 2 ) void * 返回值 : 返回一个 void* 指针, 需要强制转换为指定类型的指针 , 该指针指向内存的首地址 ;
  • 3.请求内存大小 : malloc 实际请求的内存大小可能会比 size 大一些, 大多少与编译器和平台先关 , 这点知道即可, 不要应用到编程中 ;
  • 4.申请失败 : 系统为程序预留出一块内存用于 在程序运行时 动态申请, 当这块预留的内存用完以后, 在使用 malloc 申请, 就会返回 NULL ;



( 3 ) free 函数 ( ① void free(void *ptr) | ② 作用 : 释放 malloc 申请的动态空间 | ③ 参数 : void *ptr 指针指向要释放的内存首地址 | ④ 返回值 : 没有返回值 )


free 函数简介 :

void free(void *ptr);
  • 1.作用 : 释放 malloc 函数申请的 动态空间 ;
  • 2.函数解析 : 该函数 没有返回值 ;
    • *( 1 ) void ptr 参数 : 要释放的内存的首地址;
  • 3.传入 NULL 参数 : 假如 free 方法传入 NULL 参数, 则直接返回, 不会报错 ;



( 4 ) calloc 函数 ( ① void *calloc(size_t nmemb, size_t size) | ② 作用 : 申请 指定元素个数 指定元素大小 的内存 , 并将每个元素初始化成 0 | ③ size_t nmemb 参数 : 元素个数 | ④ size_t size 参数 : 元素大小 )


calloc 函数简介 :

void *calloc(size_t nmemb, size_t size);
  • 1.作用 : 比 malloc 先进一些, 可以申请 ① 指定元素个数 ② 指定元素大小 的内存 ;
  • 2.函数解析 :
    • ( 1 ) void * 类型返回值 : 返回值是一个 void * 类型, 需要转换为实际的类型才可以使用 ;
    • ( 2 ) size_t nmemb 参数 : 申请内存的元素 个数 ;
    • ( 3 ) size_t size 参数 : 申请内存的元素 大小 ;
  • 3.内存中的值初始化 : calloc 分配动态内存后, 会将其中每个元素的值都初始化为 0 ;



( 5 ) realloc 函数 ( ① void *realloc(void *ptr, size_t size) | ② 作用 : 重新分配一个已经分配并且未释放的动态内存的大小 | ③ void *ptr 参数 : 指向 一块已经存在的动态内存空间的首地址 | ④ size_t size 参数 : 需要重新分配内存大小 | ⑤ ptr 参数为 NULL , 函数与 malloc 作用一样 | ⑥ 要使用新地址 旧地址 ptr 不能继续使用了 )


realloc 函数简介 :

void *realloc(void *ptr, size_t size);
  • 1.作用 : 重新分配一个已经分配并且未释放的动态内存的大小 ;
  • 2.函数解析 :
    • ( 1 ) void * 类型返回值 : 重新分配后的指针首地址, 与参数 ptr 指向的地址是相同的, 但是需要使用 返回的新地址 , 不能再使用老地址了 ;
    • *( 2 ) void ptr 参数 : 指向 一块已经存在的动态内存空间的首地址 ;
    • ( 3 ) size_t size 参数 : 需要分配的新内存大小 ;
  • 3.void *ptr 参数为 NULL : 如果传入的 ptr 参数为 NULL, 那么该函数执行效果与 malloc 一样, 直接分配一块新的动态内存, 并返回一个指向其首地址的指针 ;



( 6 ) 代码示例 ( 动态内存分配简单示例)


代码示例 :

  • 1.代码 :
#include 
#include 

int main()
{
	//1. 使用 malloc 分配 20 个字节的内存, 这些内存中的数据保持原样
	int* p1 = (int*)malloc(sizeof(int) * 5);
	
	//2. 使用 calloc 分配 5 个 int 类型元素的 内存, 初始化 5 个元素的值为 0
	int* p2 = (int*)calloc(5, sizeof(int));
	
	//3. 以 int 类型 打印 p1 和 p2 指向的内存中的数据值
	int i = 0; 
	for(i = 0; i < 5; i ++)
	{
		printf("p1[%d] = %d, p2[%d] = %d\n", i, p1[i], i, p2[i]);
	}
	
	//4. 重新分配 p1 指向的内存, 在多分配 10 个数据;
	p1 = (int*) realloc(p1, 15);
	for(i = 0; i < 15; i ++)
	{
		printf("p1[%d] = %d\n", i, p1[i]);
	}
	
	return 0;
}
  • 2.编译运行结果 :
    【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第1张图片





二. 栈 堆 静态存储区




1. 栈



( 1 ) 栈 相关概念


栈 简介 :

  • 1.主要作用 : 维护 程序的 上下文 信息, 主要是 局部变量, 函数 的存储 ;
  • 2.存储策略 : 后进先出 ;

栈对函数的作用 :

  • 1.函数依赖于栈 : 栈内存中保存了函数调用需要所有信息 :
    • ( 1 ) 栈 保存 函数参数 : 函数的参数都会依次入栈, 保存在栈内存中 ;
    • ( 2 ) 栈 保存 函数返回地址 : ebp 指针指向 返回地址, 函数执行完毕后跳转到该返回地址 继续执行下面的语句 ;
    • ( 3 ) 栈 保存 数据 : 局部变量保存在栈内存中 ;
    • ( 4 ) 栈 保存 函数调用的上下文 : 栈中保存几个地址, 包括 返回地址, old ebp 地址, esp指向栈顶地址 ;
  • 2.栈是高级语言必须的 : 如果没有栈, 那么就没有函数, 程序则回退到汇编代码的样子, 程序从头执行到尾 ;

函数 栈内存 的几个相关概念 :

  • 1.esp 指针 : esp 指针变量所在的地址不重要, 讲解的全程没有涉及到过, 重要的是 esp 指向的值, 这个值随着 函数 入栈 出栈 一直的变 ;
    • ( 1 ) 入栈 : esp 上次指向的地址 放入 返回地址 中, 然后 esp 指向新的栈顶 ;
    • ( 2 ) 出栈 : 获取 返回地址 中的地址, esp 指向 该获取的地址 (获取方式 通过 ebp 指针获取);
  • 2.ebp 指针 : ebp 指针变量所在的地址不重要, 讲解全过程中没有涉及到, 重要的是 ebp 指向的值, 这个是随着 函数 入栈 出栈 一直在变 ;
    • ( 1 ) 入栈 : 将 ebp 指针指向的地址 入栈, 并且 ebp 指向新的栈内存地址 ;
    • ( 2 ) 出栈 : ebp 回退一个指针即可获取 返回地址 (这个返回地址供 esp 指针使用), 然后 ebp 获取内存中的地址, 然后ebp 直接指向这个地址, 即回退到上一个函数的ebp地址;
  • 3.返回地址作用 : 指引 esp 指针回退到上一个函数的栈顶 ;
  • 4.ebp 地址作用 : 指引 ebp 指针会退到上一个函数的 ebp 地址, 获取 esp 的返回地址 ;
  • 5.初始地址 : 最初的 返回地址 和 old ebp 地址值 是 栈底地址 ;

函数入栈流程 :

  • 1.参数入栈 : 函数的参数 存放到栈内存中 ;
  • 2.返回地址 入栈 : 每个函数都有一个返回地址, 这个返回地址是当前 esp 指针指向的地址, 即上一个函数的栈顶, 当出栈时 esp 还要指向这个地址用于释放被弹出的函数占用的栈空间 ;
  • 3.old esp 入栈 : old esp 是上一个 esp 指针指向的地址, 将这个地址存入栈内存中, 并且 esp 指针指向这个栈内存的首地址 ( 这个栈内存是存放 old esp 的栈内存 ) ;
  • 4.数据入栈 : 寄存器 和 局部变量数据 入栈 ;
  • 5.esp指向栈顶 : esp 指针指向当前的栈顶 ;
    【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第2张图片

函数出栈流程 :

  • 1.esp 指针返回 : 根据 ebp 指针 获取 返回地址, esp 直接指向这个返回地址 ;
    • ebp 获取 返回地方方式 : ebp 指向返回地址的下一个指针, ebp 指针回退一个指针 即可获取 返回地址 的指针, 然后获取指针指向的内容 即返回地址 ;
  • 2.ebp 指针返回 : 获取 ebp 指针指向的内存中的数据, 这个数据就是上一个ebp指向的内存地址值, ebp 指向这个地址值, 即完成操作 ;
  • 3.释放栈空间 : 随着 esp 和 ebp 指针返回, 栈空间也随之释放了 ;
  • 4.继续执行函数体 : 从函数2返回函数1后, 继续执行该函数1的函数体 ;
    【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第3张图片




(2) 代码示例 ( 简单的函数调用的栈内存分析 )


代码示例 :

  • 1.代码 :
#include 

void fun1(int i)
{
}

int fun2(int i)
{
	fun1();
	return i;
}

/*
	分析栈内存 入栈 出栈 esp ebp 指针操作; 

	程序开始执行, 目前 栈 中是空的, 栈底没有数据 ; 
	
	注意点 : 
	1. esp 指针 : esp 指针变量所在的地址不重要, 讲解的全程没有涉及到过, 重要的是 esp 指向的值, 这个值随着 函数 入栈 出栈 一直的变 ; 
		( 1 ) 入栈 : esp 上次指向的地址 放入 返回地址 中, 然后 esp 指向新的栈顶 ; 
		( 2 ) 出栈 : 获取 返回地址 中的地址, esp 指向 该获取的地址 (获取方式 通过 ebp 指针获取); 
	2. ebp 指针 : ebp 指针变量所在的地址不重要, 讲解全过程中没有涉及到, 重要的是 ebp 指向的值, 这个是随着 函数 入栈 出栈 一直在变 ;
		( 1 ) 入栈 : 将 ebp 指针指向的地址 入栈, 并且 ebp 指向新的栈内存地址 ; 
		( 2 ) 出栈 : ebp 回退一个指针即可获取 返回地址 (这个返回地址供 esp 指针使用), 然后 ebp 获取内存中的地址, 然后ebp 直接指向这个地址, 即回退到上一个函数的ebp地址;			 
	3. 返回地址作用 : 指引 esp 指针回退到上一个函数的栈顶 ; 
	4. ebp 地址作用 : 指引 ebp 指针会退到上一个函数的 ebp 地址, 获取 esp 的返回地址 ;  
	5. 初始地址 : 最初的 返回地址 和 old ebp 地址值 是 栈底地址 ; 
	
	1. main 函数执行
		( 1 ) 参数入栈 : 将 参数 放入栈中, 此时 main 函数 参数 在栈底 ;
		( 2 ) 返回地址入栈 : 然后将 返回地址 放入栈中, 返回地址是 栈底地址 ;
		( 3 ) ebp 指针入栈 : 将 old ebp 指针入栈, ebp 指针指向 old ebp 存放的地址 address1 , 这个 address1 是 栈底地址; 
		( 3 ) 数据入栈 :  ( 局部变量, 寄存器的值 等 ) ; 
		( 4 ) esp 指向栈顶 : esp 指针指向 栈顶 (即数据后面的内存首地址), 此时栈顶数据 address2;
		( 5 ) 数据总结 : main 的栈中 栈底 到 栈顶 的数据 : main 参数 -> 返回地址 -> old ebp -> 数据
		( 6 ) 执行函数体 : 开始执行 main 函数的函数体, 执行 fun1 函数, 下面是 栈 中内存变化 : 
		
	2. 调用 fun1 函数, 继续将 fun1 函数内容入栈 : 
		( 1 ) 参数入栈 : 将 fun1 参数 入栈 
		( 2 ) 返回地址入栈 : 存放一个返回地址, 此时存放的是栈顶的值 address2 地址, 返回的时候通过 ebp 指针回退一个读取 ;
		( 3 ) ebp 指针入栈 : old ebp (上次 ebp 指针指向的地址) 指针指向的地址值入栈, 该指针指向 address1 地址, 即 ebp 指针上一次指向的位置, 
				该栈内存中存放 ebp 指针上次指向的地址 address1, 这段存放 address1 的内存首地址为 address3,
				ebp 指针指向 address3 , 即 ebp 指针变量存储 address3 的地址值, 栈内存中的 address3 存放 address1 地址 ; 
		( 3 ) 数据入栈 : 存放数据 (局部变量) 
		( 4 ) esp 指向栈顶 : esp 指向 栈顶 
		( 5 ) 执行函数体 : 开始执行 fun1 函数体内容, 执行结束后需要出栈 返回 ;
		
	3. fun1 函数执行完毕, 开始 退栈 返回 操作 : 
		( 1 ) 获取返回地址 : 返回地址存放在 ebp 的上一个指针地址, ebp 指向 返回地址的尾地址, 
				ebp 回退一个指针位置即可获取返回地址 , 此时的返回地址是 address2 上面已经描述过了 ;
		( 2 ) esp 指针指向 : esp 指向 address2, 即将 esp 指针变量的值 设置为 address2 即可 ;
		( 3 ) ebp 指针指向 : 
					获取上一个 ebp 指向的地址 : 当前 ebp 指向的内存中存储了上一个 ebp 指向的内存地址, 获取这个地址;
					ebp 指向这个刚获取的地址 ; 
		( 4 ) 释放栈空间 : 将 esp 指针指向的当前地址 和 之后的地址 都释放掉 ; 
		
		( 5 ) 执行 main 函数体 : 继续执行 main 函数 函数体 , 然后执行 fun2 函数; 
		
		
	4. 执行 fun2 函数
		( 1 ) 参数入栈 : fun2 函数参数入栈; 
		( 2 ) 返回地址 入栈 : esp 指向的地址 存放到 返回地址中 ; 
		( 3 ) ebp 地址入栈 : 将 ebp 指向的地址存放到栈内存中, ebp 指向 该段内存的首地址 (即返回地址的尾地址);
		( 4 ) 数据入栈 : 将数据 入栈
		( 5 ) esp 指向栈顶 : esp 指向 数据 的末尾地址 ; 
		
		( 6 ) 执行函数体 : 执行 fun2 函数体时, 发现 fun2 中居然调用了 fun1, 此时又要开始将 fun1 函数入栈 ; 
		
		
	5. fun1 函数入栈
		( 1 ) 参数入栈 : 将 fun1 参数入栈
		( 2 ) 返回地址入栈 : esp 指向的 返回地址 存入栈内存 ; 
		( 3 ) ebp 地址入栈 : 将 old ebp 地址 入栈, 并且 ebp 指针指向 该段 栈内存首地址 (即 返回地址 的尾地址);
		( 4 ) 数据入栈 : 局部变量, 寄存器值 入栈 ;  
		( 5 ) esp 指针指向 : esp 指针指向栈顶 ; 
		
		( 6 ) 执行函数体 : 继续执行函数体, 执行完 fun1 函数之后, 函数执行完毕, 开始出栈操作 ; 
		
	6. fun1 函数 出栈
		( 1 ) esp 指针返回 : 通过 ebp 读取上一个指针, 获取 返回地址, esp 指向 返回地址, 即上一个栈顶 ; 
		( 2 ) ebp 指针返回 : 读取 ebp 指针指向的内存中的数据, 这个数据是上一个 ebp 指针指向的地址值, ebp 指向这个地址值; 
		( 3 ) 释放栈空间 : 执行完这两个操作后, 栈空间就释放了 ; 
		
		( 4 ) 执行函数体 : 执行完 fun1 出栈后, 继续执行 fun2 中的函数体, 发现 fun2 函数体也执行完了, 开始 fun2 出栈 ; 
		
	7. fun2 函数 出栈 
		( 1 ) esp 指针返回 : 通过 ebp 读取上一个指针, 获取 返回地址, esp 指向 返回地址, 即上一个栈顶 ; 
		( 2 ) ebp 指针返回 : 读取 ebp 指针指向的内存中的数据, 这个数据是上一个 ebp 指针指向的地址值, ebp 指向这个地址值; 
		( 3 ) 释放栈空间 : 执行完这两个操作后, 栈空间就释放了 ; 
		
		( 4 ) 执行函数体 : 执行完 fun2 出栈后, 继续执行 main 中的函数体, 如果 main 函数执行完毕, esp 和 ebp 都指向 栈底 ; 
	
*/
int main()
{
	fun1(1);
	fun2(1);
	
	return 0;
}
  • 2.编译运行结果 : 没有输出结果, 编译通过 ;



( 3 ) 栈内存行为分析 ( 图文分析版本 )


分析的代码内容 :

#include 

void fun1(int i)
{
}

int fun2(int i)
{
	fun1();
	return i;
}

int main()
{
	fun1(1);
	fun2(1);
	
	return 0;
}

代码 栈内存 行为操作 图示分析 :

  • 1.main 函数执行 :
    • ( 1 ) 参数入栈 : 将 参数 放入栈中, 此时 main 函数 参数 在栈底 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第4张图片
    • ( 2 ) 返回地址入栈 : 然后将 返回地址 放入栈中, 返回地址是 栈底地址 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第5张图片
    • ( 3 ) ebp 指针入栈 : 将 old ebp 指针入栈, ebp 指针指向 old ebp 存放的地址 address1 , 这个 address1 是 栈底地址;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第6张图片
    • ( 4 ) 数据入栈 : ( 局部变量, 寄存器的值 等 ) ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第7张图片
    • ( 5 ) esp 指向栈顶 : esp 指针指向 栈顶 (即数据后面的内存首地址), 此时栈顶数据 address2;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第8张图片
    • ( 6 ) 数据总结 : main 的栈中 栈底 到 栈顶 的数据 : main 参数 -> 返回地址 -> old ebp -> 数据
    • ( 7 ) 执行函数体 : 开始执行 main 函数的函数体, 执行 fun1 函数, 下面是 栈 中内存变化 :
int main()
{
	fun1(1);
	fun2(1);
	
	return 0;
}
  • 2.调用 fun1 函数, 继续将 fun1 函数内容入栈 :
    • ( 1 ) 参数入栈 : 将 fun1 参数 入栈 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第9张图片
    • ( 2 ) 返回地址入栈 : 存放一个返回地址, 此时存放的是栈顶的值 address2 地址, 返回的时候通过 ebp 指针回退一个读取 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第10张图片
    • ( 3 ) ebp 指针入栈 : old ebp (上次 ebp 指针指向的地址) 指针指向的地址值入栈, 该指针指向 address1 地址, 即 ebp 指针上一次指向的位置,
      该栈内存中存放 ebp 指针上次指向的地址 address1, 这段存放 address1 的内存首地址为 address3,
      ebp 指针指向 address3 , 即 ebp 指针变量存储 address3 的地址值, 栈内存中的 address3 存放 address1 地址 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第11张图片
    • ( 4 ) 数据入栈 : 存放数据 (局部变量) ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第12张图片
    • ( 5 ) esp 指向栈顶 : esp 指向 栈顶 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第13张图片
    • ( 6 ) 执行函数体 : 开始执行 fun1 函数体内容, 执行结束后需要出栈 返回 ;
void fun1(int i)
{
}
  • 3.fun1 函数执行完毕, 开始 退栈 返回 操作 :
    • ( 1 ) 获取返回地址 : 返回地址存放在 ebp 的上一个指针地址, ebp 指向 返回地址的尾地址,
      ebp 回退一个指针位置即可获取返回地址 , 此时的返回地址是 address2 上面已经描述过了 ;
    • ( 2 ) esp 指针指向 : esp 指向 address2, 即将 esp 指针变量的值 设置为 address2 即可 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第14张图片
    • ( 3 ) ebp 指针指向 :
      获取上一个 ebp 指向的地址 : 当前 ebp 指向的内存中存储了上一个 ebp 指向的内存地址, 获取这个地址;
      ebp 指向这个刚获取的地址 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第15张图片
    • ( 4 ) 释放栈空间 : 将 esp 指针指向的当前地址 和 之后的地址 都释放掉 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第16张图片
    • ( 5 ) 执行 main 函数体 : 继续执行 main 函数 函数体 , 然后执行 fun2 函数;
int main()
{
	fun1(1);
	fun2(1);
	
	return 0;
}
  • 4.执行 fun2 函数 :
    • ( 1 ) 参数入栈 : fun2 函数参数入栈;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第17张图片
    • ( 2 ) 返回地址 入栈 : esp 指向的地址 存放到 返回地址中 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第18张图片
    • ( 3 ) ebp 地址入栈 : 将 ebp 指向的地址存放到栈内存中, ebp 指向 该段内存的首地址 (即返回地址的尾地址);
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第19张图片
    • ( 4 ) 数据入栈 : 将数据 入栈 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第20张图片
    • ( 5 ) esp 指向栈顶 : esp 指向 数据 的末尾地址 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第21张图片
    • ( 6 ) 执行函数体 : 执行 fun2 函数体时, 发现 fun2 中居然调用了 fun1, 此时又要开始将 fun1 函数入栈 ;
int fun2(int i)
{
	fun1();
	return i;
}
  • 5.fun1 函数入栈 :
    • ( 1 ) 参数入栈 : 将 fun1 参数入栈 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第22张图片
    • ( 2 ) 返回地址入栈 : esp 指向的 返回地址 存入栈内存 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第23张图片
    • ( 3 ) ebp 地址入栈 : 将 old ebp 地址 入栈, 并且 ebp 指针指向 该段 栈内存首地址 (即 返回地址 的尾地址);
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第24张图片
    • ( 4 ) 数据入栈 : 局部变量, 寄存器值 入栈 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第25张图片
    • ( 5 ) esp 指针指向 : esp 指针指向栈顶 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第26张图片
    • ( 6 ) 执行函数体 : 继续执行函数体, 执行完 fun1 函数之后, 函数执行完毕, 开始出栈操作 ;
void fun1(int i)
{
}
  • 6.fun1 函数 出栈 :
    • ( 1 ) esp 指针返回 : 通过 ebp 读取上一个指针, 获取 返回地址, esp 指向 返回地址, 即上一个栈顶 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第27张图片
    • ( 2 ) ebp 指针返回 : 读取 ebp 指针指向的内存中的数据, 这个数据是上一个 ebp 指针指向的地址值, ebp 指向这个地址值;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第28张图片
    • ( 3 ) 释放栈空间 : 执行完这两个操作后, 栈空间就释放了 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第29张图片
    • ( 4 ) 执行函数体 : 执行完 fun1 出栈后, 继续执行 fun2 中的函数体, 发现 fun2 函数体也执行完了, 开始 fun2 出栈 ;
int fun2(int i)
{
	fun1();
	return i;
}
  • 7.fun2 函数 出栈 :
    • ( 1 ) esp 指针返回 : 通过 ebp 读取上一个指针, 获取 返回地址, esp 指向 返回地址, 即上一个栈顶 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第30张图片
    • ( 2 ) ebp 指针返回 : 读取 ebp 指针指向的内存中的数据, 这个数据是上一个 ebp 指针指向的地址值, ebp 指向这个地址值;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第31张图片
    • ( 3 ) 释放栈空间 : 执行完这两个操作后, 栈空间就释放了 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第32张图片
    • ( 4 ) 执行函数体 : 执行完 fun2 出栈后, 继续执行 main 中的函数体, 如果 main 函数执行完毕, esp 和 ebp 都指向 栈底 ;
      【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第33张图片




2. 堆



( 1 ) 标题3


堆 相关 概念 :

  • 1.栈的特性 : 函数执行开始时入栈, 在函数执行完毕后, 函数栈要释放掉, 因此函数栈内的部分类型数据无法传递到函数外部 ;
  • 2.堆 空间 : malloc 动态申请内存空间, 申请的空间是操作系统预留的一块内存, 这块内存就是堆 , 程序可以自由使用这块内存 ;
  • 3.堆 有效期 : 堆空间 从申请获得开始生效, 在程序主动释放前都是有效的, 程序释放后, 堆空间不可用 ;

堆 管理 方法 :

  • 1.空闲链表法 ;
  • 2.位图法 ;
  • 3.对象池法 ;

空闲链表法方案 :

  • 1.空闲链表图示 : 表头 -> 列表项 -> NULL ;
    【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第34张图片

  • 2.程序申请堆内存 : int* p = (int*)malloc(sizeof(int)) ; 申请一个 4 字节的堆空间, 从空闲链表中查找能满足要求的空间, 发现一个 5 字节的空间, 满足要求, 这里直接将 5 字节的空间, 分配给了程序 , 不一定要分配正好的内存给程序, 可能分配的内存比申请的要大一些 ;
    【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第35张图片

  • 3.程序释放堆内存 : 将 p 指向的内存插入到空闲链表中 ;





3. 静态存储区



( 1 ) 标题3


静态存储区 相关概念 :

  • 1.静态存储区 内容 : 静态存储区用于存储程序的静态局部变量 和 全局变量 ;
  • 2.静态存储区大小 : 在程序编译阶段就可以确定静态存储区大小了, 将静态局部变量和全部变量 的大小相加即可 ;
  • 3.静态存储区 生命周期 : 程序开始运行时分配静态存储区, 程序运行结束后释放静态存储区 ;
  • 4.静态局部变量 : 静态局部变量在程序运行过程中, 会一直保存着 ;

总结 :
1.栈内存 : 主要存储函数调用相关信息 ;
2.堆内存 : 用于程序申请动态内存, 归还动态内存使用 ;
3.静态存储区 : 用于保存程序中的 全局变量 和 静态局部变量 ;





三. 程序内存布局




1. 程序运行前的程序文件的布局 ( 代码段 | 数据段 | bss段 )



(1) 相关概念简介


可执行程序文件的内容 : 三个段 是程序文件的信息, 编译后确定 ;

  • 1.文本段 ( .text section ) : 存放代码内容, 编译时就确定了, 只能读, 不能写 ;
  • 2.数据段 ( .data section ) : 存放 已经初始化的 静态局部变量 和 全局变量, 编译阶段确定, 可读写 ;
  • 3.BSS段 ( .bss section ) : 存放 没有初始化的 静态局部变量 和 全局变量, 可读写 , 程序开始执行的时候 初始化为 0 ;



( 2 ) 分析程序文件的内存布局


分析简单程序的 程序文件布局 :

  • 1.示例代码 :
#include 

//1. 全局的 int 类型变量, 并且进行了初始化, 存放在 数据段
int global_int = 666;
//2. 全局 char 类型变量, 没有进行初始化, 存放在 bss段
char global_char;

//3. fun1 和 fun2 函数存放在文本段
void fun1(int i)
{
}

int fun2(int i)
{
	fun1();
	return i;
}

int main()
{
	//4. 静态局部变量, 并且已经初始化过, 存放在 数据段;
	static int static_part_int = 888;
	//5. 静态局部变量, 没有进行初始化, 存放在 bss段;
	static char static_part_char;
	
	//6. 局部变量存放到文本段
	int part_int = 999;
	char part_char;

	//7. 函数语句等内容存放在文本段
	fun1(1);
	fun2(1);
	
	return 0;
}
  • 2.代码分析图示 :
    【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第36张图片




2. 程序运行后的内存布局 ( 栈 | 堆 | 映射文件数据 [ bss段 | data段 | text段 ] )



( 1 ) 相关概念简介


程序运行后的内存布局 : 从高地址 到 低地址 介绍, 顺序为 栈 -> 堆 -> bss段 -> data 段 -> text段 ;

  • 1.栈 : 程序运行后才分配栈内存, 存放程序的函数信息 ;
  • 2.堆 : 分配完栈内存后分配堆内存, 用于响应程序的动态内存申请 ;
  • 3.bss 段 : 从程序文件映射到内存空间中 , 存放 没有初始化的 静态局部变量 和 全局变量, 其值自动初始化为 0 ;
  • 4.data 段 : 从程序文件映射到内存空间中 , 存放 已经初始化过的 静态局部变量 和 全局变量 ;
  • 5.text 段 : 从程序文件映射到内存空间中 , 存放编写的程序代码 ;
  • 6.rodata 段 : 存放程序中的常量信息 , 只能读取, 不能修改, 如 字符串常量, 整形常量 等信息 , 如果强行修改该段的值, 在执行时会报段错误 ;




3. 总结


程序内存总结 :

  • 1.静态存储区 : .bss 段 和 .data 段 是静态存储区 ;
  • 2.只读存储区 : .rodata 段存放常量, 是只读存储区 ;
  • 3.栈内存 : 局部变量存放在栈内存中 ;
  • 4.堆内存 : 使用 malloc 动态申请 堆内存 ;
  • 5.代码段 : 代码存放在 .text 段 中 , 函数的地址 是代码段中的地址 ;

函数调用过程 :

  • 1.函数地址 : 函数地址对应着程序内存空间中代码段的位置 ;
  • 2.函数栈 : 函数调用时, 会在栈内存中建立 函数调用的 活动记录, 如 参数 返回地址 old ebp地址 数据 等 ;
  • 3.相关资源访问 : 函数调用时, 在代码段的函数存放内存操作信息, 执行函数时, 会根据 esp 栈顶指针 查找函数的 局部变量等信息, 需要静态变量会从 bss 段 或 data段 查找信息, 需要常量值时 去 rodata 段去查找信息 ;




四. 野指针 ( 程序BUG根源 )




1. 野指针相关概念



( 1 ) 野指针简介


野指针相关概念 :

  • 1.野指针定义 : 野指针的 指针变量 存储的地址值值 不合法 ;
  • 2.指针合法指向 : 指针只能指向 栈 和 堆 中的地址, 除了这两种情况, 指针指向的其它地址都是不合法的 ;
  • 3.空指针 与 野指针 : 空指针不容易出错, 因为可以判断出来, 其指针地址为 0 ; 野指针指针地址 不为 0 , 但是其指向的内存不可用 ;
  • 4.野指针不可判定 : 目前 C 语言中 无法判断 指针 是否 为野指针 ;




( 2 ) 野指针的三大来源


野指针来源 :

  • 1.局部变量指针未初始化 : 局部指针变量, 定以后, 没有进行初始化 ;
#include 
#include 

//1. 定义一个结构体, 其中包含 字符串 和 int 类型元素
struct Student
{
	char* name;
	int age;
};

int main()
{
	//2. 声明一个 Student 结构体变量但是没有进行初始化, 
	//	结构体中的两个元素都是随机值
	//  需要 malloc 初始化该局部变量
	struct Student stu;
	
	//3. 向 stu.name 指针指向的地址 写入 "Bill Gates" 字符串, 
	//		要出事, stu.name 没有进行初始化, 其地址是随机值, 
	//		向一个随机地址中写入数据, 会出现任意情况, 严重会直接让系统故障
	//4. 此时 stu.name 就是一个野指针
	strcpy(stu.name, "Bill Gates");
	
	stu.age = 63;
	return 0;
}

【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第37张图片

  • 2.使用已经释放的指针 : 指针指向的内存控件, 已经被 free 释放了, 之后在使用就变成了野指针 ; 如果该指针没有分配, 写入无所谓; 如果该地址被分配给程序了, 随意修改该值会造成无法估计的后果;
#include 
#include 
#include 

int main()
{
	//1. 创建一个字符串, 并为其分配空间
	char* str = (char *)malloc(3);
	//2. 给字符串赋值, 申请了 3 个字节, 但是放入了 11 个字符
	//   有内存越界的风险
	strcpy(str, "HanShuliang");
	//3. 打印字符串
	printf("%s\n", str);
	//4. 释放字符串空间
	free(str);
	//5. 再次打印, 为空
	printf("%s\n", str);
}

【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第38张图片

  • 3.指针指向的变量在使用前被销毁 : 指针指向的变量如果被销毁, 这个变量所在的空间也可能被分配了, 修改该空间内的内容, 后果无法估计;
#include 

//从函数中返回的局部变量要注意一定要是值传递, 不能有地址传递
//局部变量在函数执行完就释放掉了

char * fun()
{
	//注意该变量是局部变量, 
	//函数执行完毕后该变量所在的栈空间就会被销毁
	char* str = "Hanshuliang";
	return str;
}

int main()
{
	//从 fun() 函数中返回的 str 的值是栈空间的值, 
	//该值在函数返回后就释放掉了, 
	//当前这个值是被已经销毁了
	char * str = fun();
	
	//打印出来的值可能是正确的
	printf("%s\n", str);
}

【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第39张图片





2. 经典指针错误分析 (本节所有代码都是错误示例)



( 1 ) 非法内存操作


非法内存操作 : 主要是**结构体的指针成员出现的问题, 如结 ① 构体指针未进行初始化(分配动态内存, 或者分配一个变量地址), 或者***② 进行了初始化, 但是超出范围使用***;

  • 1.结构体成员指针未初始化 : 结构体的成员中 如果有指针, 那么这个指针在使用时需要进行初始化, 结构体变量声明后, 其成员变量值是随机值, 如果指针值是随机值得话, 那么对该指针操作会产生未知后果; 错误示例 :
#include 

//在结构体中定义指针成员, 当结构体为局部变量时, 该指针成员 int* ages 需要手动初始化操作
struct Students
{
	int* ages;
};

int main()
{
	//1. 在函数中声明一个结构体局部变量, 结构体成员不会自动初始化, 此时其中是随机值 
	struct Students stu1;
	
	//2. 遍历结构体的指针成员, 并为其赋值, 但是该指针未进行初始化, 对一个随机空间进行操作会造成未知错误
	int i = 0;
	for(i = 0; i < 10; i ++)
	{
		stu1.ages[i] = 0;
	}
	
	return 0;
}
  • 2.结构体成员初始化内存不足 : 给结构体初始化时为其成员分配了空间, 但是使用的指针操作超出了分配的空间, 那么对于超出的空间的使用会造成无法估计的错误; 错误示例 :
#include 
#include 

//在结构体中定义指针成员, 当结构体为局部变量时, 该指针成员 int* ages 需要手动初始化操作
struct Students
{
	int* ages;
};

int main()
{
	//1. 在函数中声明一个结构体局部变量, 结构体成员不会自动初始化, 此时其中是随机值 
	struct Students stu1;
	
	//2. 为结构体变量中的 ages 指针分配内存空间, 并进行初始化;
	stu1.ages = (int *)calloc(2, sizeof(int));
	
	//3. 遍历结构体的指针成员, 并为其赋值, 此处超出了其 2 * 4 字节的范围, 8 ~ 11 字节可能分配给了其他应用
	int i = 0;
	for(i = 0; i < 3; i ++)
	{
		stu1.ages[i] = 0;
	}
	
	free(stu1.ages);
	
	return 0;
}




( 2 ) 内存申请成功后未初始化


内存分配成功, 没有进行初始化 : 内存中的是随机值, 如果对这个随机值进行操作, 也会产生未知后果;

#include 
#include 

//内存分配成功, 需要先进行初始化, 在使用这块内存

int main()
{
	//1. 定义一个字符串, 为其分配一个 20 字节空间
	char* str = (char*)malloc(20);
	
	//2. 打印字符串, 这里可能会出现错误, 因为内存没有初始化
	//   此时其中的数据都是随机值, 不确定在哪个地方有 '\0' 即字符串结尾
	//   打印一个位置长度的 str, 显然不符合我们的需求
	printf(str);
	
	//3. 释放内存
	free(str);
	
	return 0;
}



( 3 ) 内存越界


内存越界分析 :

#include 

//数组退化 : 方法中的数组参数会退化为指针, 即这个方法可以传入任意 int* 类型的数据
//不能确定数组大小 : 只有一个 int* 指针变量, 无法确定这个数组的大小 
//可能出错 : 这里按照10个字节处理数组, 如果传入一个 小于 10字节的数组, 可能出现错误 
void fun(int array[10])
{
	int i = 0;
	
	for(i = 0; i < 10; i ++)
	{
		array[i] = i;
		printf("%d\n", array[i]);
	}
}

int main()
{
	//1. 定义一个大小为 5 的int 类型数组, 稍后将该数组传入fun方法中
	int array[5];
	
	//2. 将大小为5的int类型数组传入fun函数, 此时fun函数按照int[10]类型超出范围为数组赋值
	//	 如果为一个未知地址赋值会出现无法估计的后果
	fun(array);
	
	return 0;
}



( 4 ) 内存泄露


内存泄露 :

  • 1.错误示例 :
#include 

/*
	内存问题 : 该函数有一个入口, 两个出口
			   正常出口 : 处理的比较完善, 内存会释放;
			   异常出口 : 临时机制, 出现某种状态, 没有处理完善, 出现了内存泄露

*/
void fun(unsigned int size)
{
	//申请一块内存空间
	int* p = (int*)malloc(size * sizeof(int));
	int i = 0;
	
	//如果size小于5, 就不处理, 直接返回
	//注意 : 在这个位置, 善后没有处理好, 没有释放内存
	//		 如果size小于5, 临时退出函数, 而 p 指针指向的内存没有释放
	//		 p 指针是一个局部变量, 函数执行完之后, 该局部变量就消失了, 之后就无法释放该内存了
	if(size < 5)
	{
		return;
	}
	
	//内存大于等于5以后才处理
	for(int i = 0; i < size; i ++)
	{
		p[i] = i;
		printf("%d\n", p[i]);
	}
	
	//释放内存
	free(p);
}

int main()
{
	fun(4);
	
	return 0;
}
  • 2.正确示例 :
#include 

/*
	内存问题 : 该函数有一个入口, 两个出口
			   正常出口 : 处理的比较完善, 内存会释放;
			   异常出口 : 临时机制, 出现某种状态, 没有处理完善, 出现了内存泄露

*/
void fun(unsigned int size)
{
	//申请一块内存空间
	int* p = (int*)malloc(size * sizeof(int));
	int i = 0;
	
	//将错误示例中的此处的出口取消即可解决内存泄露的问题
	if(size >= 5)
	{
		//内存大于等于5以后才处理
		for(int i = 0; i < size; i ++)
		{
			p[i] = i;
			printf("%d\n", p[i]);
		}
	}
	
	//释放内存
	free(p);
}

int main()
{
	fun(4);
	
	return 0;
}



( 5 ) 指针多次释放 (谁申请谁释放)


指针被多次释放 :

#include 
#include 

/*
	内存问题 : 多次释放指针
	
	如果规避这种问题 : 动态内存 谁申请 谁释放

*/
void fun(int* p, int size)
{
	int i = 0;
	
	for(i = 0; i < size; i ++)
	{
		p[i] = i;
		printf("%d\n", p[i]);
	}
	
	//释放内存
	//	注意这里 p 不是在本函数中申请的内存
	//	如果在其它位置再次释放内存, 就可能会出错
	free(p);
}

int main()
{
	//申请内存
	int* p = (int*)malloc(3 * sizeof(int));
	
	//使用内存, 并在函数中释放内存
	fun(p, 3);
	
	//如果在此处释放一个已经释放的内存, 就会报错
	free(p);
	
	return 0;
}

【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )_第40张图片




( 6 ) 使用已经释放的指针


使用已经释放的指针 :

#include 
#include 

/*
	内存问题 : 使用已经释放的指针
	
	如果规避这种问题 : 动态内存 谁申请 谁释放

*/
void fun(int* p, int size)
{
	int i = 0;
	
	for(i = 0; i < size; i ++)
	{
		p[i] = i;
		printf("%d\n", p[i]);
	}
	
	//释放内存
	//	注意这里 p 不是在本函数中申请的内存
	//	如果在其它位置再次释放内存, 就可能会出错
	free(p);
}

int main()
{
	//申请内存
	int* p = (int*)malloc(3 * sizeof(int));
	int i = 0;
	
	//使用内存, 并在函数中释放内存
	fun(p, 3);
	
	//使用已经释放的指针
	//产生的后果无法估计
	for(i = 0; i <= 2; i ++)
	{
		p[i] = i;
	}
	
	return 0;
}




3. C语言中避免指针错误的编程规范



( 1 ) 申请内存后先判空


申请空间后先判断 : 使用 malloc 申请内存之后, 先检查返回值是否为 NULL, 防止使用 NULL 指针, 防止对 0 地址进行操作, 这样会破坏操作系统的内存区; 操作系统检测到程序使用 0 地址, 就会杀死本程序;

#include 
#include 

int main()
{
	//申请内存
	int* p = (int*)malloc(3 * sizeof(int));
	
	//申请完内存后, 先判断是否申请成功, 在使用这段内存
	if(p != NULL){
		//执行相关操作
	}
	
	//释放内存
	free(p);
	
	return 0;
}



( 2 ) 避免数组越界 注意数组长度


避免数组越界 : 数组创建后, 一定要记住数组的长度, 防止数组越界, 推荐使用柔性数组;




( 3 ) 动态内存 谁申请 谁释放


动态内存申请规范 : 动态内存的***申请操作*** 和 释放操作 一一对应匹配, 防止内存泄露和多次释放; 谁申请 谁 释放, 在哪个方法中申请, 就在哪个方法中释放 ;




( 4 ) 释放后立即置NULL


指针释放后立即设置NULL : 在一个指针被 free() 掉以后, 马上将该指针设置为 NULL, 防止重复使用该指针;


你可能感兴趣的:(C)