【内存管理大猫腻:从“越界”到“内存泄漏”应有尽有】

简介: 【内存管理大猫腻:从“越界”到“内存泄漏”应有尽有】

本章重点


  • 什么是动态内存
  • 为什么要有动态内存
  • 什么是野指针
  • 对应到C空间布局, malloc 在哪里申请空间
  • 常见的内存错误和对策
  • C中动态内存“管理”体现在哪


什么是动态内存


  • 动态内存是指在程序运行时,根据需要动态分配的内存空间。
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#define N 10
int main()
{
  int* p = (int*)malloc(sizeof(int) * N); //动态开辟空间40个字节的空间
  if (NULL == p) { //判断是否成功申请空间
    perror("malloc\n");
    //perror函数是标准库函数,其作用是输出上一个系统调用(例如malloc)的错误信息
    exit(0);
    //exit函数结束程序运行
  }
  for (int i = 0; i < N; i++) {
    p[i] = i;//赋值0,1,2,3,4,5,6,7,8,9
  }
  for (int i = 0; i < N; i++) {
    printf("%d ", i);//打印
  }
  printf("\n");
  free(p); //开辟完之后,要程序员自主释放
  p = NULL;
  return 0;
}



为什么要有动态内存


  • 通常,在编写程序时,我们可以使用静态内存(静态分配内存),也就是在程序编译阶段就确定了内存空间的大小和位置,但是静态内存存在一定的限制和局限性,比如无法在运行时改变分配的内存大小等。
  • 而动态内存则具有更大的灵活性和可变性,可以在程序运行时动态地分配、释放内存,以适应程序的实际需求。



栈、堆和静态区


C程序动态地址空间分布


#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int g_val2;//未初始化变量
int g_val1 = 1;//已初始化变量
int main()
{
  printf("code addr: %p\n", main);//代码区
  const char* str = "hello world";//字符常量区
  printf("static readonly: %p\n", str);//这里输出的是字符串的首地址
  printf("init(初始化) global val: %p\n", &g_val1);//已初始化变量区
  printf("uninit(未初始化) global val: %p\n", &g_val2);//未初始化变量区
  int* p = (int*)malloc(sizeof(int) * 10);
  printf("heap(堆) : %p\n", p);//这里输出的是开辟40个字节空间的首地址
  //输出两个局部指针变量的地址
  printf("stack(栈) addr: %p\n", &str);
  printf("stack(栈) addr: %p\n", &p);
}


由于win中有地址随机化保护,我们这里的结果是再Linux中验证的



同时我们也可以发现栈是向下增长的,后定义的变量后入栈,其相应的地址也较小。


再来验证一下堆区的特点。


char* p1 = (char*)malloc(sizeof(char) * 10); 
printf("heap(堆) : %p\n", p1);
char* p2 = (char*)malloc(sizeof(char) * 10); 
printf("heap(堆) : %p\n", p2);
char* p3 = (char*)malloc(sizeof(char) * 10); 
printf("heap(堆) : %p\n", p3);
printf("stack(栈) addr: %p\n", &p1);
printf("stack(栈) addr: %p\n", &p2);
printf("stack(栈) addr: %p\n", &p3);



堆是符合向上增长的,先开辟的变量,其相应的地址较小。


在C语言中,为何一个临时变量,使用static修饰之后,它的生命周期变成全局的了?



    当在一个函数中将一个局部变量添加了static关键字时,编译器会将其转化为对应的静态变量,这使得该变量的存储位置从栈(stack)转变为全局数据区(data segment)中的静态变量存储区,使得该变量在函数调用结束后不会被自动销毁。


常见的内存错误及对策


ONE:指针没有指向一块合法的内存


1、结构体成员指针未初始化



2、没有为结构体指针分配足够的内存



3、函数的入口检测



TWO:为指针分配的内存太小



THREE:内存分配成功,但并未初始化



FOUR:内存越界



FIVE:内存泄漏


  • 申请内存是在哪里申请?- 堆
  • 申请内存是向谁要空间?- 操作系统
  • 如何申请内存? - malloc函数
  • 申请内存是否需要释放?如何释放? - 需要,free函数
  • 申请内存不释放会有什么问题? - 内存泄露


程序退出的时候,曾经的内存泄漏问题还存在吗?



内存释放的本质是什么?



观察free函数的参数,free函数只知道释放空间的起始地址,貌似并不知道要释放多大空间,那如何正确释放呢?



我们这里写一个单链表代码来演示动态开辟内存


#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <windows.h>
#define N 10
typedef struct _Node {
  int data;
  struct _Node* next;
}node_t;
static node_t* AllocNode(int x)
{
  node_t* n = (node_t*)malloc(sizeof(node_t));
  if (NULL == n) {
    exit(EXIT_FAILURE);
  }
  n->data = x;
  n->next = NULL;
  return n;
}
void InsertList(node_t* head, int x)
{
  node_t* end = head;
  while (end->next) {
    end = end->next;
}
node_t* n = AllocNode(x);
end->next = n;
}
void ShowList(node_t* head)
{
  node_t* p = head->next;
  while (p) {
    printf("%d ", p->data);
    p = p->next;
  }
  printf("\n");
}
void DeleteList(node_t* head)
{
  node_t* n = head->next;
  if (n != NULL) {
    head->next = n->next;
    free(n);
  }
}
int main()
{
  node_t* head = AllocNode(0); //方便操作,使用带头结点的单链表
  printf("插入演示...\n");
  Sleep(10000);
  for (int i = 1; i <= N; i++) {
    InsertList(head, i); //插入一个节点,尾插方案
    ShowList(head); //显示整张链表
    Sleep(1000);
  }
  printf("删除演示...\n");
  for (int i = 1; i <= N; i++) {
    DeleteList(head); //删除一个节点,头删方案
    ShowList(head); //显示整张链表
    Sleep(1000);
  }
  free(head); //释放头结点
  head = NULL;
  return 0;
}



SIX:内存已经被释放了,但是继续通过指针来试用


 

C中动态内存“管理”体现在哪


相关文章
|
2月前
|
缓存 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
29 1
|
2月前
|
存储 设计模式 监控
运用Unity Profiler定位内存泄漏并实施对象池管理优化内存使用
【7月更文第10天】在Unity游戏开发中,内存管理是至关重要的一个环节。内存泄漏不仅会导致游戏运行缓慢、卡顿,严重时甚至会引发崩溃。Unity Profiler作为一个强大的性能分析工具,能够帮助开发者深入理解应用程序的内存使用情况,从而定位并解决内存泄漏问题。同时,通过实施对象池管理策略,可以显著优化内存使用,提高游戏性能。本文将结合代码示例,详细介绍如何利用Unity Profiler定位内存泄漏,并实施对象池来优化内存使用。
106 0
|
5天前
|
Arthas 监控 Java
监控线程池的内存使用情况以预防内存泄漏
监控线程池的内存使用情况以预防内存泄漏
|
10天前
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。
|
10天前
|
存储 并行计算 算法
CUDA统一内存:简化GPU编程的内存管理
在GPU编程中,内存管理是关键挑战之一。NVIDIA CUDA 6.0引入了统一内存,简化了CPU与GPU之间的数据传输。统一内存允许在单个地址空间内分配可被两者访问的内存,自动迁移数据,从而简化内存管理、提高性能并增强代码可扩展性。本文将详细介绍统一内存的工作原理、优势及其使用方法,帮助开发者更高效地开发CUDA应用程序。
|
1月前
|
存储 Java 程序员
JVM自动内存管理之运行时内存区
这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
JVM自动内存管理之运行时内存区
|
27天前
|
Linux 测试技术 C++
内存管理优化:内存泄漏检测与预防。
内存管理优化:内存泄漏检测与预防。
34 2
|
5天前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
17 0
|
2月前
|
Arthas 存储 监控
JVM内存问题之JNI内存泄漏没有关联的异常类型吗
JVM内存问题之JNI内存泄漏没有关联的异常类型吗
|
1月前
|
搜索推荐 Java API
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
38 0

热门文章

最新文章