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

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

本章重点


  • 什么是动态内存
  • 为什么要有动态内存
  • 什么是野指针
  • 对应到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中动态内存“管理”体现在哪


相关文章
|
1月前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
39 8
|
2月前
|
容器
在使用指针数组进行动态内存分配时,如何避免内存泄漏
在使用指针数组进行动态内存分配时,避免内存泄漏的关键在于确保每个分配的内存块都能被正确释放。具体做法包括:1. 分配后立即检查是否成功;2. 使用完成后及时释放内存;3. 避免重复释放同一内存地址;4. 尽量使用智能指针或容器类管理内存。
|
2月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
53 6
|
2月前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
351 9
|
3月前
|
存储 安全 程序员
内存越界写入
【10月更文挑战第13天】
55 4
|
3月前
|
Rust 安全 Java
内存数组越界
【10月更文挑战第14天】
37 1
|
3月前
|
Java 编译器 C++
内存越界读取
【10月更文挑战第13天】
57 2
|
3月前
|
存储 容器
内存越界访问(Out-of-Bounds Access)
【10月更文挑战第12天】
311 2
|
3月前
|
存储 程序员 编译器
C语言——动态内存管理与内存操作函数
C语言——动态内存管理与内存操作函数
|
3月前
|
存储 缓存 监控
深入了解MySQL内存管理:如何查看MySQL使用的内存
深入了解MySQL内存管理:如何查看MySQL使用的内存
467 1

热门文章

最新文章