动态(堆区)内存管理与内存泄漏规避

个人主页流年如夢

专栏《C语言》

文章目录

Ladies and gentlemen,本篇文章讲的是动态内存管理,下面将带领大家学习 malloc、free、calloc、realloc四大函数、动态内存常见错误、柔性数组与程序内存区域划分 ;全程高能,不容错过!!!

前言

在C语言中,普通变量与数组在栈区 开辟空间,大小固定、无法更改。而实际开发中,常常需要在运行时决定内存大小 、能够自由扩容与释放的空间。为此,C语言提供了动态内存管理机制 ,允许程序猿手动在堆区申请、使用、释放内存。本篇文章将带大家从基础用法到底层坑点全覆盖,彻底避开内存泄漏、野指针、越界等致命错误。

一.动态内存分配

1.1为什么需要动态内存分配

相对于我们平常用的内存开辟方式而言,动态内存的优势在于:

  1. 运行时按需申请、按需释放
  2. 空间大小可随时扩容或缩容
  3. 存放在堆区,空间充足

它弥补了空间大小固定不可变,例如:

c 复制代码
int val = 20;//栈上开辟4字节
char arr[10] = {0};//数组长度栈上开辟10字节

二.malloc与free

2.1malloc(函数原型、功能)

头文件为<stdlib.h>
函数原型

c 复制代码
void* malloc(size_t size);

功能

1.在堆区申请连续内存空间

  1. 如果开辟成功,返回起始地址;如果开辟失败,返回NULL

  2. 返回·void*·,由自己决定类型

4 .内存不初始化,内容随机

2.2free(函数原型、功能)

函数原型

c 复制代码
void free(void* ptr);

功能:

  1. 释放或回收动态开辟的内存
  2. 如果ptrNULL,则什么都不做;如果ptr不是动态开辟,则行为未定义

2.3举例

c 复制代码
#include <stdio.h>
#include <stdlib.h>//头文件不能少了

int main()
{
    int num = 0;
    scanf("%d", &num);
    int* ptr = (int*)malloc(num * sizeof(int));//申请num个int空间
    if (ptr != NULL)//检查是否开辟成功
    {
        for (int i = 0; i < num; i++)
        {
            *(ptr + i) = 0;
        }
    }
    
    free(ptr);
    ptr = NULL;

    return 0;
}

🧐分析 :先申请numint空间,再检查是否开辟成功(ptr != NULL),最后free释放后手动将指针置NULL,避免野指针

三.calloc与realloc

3.1calloc(函数原型、功能)

函数原型

c 复制代码
void* calloc(size_t num, size_t size);

功能

  1. num个大小为size的元素开辟空间
  2. 自动把内存初始化为0
  3. malloc唯一区别就是calloc会初始化

举个例子

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* p = (int*)calloc(10, sizeof(int));

    if (p != NULL)
    {
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", *(p + i));
        }
    }

    free(p);
    p = NULL;
    
    return 0;
}

🧐分析:先申请10个int,并全部初始化为0

运行结果

3.2realloc(函数原型、功能)

函数原型

c 复制代码
void* realloc(void* ptr, size_t size);

功能

  1. 对已动态开辟的内存重新调整大小
  2. ptr --> 要调整的内存地址
  3. size --> 调整后的新大小(字节)
  4. 返回调整后的起始地址

我们知道realloc是用来对已动态开辟的内存重新调整大小的,会有两种情况

  1. 原有空间后有足够空间,直接向后扩容
  2. 原有空间后无空间,重新找整块空间,拷贝数据后返回新地址

如何使用realloc,如下所示:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int* ptr = (int*)malloc(100);
    if (ptr == NULL)
    {
        perror("malloc");
        return 1;
    }
    int* tmp = (int*)realloc(ptr, 1000);//<<=扩容,由100->1000
    if (tmp != NULL)
    {
        ptr = tmp;
    }

    //...

    free(ptr);
    ptr = NULL;

    return 0;
}

🧐分析绝对不能直接用原指针(ptr)接收realloc返回值 ,并且申请失败返回NULL,会导致原地址丢失,造成内存泄漏,应该用临时变量(tmp)在判断成功后再赋值

四.常见的动态内存错误

4.1对NULL(空指针)解引用

c 复制代码
void test()
{
    int* p = (int*)malloc(INT_MAX / 4);
    *p = 20;
    free(p);
}

🧐分析malloc申请过大内存,极易开辟失败,返回 NULL;上面代码没有判断指针是否为空 ,直接对NULL解引用;程序会直接崩溃,出现段错误;所以必须先判断 p != NULL 再使用

4.2动态空间越界访问

c 复制代码
void test()
{
    int* p = (int*)malloc(10 * sizeof(int));
    for (int i = 0; i <= 10; i++)
    {
        *(p + i) = i;
    }
    free(p);
}

🧐分析 :申请了10int空间,在for循环里最后i=10,但我们的下标最大只有 9,超出申请范围,导致内存越界;所以我们应该将i<=10改成 i<10即可

4.3对非动态内存free

c 复制代码
void test()
{
    int a = 10;
    int* p = &a;
    free(p);
}

🧐分析 :变量a是局部变量,是在栈区,然而free只能释放堆区的动态内存 (如malloc、calloc、realloc );我们知道,栈内存由系统自动释放,用不上free

4.4只释放一部分动态内存

c 复制代码
void test()
{
    int* p = (int*)malloc(100);
    p++;
    free(p);
}

🧐分析p++后,指针不再指向动态内存的起始地址;free只能释放申请时返回的起始地址;此时调用free,导致非法释放;所以不能移动原指针或者用临时指针遍历

4.5重复释放同一块内存

c 复制代码
void test()
{
    int* p = (int*)malloc(100);
    free(p);
    free(p);
}

🧐分析 :已经free过一次了,再free一次是属于重复释放 ,会导致堆破坏,程序崩溃;free过后应该让p成为空指针更安全 ,即p=NULL

4.6忘记释放导致内存泄漏

c 复制代码
void test()
{
    int* p = (int*)malloc(100);
}

🧐分析当函数使用完后指针p会被销毁,但依然会占用堆内存 ,从而造成内存泄漏,长期运行会耗尽内存;我们应该在不使用的时候让指针free ,即free(p)

五.柔性数组

C99允许结构体最后一个成员是未知大小的数组 ,被称为柔性数组

5.1声明

c 复制代码
struct st_type
{
    int i;
    int a[];//<--这个便是柔性数组成员(举例)
};

5.2特点

如下:

  1. 前面至少有一个成员(例如上面int a[]前面有一个int i
  2. sizeof 不计算柔性数组大小
  3. 必须用 malloc 动态分配空间

5.3如何使用

举例(分配结构体再加上100个int空间):

c 复制代码
typedef struct st_type
{
    int i;
    int a[];
} type_a;

int main()
{
    type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
    p->i = 100;
    for (int i = 0; i < 100; i++)
    {
        p->a[i] = i;
    }
    free(p);
    p = NULL;
    
    return 0;
}

🧐分析 :在这里只需一次 free,方便释放 ;内存连续,访问更快、碎片更少

六.C/C++程序内存区域划分(栈区、堆区、静态区)

区域名称 存放内容 管理方式 特点
内核空间 系统内核、驱动 操作系统 用户不可读写
栈区 局部变量、函数参数、返回地址 自动创建、自动销毁 空间小、速度快、地址向下增长
内存映射段 文件映射、动态库 操作系统 用于共享库、文件映射
堆区 malloc、calloc、realloc 动态内存 手动申请、手动释放 空间大、灵活、易产生内存碎片、地址向上增长
静态区(数据段) 全局变量、static变量 程序结束释放 生命周期贯穿整个程序
代码段 执行代码、只读常量 操作系统 只读、不可修改

相关截图如下所示:

🎯总结

  1. 动态内存函数 malloccallocreallocfree,都在头文件 <stdlib.h>
  2. malloc申请不初始化;calloc申请并初始化为 0
  3. realloc 扩容必须用临时变量接收,防止丢失地址
  4. 必须检查返回值是否为 NULL(空指针),避免空指针崩溃
  5. 动态内存常见错误:空指针、越界、非法 free、重复 free、内存泄漏
  6. 柔性数组方便释放、内存连续,是结构体动态扩容的最佳方案
  7. 栈区自动管理,堆区手动管理,分清区域避免野指针

⚠️易错点

  1. 不检查 malloc 返回值,直接使用导致崩溃
  2. realloc 直接赋值给原指针,造成内存泄漏
  3. free 后不置空,产生野指针
  4. 栈空间地址返回给上层使用,非法访问
  5. 传值调用想修改指针,导致无法分配成功
  6. 忘记释放动态内存,造成长期内存泄漏
  7. 柔性数组前面无成员、或用非动态方式创建

👀 关注 我们一路同行,从入门到大师,慢慢沉淀、稳步成长
❤️ 点赞 鼓励原创,让优质内容被更多人看见
⭐ 收藏 收好核心知识点与实战技巧,需要时随时查阅
💬 评论 分享你的疑问或踩坑经历,一起交流避坑、共同进步

相关推荐
三品吉他手会点灯2 小时前
C语言学习笔记 - 16.C编程预备计算机专业知识 - Hello World程序的运行原理
c语言·笔记·学习
流年如夢2 小时前
文件读写操作与易错点总结
c语言
c++圈来了个新人3 小时前
C++类和对象(中)
c语言·开发语言·数据结构·c++·考研·算法
bucenggaibian3 小时前
C语言如何直接控制硬件?指针、内存与寄存器
c语言·内存·指针·寄存器·硬件控制
2401_892070983 小时前
红黑树(RBTree):原理 + 5 大性质 + 旋转 + 插入 + 删除 + 完整工程级代码逐行解析
c语言·数据结构·红黑树
Lazionr3 小时前
【链表经典OJ-下】
c语言·数据结构·链表
CPUOS20103 小时前
嵌入式C语言高级编程之接口隔离原则
c语言·网络·接口隔离原则
sghuter3 小时前
HTML头部元信息避坑指南
c语言·前端·html·cocoa