C语言——动态内存管理

动态内存管理是C语言进阶的核心难点,也是笔试面试的高频考点。本文将结合实战案例,系统梳理动态内存的常见错误、柔性数组的优势,以及内存分区的底层逻辑,帮你夯实底层编程能力。


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

已掌握的内存开辟方式

cpp 复制代码
// 在栈空间开辟4字节
int val = 20;
// 在栈空间开辟10字节连续空间
char arr[10] = {0};

静态开辟内存的缺点

• 空间大小固定:数组在声明时必须指定长度,运行时无法调整

• 无法满足动态需求:程序运行时才知道所需空间大小的场景无法处理

动态内存分配的意义

让程序可以在运行时申请和释放内存,更灵活地管理内存资源。

二、malloc 和 free

2.1 malloc 函数

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

• 功能:在堆区申请一块连续可用的内存空间

• 参数:size - 要分配的内存字节数

• 返回值:

成功:返回指向内存起始地址的 void* 指针

失败:返回 NULL(必须检查返回值)

• 注意:

返回的 void* 需根据实际使用类型强制转换

size 为 0 时行为未定义,取决于编译器

2.2 free 函数

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

• 功能:释放动态内存分配函数(malloc/calloc/realloc)申请的空间,malloc 和 free 都声明在 stdlib.h 头文件中

• 参数:ptr - 指向要释放内存的指针

• 注意:

不能释放非动态开辟的内存

ptr 为 NULL 时函数不执行任何操作

释放后应将指针置为 NULL,避免野指针

示例代码

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

int main()
{
    int num = 0;
    scanf("%d", &num);
    int* ptr = NULL;
    // 动态申请num个int大小的空间
    ptr = (int*)malloc(num * sizeof(int));
    
    if (NULL != ptr) // 检查malloc是否成功
    {
        int i = 0;
        for (i = 0; i < num; i++)
        {
            *(ptr + i) = 0;
        }
    }
    else
    {
        perror("malloc");
        return 1;
    }
    
    free(ptr); // 释放动态内存
    ptr = NULL; // 将指针置空,避免野指针
    
    return 0;
}

三、calloc 和 realloc

3.1 calloc 函数

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

• 功能:为 num 个大小为 size 的元素开辟空间,并将每个字节初始化为 0

• 与 malloc 的区别:calloc 会自动初始化内存为全 0,而 malloc 不会

• 适用场景:需要初始化内存的场景

示例代码

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

int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if (NULL != p)
    {
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            printf("%d ", *(p + i)); // 输出全0
        }
    }
    free(p);
    p = NULL;
    return 0;
}

3.2 realloc 函数

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

• 功能:调整已分配内存块的大小,可在保留原有数据的情况下扩大或缩小内存

• 参数:

ptr:指向要调整的内存起始地址(ptr为NULL 时,realloc功能等同于 malloc)

size:调整后的内存大小(字节)

• 返回值:

成功:返回新的内存地址(可能与原地址相同或不同)

失败:返回 NULL,原内存保持不变

内存调整的两种情况

  1. 原有空间后有足够空间:直接在原内存后追加空间,返回原地址

  2. 原有空间后无足够空间:在堆区寻找新空间,拷贝原数据到新空间,释放原空间,返回新地址

安全使用方式

cpp 复制代码
int* p = (int*)malloc(100);
if (p != NULL)
{
    // 业务处理
}
else
{
    return 1;
}

// 扩展容量
int* temp = (int*)realloc(p, 1000);
if (temp != NULL)
{
    p = temp;
}

free(p);
p = NULL;

四、常见的动态内存错误

4.1 对 NULL 指针的解引用

cpp 复制代码
void test()
{
    int* p = (int*)malloc(INT_MAX / 4);
    *p = 20; // 如果malloc失败,p为NULL,会导致程序崩溃
    free(p);
}

✅ 解决:必须检查 malloc/calloc/realloc 的返回值

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

cpp 复制代码
void test()
{
    int i = 0;
    int* p = (int*)malloc(10 * sizeof(int));
    if (NULL == p)
    {
        return;
    }
    for (i = 0; i <= 10; i++)
    {
        *(p + i) = i; // 当i=10时越界访问
    }
    free(p);
}

✅ 解决:确保访问不超过申请的内存范围

4.3 对非动态开辟内存使用 free

cpp 复制代码
int main() {
    int a = 10; // 栈区局部变量
    int* p = &a; 
    free(p); // 错误:释放栈区内存,程序崩溃
    p = NULL;
    return 0;
}

错误原因:free函数仅能释放堆区由malloc/calloc/realloc开辟的动态内存,若对栈区局部变量、全局变量、常量区内存调用free,会触发段错误,因为这些内存的管理由编译器/操作系统负责,并非堆区。

✅ 解决:free 只能用于动态开辟的内存,明确free的使用范围:仅当指针指向堆区动态内存时,才能调用free,栈区/全局区/常量区内存无需手动释放(编译器自动回收)。

4.4 使用 free 释放一块动态内存的一部分

cpp 复制代码
void test()
{
    int* p = (int*)malloc(100);
    p++;
    free(p); // p不再指向动态内存的起始位置
}

错误原因:动态内存申请后,指针被非法偏移,导致free时仅释放了堆区的一部分内存,剩余未释放的内存成为内存碎片,程序运行期间无法再被使用,最终造成内存泄漏(堆内存被占用,直至程序退出才会被系统回收)。

✅ 解决:必须保持指针指向动态内存的起始地址

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

cpp 复制代码
void test()
{
    int* p = (int*)malloc(100);
    free(p);
    free(p); // 重复释放,导致未定义行为
}

错误原因:同一块堆区动态内存,调用多次free,会破坏堆区的内存管理链表,导致程序崩溃,属于未定义行为。

✅ 解决:释放后将指针置为 NULL(free(NULL) 是安全的)

4.6 动态开辟内存忘记释放(内存泄漏)

cpp 复制代码
void test()
{
    int* p = (int*)malloc(100);
    if (NULL != p)
    {
        *p = 20;
    }
}

int main()
{
    test();
    while (1);
}

错误原因:最常见的动态内存错误,申请堆内存后,程序运行期间未调用free释放,且指向该内存的指针丢失,导致堆内存被永久占用,程序不退出则内存无法回收。

• 短期小程序:影响可忽略,程序退出后系统会回收所有内存;

• 长期运行程序(如服务器、嵌入式程序):内存泄漏会持续占用堆空间,最终导致内存耗尽,程序崩溃。

✅ 解决:动态开辟的内存必须释放,且正确释放

五、经典笔试题分析

题目 1:函数传参错误(无法修改主函数指针)

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

void GetMemory(char *p)
{
    p = (char *)malloc(100);
}

void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}

int main()
{
    Test();
    return 0;
}

运行结果:程序会崩溃(段错误),并存在内存泄漏。

问题分析

  1. 指针传参本质是值传递:GetMemory 的形参 p 是 str 的临时拷贝,函数内对 p 的赋值不会影响主函数的 str,所以 str 依然是 NULL。

  2. NULL指针解引用:strcpy(str, "hello world") 中 str 为 NULL,直接解引用会触发段错误。

  3. 内存泄漏:GetMemory 中 malloc 申请的100字节内存,因为临时指针 p 被销毁,没有任何指针指向它,无法释放。

修复方案(两种方式)

方式1:使用二级指针传参

cpp 复制代码
void GetMemory(char **p)
{
    *p = (char *)malloc(100);
    if (*p == NULL) { // 新增:malloc后判空
        perror("malloc fail");
        return;
    }
}

void Test(void)
{
    char *str = NULL;
    GetMemory(&str); // 传入指针的地址
    if (str != NULL) {
        strcpy(str, "hello world");
        printf(str);
        free(str); // 释放内存
        str = NULL;
    }
}

方式2:函数返回动态内存的指针

cpp 复制代码
char* GetMemory()
{
    char* p = (char *)malloc(100);
    if (p == NULL) {
        perror("malloc fail");
        return NULL;
    }
    return p;
}

void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    if (str != NULL) {
        strcpy(str, "hello world");
        printf(str);
        free(str);
        str = NULL;
    }
}

题目 2:返回栈区指针(野指针经典问题)

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

char *GetMemory(void)
{
    char p[] = "hello world";
    return p;
}

void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}

int main()
{
    Test();
    return 0;
}

运行结果:程序会出现乱码或崩溃,属于未定义行为。

问题分析

  1. 返回栈区局部变量地址:GetMemory 中的数组 p 是栈区局部变量,函数执行结束后栈帧被销毁,p 占用的内存被回收。

  2. 野指针访问:Test 中的 str 接收了已失效的栈区地址,成为野指针,此时访问 str 属于非法内存操作。

修复方案(三种方式)

方式1:返回堆区动态内存

cpp 复制代码
char *GetMemory(void)
{
    char* p = (char *)malloc(12); // 存"hello world"+'\0'
    if (p == NULL) {
        perror("malloc fail");
        return NULL;
    }
    strcpy(p, "hello world");
    return p;
}

void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    if (str != NULL) {
        printf(str);
        free(str);
        str = NULL;
    }
}

方式2:返回常量区字符串(字符串常量存于常量区,只读)

cpp 复制代码
char *GetMemory(void)
{
    return "hello world"; // 常量区字符串,不可修改
}

方式3:使用全局变量

cpp 复制代码
char p[] = "hello world"; // 全局区内存
char *GetMemory(void)
{
    return p;
}

核心考点

内存分区的生命周期:

• 栈区:函数进栈创建,出栈销毁,局部变量存于此,不可返回栈区地址;

• 全局/静态区:程序启动时分配,退出时释放;

• 常量区:存字符串常量等,只读,程序运行期间一直存在;

• 堆区:malloc申请,free释放,手动管理生命周期。

题目 3:malloc后未判空

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

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
}

void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}

int main()
{
    Test();
    return 0;
}

运行结果

• 程序会正常打印出 hello,但存在内存泄漏。

问题分析

  1. 传参逻辑正确:GetMemory 接收二级指针 **p,可以成功修改主调函数 Test 里的指针 str,让它指向动态分配的100字节堆内存。

  2. 缺少判空检查:malloc 有可能失败返回 NULL,如果不检查,后续的 strcpy 会解引用 NULL 导致程序崩溃。

  3. 内存泄漏:Test 函数中申请的堆内存没有调用 free 释放,程序退出前这块内存会一直被占用。

修复方案

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

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
    // 新增:malloc后必须判空
    if (*p == NULL)
    {
        perror("malloc fail");
        return;
    }
}

void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    if (str != NULL) // 判空后再操作
    {
        strcpy(str, "hello");
        printf(str);
        // 新增:使用完释放内存
        free(str);
        str = NULL; // 释放后置空,避免野指针
    }
}

int main()
{
    Test();
    return 0;
}

题目 4:野指针

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

void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}

int main()
{
    Test();
    return 0;
}

运行结果

• 这是未定义行为,可能打印 world,也可能程序崩溃或出现乱码。

问题分析

  1. 野指针访问:free(str) 释放了 str 指向的堆内存,但 str 本身没有被置为 NULL,此时 str 是一个野指针(指向已被回收的无效地址)。

  2. 非法内存操作:if(str != NULL) 的判断结果为真,会执行 strcpy(str, "world"),这是对已释放内存的非法写入,会破坏堆区的内存管理结构,导致程序异常。

修复方案

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

void Test(void)
{
    char *str = (char *) malloc(100);
    if (str == NULL) // 新增:malloc后判空
    {
        perror("malloc fail");
        return;
    }
    strcpy(str, "hello");
    free(str);
    str = NULL; // 新增:释放后置空
    if(str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}

int main()
{
    Test();
    return 0;
}

六、柔性数组

6.1 定义与语法

柔性数组是C99标准引入的特性,指结构体的最后一个成员是未知大小的数组。

cpp 复制代码
// 标准写法
struct st_type {
    int i;
    int a[0]; // 柔性数组成员
};

// 兼容某些编译器的写法
struct st_type {
    int i;
    int a[]; // 省略大小
};

6.2 核心特点

• 前置成员要求:柔性数组成员前必须至少有一个其他成员。

• sizeof计算规则:sizeof(struct st_type) 的结果不包含柔性数组的内存(例如上面结构体大小为4,仅包含int i)。

• 动态分配要求:必须用 malloc 一次性分配结构体和柔性数组的内存,且分配的总大小要大于结构体本身的大小。

cpp 复制代码
typedef struct st_type
{
    int i;
    int a[0]; // 柔性数组成员
}type_a;

int main()
{
    printf("%d\n", sizeof(type_a)); // 输出的是4
    return 0;
}

6.3 使用示例

代码1

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

typedef struct st_type
{
    int i;
    int a[0]; // 柔性数组成员
}type_a;

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

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

6.4 柔性数组的优势

上述的 type_a 结构也可以设计为下面的结构,也能完成同样的效果。

代码2

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

typedef struct st_type
{
    int i;
    int *p_a;
}type_a;

int main()
{
    type_a *p = (type_a *)malloc(sizeof(type_a));
    p->i = 100;
    p->p_a = (int *)malloc(p->i*sizeof(int));
    // 业务处理
    for(i=0; i<100; i++)
    {
        p->p_a[i] = i;
    }
    // 释放空间
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
    return 0;
}

上述代码1和代码2可以完成同样的功能,但是方法1的实现有两个好处:

第一个好处是:方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也释放掉。

第二个好处是:这样有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

  1. 优势对比(与"结构体+指针"方案)

|-------|------------------|----------------------|
| 特性 | 柔性数组方案 | 结构体+指针方案 |
| 内存分配 | 一次分配,内存连续 | 两次分配,内存碎片化 |
| 内存释放 | 一次free即可 | 需分别释放指针指向的内存和结构体,易泄漏 |
| 访问效率 | 连续内存,缓存友好,访问速度更快 | 指针间接访问,多一次内存寻址 |
| 代码复杂度 | 更简洁,不易出错 | 需管理两次内存分配,代码繁琐 |

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

程序运行时,内存会被划分为不同区域,各自负责存储不同类型的数据,生命周期也不同。

  1. 栈区(Stack)

• 存储内容:函数的局部变量、参数、返回地址等。

• 管理方式:由编译器自动分配和释放,函数执行结束时栈帧自动销毁。

• 特点:内存大小固定、速度快,但空间有限;地址向下增长。

  1. 堆区(Heap)

• 存储内容:动态分配的内存(malloc/calloc/realloc 申请的内存)。

• 管理方式:由程序员手动分配和释放,若忘记释放会导致内存泄漏,程序结束后由操作系统回收。

• 特点:空间大、灵活;地址向上增长。

  1. 数据段(静态区)

• 存储内容:全局变量、静态变量(static修饰的变量)。

• 管理方式:程序启动时分配,退出时由操作系统释放。

• 特点:生命周期与程序一致。

  1. 代码段(常量区)

• 存储内容:可执行代码、字符串常量等只读数据。

• 管理方式:由操作系统加载,程序运行期间只读。

• 特点:内存不可修改。

  1. 内核空间

• 存储内容:操作系统内核代码和数据。

• 特点:用户程序无法直接访问。


动态内存管理与内存分区的知识,不仅是C语言进阶的必经之路,更是写出高性能、稳定代码的基石。掌握柔性数组的设计思想,避开动态内存的常见陷阱,理解内存分区的底层逻辑,这些能力会让你在复杂场景下的编程更加游刃有余。希望本文的梳理与实战案例,能帮你建立起清晰的内存管理思维,在未来的开发与面试中都能从容应对。

相关推荐
Vect__5 小时前
基于线程池从零实现TCP计算器网络服务
c++·网络协议·tcp/ip
草履虫建模8 小时前
力扣算法 1768. 交替合并字符串
java·开发语言·算法·leetcode·职场和发展·idea·基础
naruto_lnq10 小时前
分布式系统安全通信
开发语言·c++·算法
学嵌入式的小杨同学10 小时前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
Re.不晚10 小时前
Java入门17——异常
java·开发语言
ASKED_201911 小时前
Langchain学习笔记一 -基础模块以及架构概览
笔记·学习·langchain
精彩极了吧11 小时前
C语言基本语法-自定义类型:结构体&联合体&枚举
c语言·开发语言·枚举·结构体·内存对齐·位段·联合
Lois_Luo11 小时前
Obsidian + Picgo + Aliyun OSS 实现笔记图片自动上传图床
笔记·oss·图床
(❁´◡`❁)Jimmy(❁´◡`❁)11 小时前
Exgcd 学习笔记
笔记·学习·算法