C语言动态内存管理深度解析

 更新时间:2026年06月13日 09:41:46   作者:liandestiny  
本文详细介绍了C语言中动态内存管理的关键函数,如`malloc`、`free`、`realloc`等及柔性数组的概念与使用,强调了灵活可控的内存分配与释放的重要性,避免内存泄漏等常见错误,通过实例分析加深理解,并对比了柔性数组与指针成员的优缺点

在C语言编程中,栈上开辟的int 、char、定长度数组,空间大小固定、无法灵活调整。但实际开发中,很多场景需要程序运行时才确定内存大小动态扩容 / 缩容,这就必须掌握动态内存管理。

一、为什么需要动态分配内存?

int vsl = 20;       在栈空间上开辟4字节
char arr[10] = {0}; 在栈空间上开辟10字节

我们之前所学的变量创建是在栈区上,根据类型字节的大小分配空间,但是这样的开辟空间方式有两个特点:

  • 空间大小固定编译时就确定,运行中无法修改。
  • 数组长度不可变:声明时必须指定长度,一旦定义无法扩容 / 缩容。

而实际需求往往是动态的:比如用户输入数据量不确定、列表需要随时增减元素。C 语言引入堆区动态内存分配,由程序员手动申请、释放,完美解决上述问题,灵活可控!

二、核心动态开辟函数

1.malloc:申请指定大小的连续内存

函数原型
void* malloc(size_t size);
  • 头文件#include<stdlib.h>
  • 功能:向堆区申请size字节连续可用内存,返回起始地址。
  • 参数size为申请的字节数。
  • 返回值:成功返回void*类型的起始地址(需强制类型转换);失败返回NULL(内存不足时),必须判空。
  • 注意:申请的内存未初始化,是随机值。

2.free:释放动态申请的内存

函数原型
void free(void* ptr);
  • 头文件#include<stdlib.h>
  • 功能释放ptr指向的堆区动态内存
  • 参数ptr动态内存起始地址
  • 注意:如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的;如果参数ptr是NULL指针,则函数什么事都不做。

3.calloc:申请并初始化内存

函数原型
void* calloc(size_t num,size_t size);
  • 功能:申请num个、每个size字节的连续内存,自动初始化为 0
  • 与malloc的区别:calloc会初始化内存为0,malloc是随机值
  • 返回值:同malloc,成功返回地址,失败返回NULL。

4.realloc:动态调整内存大小

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时候我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。
函数原型
void* realloc(void* ptr,size_t size);
  • 功能:调整ptr 指向的动态内存大小为size字节,保留原数据
  • 参数ptr为原内存地址,NULL时等价于malloc;size为调整后的新字节数
  • 返回值:成功返回新地址(可能与原地址不同);失败返回NULL,原内存不变,不释放。
  • 扩容两种情况
    1. 原内存后有足够空间直接追加返回原地址
    2. 原内存后无空间开辟新空间拷贝原数据释放旧空间返回新地址

三、动态内存常见的6大错误

1.对NULL指针解引用

malloc、realloc、calloc函数返回值可能为NULL,直接解引用会崩溃。

2.对动态开辟空间的越界访问

申请10个int,访问第11个int,越界访问。

3.对非动态开辟内存使用free释放

free(栈区变量),行为未定义。

4.使用free释放一块动态开辟内存的一部分

指针偏移后free,行为未定义。

5.对同一块动态内存多次释放

同一块内存被多次释放后,程序崩溃。

6.动态开辟内存未释放(内存泄漏)

动态内存忘记释放,程序运行占用内存不释放。

四、动态内存经典笔试题分析

1.传值调用,内存泄漏

  • 结果:程序崩溃,内存泄漏。
  • 原因:GetMemory 是值传递,p 是 str 的临时拷贝,修改 p 不影响原 str,str 仍为 NULL,解引用崩溃;malloc 的内存无法释放,泄漏。

改进方法1:使用二级指针作为参数。

改进方法2:函数返回指针。

2.返回栈区地址,野指针

  • 结果:打印随机值,野指针。
  • 原因:p 是栈区局部数组,函数返回后栈帧销毁,p 的地址失效,str 指向野指针。

改进方法1:  使用静态区内存

3.传址调用,内存泄漏

  • 结果:正常打印 hello,内存泄漏,程序崩溃。
  • 原因:二级指针传递,GetMemory 修改 str 本身,指向堆区内存,合法可用;malloc 的内存无法释放,泄漏。

4.free后野指针

  • 结果:程序崩溃或打印随机值。
  • 原因:free 后 str 未置空,仍指向已释放的内存(野指针),非法访问。

五、柔性数组:结构体的动态数组

1.柔性数组的定义

C99 规定:结构体最后一个成员可以是未知大小的数组,称为柔性数组

// 写法1:常用
struct S 
{
    int n;
    int arr[];柔性数组成员
};
// 写法2:部分编译器支持
struct S 
{
    int n;
    int arr[0];
};

2.柔性数组的特点

  1. 结构体中至少有一个其他成员(不能只有柔性数组)。
  2. sizeof(结构体) 不包含柔性数组的内存。
  3. 必须用malloc一次性分配结构体 + 柔性数组的内存。
struct S
{
    int n;
    char c;
    int arr[];
};
int main()
{
    printf("%zu\n",sizeof(struct S)); 8
    return 0;
}

3.柔性数组 vs 指针成员

struct S
{
    int n;
    int arr[];柔性数组成员
};
int main()
{
    struct S* p=(Struct S*)malloc(sizeof(struct S)+10*sizeof(int));
    1.一次malloc,同时包含结构体n+arr数组空间
    if(p==NULL)
    { 
        perror("malloc");//给出错误信息
        rerturn 1;
    }
    p->n=100;
    for(int i=0;i<10;i++)
    {
        ps->arr[i]=i+1;
    }
    free(p);//一次释放空间
    p=NULL;
    return 0;
}
    
struct S
{
    int n;
    int* arr; 指针变量
};
int main()
{
    struct S* p=(struct S*)malloc(sizeof(struct S));
    1.只开辟了int n + int* arr的大小,此时arr是野指针,没有指向任何可用数组空间
    if(p==NULL)
    {
        perror("malloc");
        return 1;
    }
    p->n=100;
    int* ptr=(int*)malloc(10*sizeof(int));
    2.单独创建arr指向的数组空间
    p->arr=ptr;
    for(int i=0;i<10;i++)
    {
        ps->arr[i]=i+1;
    }
    free(p->arr); 3.第一次释放数组数据空间
    free(p);      4.第二次释放结构体空间
    p=NULL;
    return 0;
}

(1)内存布局

  • 柔性数组:一整块连续内存,narr紧紧挨在一起。
  • 指针成员:两块分散内存,结构体和数组内存相互独立。

(2)释放区别(最大优势)

  1. 柔性数组:一次 free 全部释放,不会漏释放,不易内存泄漏。
  2. 指针成员:必须先后两次 free,任意一次遗漏都会内存泄漏。

(3)效率

  • 柔性数组内存连续,CPU 缓存命中率高,访问速度更快,内存碎片更少。
  • 指针成员两块内存分散,效率略低,容易产生内存碎片。

(4)使用场景总结

  1. 追求简洁、稳定、高性能:优先柔性数组(结构体动态数组首选)。
  2. 数组需要中途单独扩容、单独销毁:选用指针成员。

总结:柔性数组 = 一体连续内存,一次释放;指针成员 = 分体内存,两次释放。

六、C/C++ 程序内存区域划分

  1. 栈区(stack):存放局部变量、函数参数、返回地址;自动分配释放,向下增长,空间小。
  2. 堆区(heap):存放动态内存malloc/calloc/realloc),手动分配释放,向上增长,空间大。
  3. 数据段(静态区):存放全局变量、静态变量;程序结束由系统释放。
  4. 代码段:存放可执行代码、只读常量;只读,防止篡改。
  5. 内核空间:操作系统占用,用户代码不可读写。

到此这篇关于C语言动态内存管理深度解析的文章就介绍到这了,更多相关C语言动态内存管理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言中6组指针和自增运算符结合方式的运算顺序问题

    C语言中6组指针和自增运算符结合方式的运算顺序问题

    本文通过代码实现分析了6种组合:* p++,(* p)++,* (p++),++* p,++( * p), * (++p),需要的朋友可以参考下
    2015-07-07
  • C语言趣味编程之水仙花数

    C语言趣味编程之水仙花数

    这篇文章介绍了C语言趣味编程之水仙花数,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-11-11
  • C++宏函数和内联函数的使用

    C++宏函数和内联函数的使用

    本文主要介绍了C++宏函数和内联函数的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • Qt实现调用相机进行拍照并进行图像处理

    Qt实现调用相机进行拍照并进行图像处理

    这篇文章主要为大家详细介绍了如何使用Qt实现调用相机进行拍照并进行图像处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2026-04-04
  • C语言中的指针 初阶

    C语言中的指针 初阶

    这篇文章主要介绍的是关于初级阶段学习C语言中指针的一些内容,那就是指针是什么?简单的说,就是通过它能找到以它为地址的内存单元。下面文章我们就来详细介绍该内容,需要的朋友可以参考一下
    2021-10-10
  • 深入理解C++ 空类大小

    深入理解C++ 空类大小

    本文主要介绍了C++ 空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下
    2025-01-01
  • C++编程小心指针被delete两次

    C++编程小心指针被delete两次

    这篇文章主要介绍了C++编程指针被delete两次的严重后果,以实例阐述了C++指针使用中的误区和注意点,需要的朋友可以参考下
    2014-07-07
  • VC枚举串口端口应用

    VC枚举串口端口应用

    这篇文章主要介绍了VC枚举串口端口应用,罗列了常见的一些串口端口的应用实例,需要的朋友可以参考下
    2014-10-10
  • C++ std::bind用法详解

    C++ std::bind用法详解

    这篇文章主要介绍了C++ std::bind用法详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-09-09
  • C++中友元的详解及其作用介绍

    C++中友元的详解及其作用介绍

    这篇文章主要介绍了C++中友元的详解及其作用介绍,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09

最新评论