C语言: 动态内存管理

动态内存管理

目录

  1. 为什么要有动态内存分配
  2. [malloc 和 free](#malloc 和 free)
  3. [calloc 和 realloc](#calloc 和 realloc)
  4. 常见的动态内存的错误
  5. 动态内存经典笔试题分析
  6. 柔性数组
  7. [总结 C/C++ 中程序内存区域划分](#总结 C/C++ 中程序内存区域划分)

1. 为什么要有动态内存分配

我们已经掌握的内存开辟方式有:

c 复制代码
int val = 20;       // 在栈空间上开辟四个字节(固定的向内存申请4个字节)
char arr[10] = {0}; // 在栈空间上开辟10个字节的连续空间(申请连续的一块空间,40字节)

但是上述的开辟空间的方式有两个特点:

  • 空间开辟大小是固定的。
  • 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。

有时候我们需要的空间大小在程序运行的时候才能知道 ,那数组的编译时开辟空间的方式就不能满足了。

C 语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

示例:为什么需要动态内存

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

int main()
{
    int n = 0;
    printf("请输入需要存储的学生人数:");
    scanf("%d", &n);

    // ❌ 错误做法:大小在编译时必须是常量(C89标准不支持VLA)
    // int arr[n]; // 某些编译器不支持

    // ✅ 正确做法:运行时动态申请
    int *arr = (int *)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("内存申请失败!\n");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        arr[i] = i + 1;
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr); // 使用完毕后释放
    arr = NULL;
    return 0;
}

2. malloc 和 free

2.1 malloc

C 语言提供了一个动态内存开辟的函数:

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

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败 ,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查
  • 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为 0,malloc 的行为是标准未定义的,取决于编译器。

2.2 free

C 语言提供了另外一个函数 free,专门用来做动态内存的释放和回收的:

c 复制代码
void free(void* ptr);
  • 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。
  • 如果参数 ptrNULL 指针,则函数什么事都不做。
  • mallocfree 都声明在 stdlib.h 头文件中。

2.3 malloc 与 free 使用示例

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

int main()
{
    int num = 0;
    scanf("%d", &num);

    // int arr[num] = {0}; // 变长数组,不推荐(C89不支持)

    int *ptr = NULL;
    ptr = (int *)malloc(num * sizeof(int)); // 动态申请 num 个 int 大小的空间

    if (NULL != ptr) // 判断 ptr 指针是否为空
    {
        int i = 0;
        for (i = 0; i < num; i++)
        {
            *(ptr + i) = i; // 等价于 ptr[i] = i;
            printf("%d ", ptr[i]);
        }
        printf("\n");
    }
    else
    {
        printf("内存申请失败!\n");
        return 1;
    }

    free(ptr);  // 释放 ptr 所指向的动态内存
    ptr = NULL; // 将指针置为 NULL,避免野指针(有必要!)
    return 0;
}

注意: free(ptr) 之后,ptr 仍然保存着原来的地址(野指针),因此要手动置为 NULL


3. calloc 和 realloc

3.1 calloc

C 语言还提供了一个函数叫 calloc,也用来动态内存分配:

c 复制代码
void* calloc(size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。
示例
c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *p = (int *)calloc(10, sizeof(int)); // 申请10个int,自动初始化为0
    if (NULL != p)
    {
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            printf("%d ", *(p + i)); // 输出:0 0 0 0 0 0 0 0 0 0
        }
        printf("\n");
    }
    free(p);
    p = NULL;
    return 0;
}

输出结果:

复制代码
0 0 0 0 0 0 0 0 0 0

如果对申请的内存空间的内容要求初始化,可以使用 calloc 函数来完成任务。

malloc vs calloc 对比
函数 初始化 参数 用途
malloc(size) 不初始化(随机值) 总字节数 快速申请内存
calloc(num, size) 全部初始化为 0 元素个数 + 单元大小 需要初始值为0时使用
c 复制代码
// 等价写法对比
int *p1 = (int *)malloc(10 * sizeof(int));   // 不初始化
int *p2 = (int *)calloc(10, sizeof(int));    // 自动初始化为0

// 若要用 malloc 实现 calloc 的效果,需要额外调用 memset
memset(p1, 0, 10 * sizeof(int));

3.2 realloc

realloc 函数可以对动态开辟内存的大小进行灵活调整

c 复制代码
void* realloc(void* ptr, size_t size);
  • ptr 是要调整的内存地址
  • size 是调整之后的新大小
  • 返回值为调整之后的内存起始位置
  • 这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc 的两种情况
复制代码
情况1:原有空间之后有足够大的空间

ptr ──→ [ 已分配 20byte ][ 尚未分配(空闲)  ]
扩容后:  [ 已分配 20byte ][ 新增空间          ]
(直接追加,ptr 不变,数据不发生变化)

情况2:原有空间之后没有足够大的空间

ptr ──→ [ 已分配 20byte ][ 已分配(被占用)   ]

在堆的其他位置另找空间:
newptr ──→ [ 复制的20byte + 新增空间          ]
(函数返回新地址,原空间自动释放)
realloc 正确使用方式
c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *ptr = (int *)malloc(100); // 申请100字节
    if (ptr != NULL)
    {
        // 业务处理...
    }
    else
    {
        return 1;
    }

    // ❌ 代码1:直接将 realloc 的返回值放到 ptr 中(有风险!)
    // ptr = (int *)realloc(ptr, 1000);
    // 如果申请失败返回 NULL,ptr 变为 NULL,原来的内存地址丢失,造成内存泄漏!

    // ✅ 代码2:先用临时变量保存返回值,判断后再赋值
    int *p = NULL;
    p = (int *)realloc(ptr, 1000);
    if (p != NULL)
    {
        ptr = p; // 确认申请成功后再更新 ptr
    }
    else
    {
        printf("扩容失败,原数据仍然可用!\n");
    }

    // 业务处理...

    free(ptr);
    ptr = NULL;
    return 0;
}
realloc 特殊用法:当 ptr 为 NULL 时,等同于 malloc
c 复制代码
// 当第一个参数为 NULL 时,realloc 等同于 malloc
int *p = (int *)realloc(NULL, 100 * sizeof(int));
// 等价于
int *p = (int *)malloc(100 * sizeof(int));

4. 常见的动态内存的错误

4.1 对 NULL 指针的解引用操作

c 复制代码
void test()
{
    int *p = (int *)malloc(INT_MAX / 4); // 申请极大空间,极可能失败返回 NULL
    *p = 20; // ❌ 如果 p 的值是 NULL,就会有问题(崩溃)
    free(p);
}

// ✅ 正确做法:
void test_correct()
{
    int *p = (int *)malloc(INT_MAX / 4);
    if (p == NULL) {
        perror("malloc failed");
        return;
    }
    *p = 20;
    free(p);
    p = NULL;
}

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

c 复制代码
void test()
{
    int i = 0;
    int *p = (int *)malloc(10 * sizeof(int)); // 申请10个int
    if (NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for (i = 0; i <= 10; i++) // ❌ i <= 10,当 i == 10 时越界访问
    {
        *(p + i) = i;
    }
    free(p);
}

// ✅ 正确做法:
void test_correct()
{
    int i = 0;
    int *p = (int *)malloc(10 * sizeof(int));
    if (NULL == p) exit(EXIT_FAILURE);
    for (i = 0; i < 10; i++) // ✅ 严格 < 10
    {
        *(p + i) = i;
    }
    free(p);
    p = NULL;
}

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

c 复制代码
void test()
{
    int a = 10;
    int *p = &a;    // p 指向栈上的变量
    free(p);        // ❌ 对非堆内存调用 free,行为未定义(崩溃)
}

// ✅ 只能 free 由 malloc/calloc/realloc 申请的内存

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

c 复制代码
void test()
{
    int *p = (int *)malloc(100);
    p++;        // p 指针向后偏移了一个 int 的大小
    free(p);    // ❌ p 不再指向动态内存的起始位置,行为未定义
}

// ✅ 正确做法:
void test_correct()
{
    int *p = (int *)malloc(100);
    int *start = p; // 保存起始地址
    p++;            // 可以移动 p 进行操作
    // ...
    free(start);    // 释放起始地址
    start = NULL;
}

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

c 复制代码
void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p); // ❌ 重复释放,行为未定义(可能崩溃)
}

// ✅ 正确做法:free 后立即置 NULL
void test_correct()
{
    int *p = (int *)malloc(100);
    free(p);
    p = NULL;   // 置为 NULL 后,再次 free(NULL) 是安全的(什么都不做)
    free(p);    // 安全,free(NULL) 不会出错
}

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

c 复制代码
void test()
{
    int *p = (int *)malloc(100); // 申请了内存
    if (NULL != p)
    {
        *p = 20;
    }
    // ❌ 函数结束,没有调用 free(p)!
    // p 是局部变量,函数结束后 p 消失,但堆上的内存没有被释放
}

int main()
{
    test();
    while (1); // 程序持续运行,内存始终无法回收 → 内存泄漏
}

// ✅ 正确做法:
void test_correct()
{
    int *p = (int *)malloc(100);
    if (NULL != p)
    {
        *p = 20;
        // 使用完毕后释放
        free(p);
        p = NULL;
    }
}

切记:动态开辟的空间一定要释放,并且正确释放。


5. 动态内存经典笔试题分析

5.1 题目1

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

void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world"); // ❌ str 仍为 NULL,程序崩溃
    printf(str);
}

分析:
GetMemory 函数接收的是 str (即 NULL),pstr 的拷贝,函数内部修改 p 不影响 str

函数执行完后,str 依然是 NULL,strcpy 对 NULL 指针解引用导致程序崩溃

另外,在函数内部 malloc 的内存没有 free,造成内存泄漏

修正方案一:传二级指针

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

void Test(void)
{
    char *str = NULL;
    GetMemory(&str);           // 传地址
    if (str != NULL) {
        strcpy(str, "hello world");
        printf(str);
        free(str);             // 记得释放
        str = NULL;
    }
}

修正方案二:返回值传递

c 复制代码
char *GetMemory(void)
{
    char *p = (char *)malloc(100);
    return p;
}

void Test(void)
{
    char *str = GetMemory(); // 通过返回值获取地址
    if (str != NULL) {
        strcpy(str, "hello world");
        printf(str);
        free(str);
        str = NULL;
    }
}

5.2 题目2

c 复制代码
char *GetMemory(void)
{
    char p[] = "hello world"; // p 是栈上的局部数组
    return p;                 // ❌ 返回栈上局部变量的地址
}

void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str); // ❌ 打印的是"野指针",行为未定义(可能乱码,可能崩溃)
}

分析:
p 是局部数组,存在于 GetMemory 函数的栈帧上。函数返回后,栈帧被销毁,p 数组的内存可能被其他内容覆盖。
str 拿到的是一个野指针printf 的结果是未定义行为(乱码或崩溃)。

修正:使用 static 或 malloc

c 复制代码
// 方案1:static 修饰(存于静态区,生命周期是整个程序)
char *GetMemory(void)
{
    static char p[] = "hello world";
    return p;
}

// 方案2:malloc(存于堆区)
char *GetMemory(void)
{
    char *p = (char *)malloc(100);
    strcpy(p, "hello world");
    return p; // 调用者负责 free
}

5.3 题目3

c 复制代码
void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num); // 通过二级指针修改调用者的指针
}

void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str); // 输出:hello
    // ❌ 没有 free,内存泄漏
}

分析:

此处传入的是 str 的地址(&str),所以 *p = malloc(num) 等于 str = malloc(100),正确地将申请到的内存地址赋给了 str

程序可以正常打印 hello,但忘记 free,存在内存泄漏。

修正:

c 复制代码
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
    free(str);   // ✅ 释放内存
    str = NULL;
}

5.4 题目4

c 复制代码
void Test(void)
{
    char *str = (char *)malloc(100);
    strcpy(str, "hello");
    free(str);            // 释放了内存
    if (str != NULL)      // ⚠️ str 不为 NULL(free 不会改变指针值)
    {
        strcpy(str, "world"); // ❌ 访问已释放的内存(野指针),行为未定义
        printf(str);
    }
}

分析:
free(str) 释放了内存,但 str 的值并没有变成 NULL,仍然保存着原来的地址(此时是野指针)。
if (str != NULL) 条件成立,继续对野指针操作,行为未定义(可能崩溃,可能输出乱码)

修正:free 后立即置 NULL

c 复制代码
void Test(void)
{
    char *str = (char *)malloc(100);
    strcpy(str, "hello");
    free(str);
    str = NULL; // ✅ free 后立即置 NULL
    if (str != NULL) // 此时条件不成立,不会进入 if
    {
        strcpy(str, "world");
        printf(str);
    }
}

6. 柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组 ,这就叫做「柔性数组」成员。

c 复制代码
struct st_type
{
    int i;
    int a[0]; // 柔性数组成员(有些编译器可能报错)
};

// 有些编译器需要写成:
struct st_type
{
    int i;
    int a[]; // 柔性数组成员(更常用写法)
};

6.1 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
c 复制代码
#include <stdio.h>
#include <stdlib.h>

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

int main()
{
    printf("%d\n", (int)sizeof(type_a)); // 输出:4(只算 int i 的大小)
    return 0;
}

6.2 柔性数组的使用(代码1)

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

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

int main()
{
    int j = 0;
    // 申请结构体大小 + 100个int的空间
    type_a *p = (type_a *)malloc(sizeof(type_a) + 100 * sizeof(int));
    if (p == NULL) {
        perror("malloc");
        return 1;
    }

    p->i = 100;
    for (j = 0; j < 100; j++)
    {
        p->a[j] = j; // 柔性数组成员 a 相当于获得了100个整型元素的连续空间
    }

    printf("i = %d\n", p->i);
    printf("a[0]=%d, a[99]=%d\n", p->a[0], p->a[99]);

    free(p); // 只需要 free 一次
    p = NULL;
    return 0;
}

6.3 柔性数组的优势

上述 type_a 结构也可以设计为下面的结构,也能完成同样的效果(代码2):

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

typedef struct st_type
{
    int i;
    int *p_a; // 用指针代替柔性数组
} type_a;

int main()
{
    int j = 0;
    type_a *p = (type_a *)malloc(sizeof(type_a));
    if (p == NULL) return 1;

    p->i = 100;
    p->p_a = (int *)malloc(p->i * sizeof(int)); // 第二次 malloc
    if (p->p_a == NULL) {
        free(p);
        return 1;
    }

    for (j = 0; j < 100; j++)
    {
        p->p_a[j] = j;
    }

    // 释放空间(需要两次 free)
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
    return 0;
}
柔性数组(代码1)相比指针方案(代码2)的两个好处

好处1:方便内存释放

复制代码
代码1(柔性数组):只需 free(p) 一次
代码2(指针):    需要先 free(p->p_a),再 free(p),顺序不能错

如果将结构体返回给调用者,调用者只需调用一次 free 即可释放所有内存,不需要了解内部结构。

好处2:有利于访问速度

复制代码
代码1:结构体和数组数据在连续的内存中
         [ i | a[0] | a[1] | ... | a[99] ]
         └──────────── 连续内存 ──────────┘

代码2:结构体和数组数据分散在两块内存中
         [ i | p_a ──────────────────→ [ a[0] | a[1] | ... | a[99] ] ]
         └── 一块 ──┘                   └───────── 另一块 ────────────┘

连续的内存有益于提高访问速度,也有益于减少内存碎片(减少缓存 miss)。


7. 总结 C/C++ 中程序内存区域划分

内存分区示意图

复制代码
高地址
┌─────────────────────────────┐
│         内核空间             │  ← 用户代码不能读写
├─────────────────────────────┤
│     栈区(stack)    ↓       │  ← 向低地址增长
│                             │
│      (空闲区域)            │
│                             │
│     堆区(heap)     ↑       │  ← 向高地址增长
├─────────────────────────────┤
│   内存映射段(mmap)         │  ← 文件映射、动态库、匿名映射
├─────────────────────────────┤
│     数据段(静态区)          │  ← 全局变量、静态变量
├─────────────────────────────┤
│     代码段(只读)            │  ← 函数二进制代码、只读常量
└─────────────────────────────┘
低地址

各区域说明

区域 内容 管理方式 生命周期
栈区 (stack) 局部变量、函数参数、返回地址 自动管理(编译器) 函数调用时创建,返回时销毁
堆区 (heap) malloc/calloc/realloc 申请的内存 程序员手动管理 直到 free 或程序结束
数据段 (静态区) 全局变量、static 变量 操作系统 程序整个生命周期
代码段 函数体的二进制代码、字符串常量 只读 程序整个生命周期

代码示例:不同变量位于不同区域

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

int globalVar = 1;              // 数据段(全局初始化变量)
static int staticGlobalVar = 1; // 数据段(静态全局变量)

void test()
{
    static int staticVar = 1;   // 数据段(静态局部变量,只初始化一次)
    int localVar = 1;           // 栈区(局部变量)
    int num1[10] = {1, 2, 3, 4};// 栈区(局部数组)

    char char2[] = "abcd";      // 栈区("abcd"复制到栈上的数组中)
    char *pChar3 = "abcd";      // pChar3 在栈区,"abcd" 字符串在代码段(只读)

    int *ptr1 = (int *)malloc(4 * sizeof(int));  // ptr1 在栈,指向堆区
    int *ptr2 = (int *)calloc(4, sizeof(int));   // ptr2 在栈,指向堆区
    int *ptr3 = (int *)realloc(ptr2, 4 * sizeof(int)); // ptr3 在栈,指向堆区

    free(ptr1);  // 释放堆内存
    free(ptr3);  // 释放堆内存(ptr2 已由 realloc 处理)
}

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

关键区别:栈 vs 堆

c 复制代码
// 栈上数组:大小必须在编译期确定,自动释放,速度快,空间有限(约1-8MB)
int arr[10]; // ✅ 编译期大小确定
// int arr[n]; // ⚠️ VLA,C99支持,但不推荐

// 堆上内存:大小可以在运行期确定,手动释放,空间大(受物理内存限制)
int *arr = (int *)malloc(n * sizeof(int)); // ✅ 运行期确定大小
// 使用后必须 free
free(arr);
arr = NULL;

总结

函数 头文件 功能 初始化 注意事项
malloc(size) stdlib.h 申请指定字节数的内存 不初始化 返回值需判空
free(ptr) stdlib.h 释放动态申请的内存 --- free后置NULL
calloc(n, size) stdlib.h 申请n个元素内存 初始化为0 返回值需判空
realloc(ptr, size) stdlib.h 调整已申请内存大小 不初始化新增部分 用临时变量接收返回值

动态内存使用六大禁忌:

  1. 对 NULL 指针解引用
  2. 越界访问动态数组
  3. free 非堆内存
  4. free 非起始地址
  5. 重复 free
  6. 忘记 free(内存泄漏)
相关推荐
破晓之翼2 小时前
从第一性原理和工程控制论角度企业去思考AI开发避免完美主义陷阱
人工智能
njsgcs2 小时前
屏幕元素定位(Grounding) ollama两个模型
人工智能
码农杂谈00072 小时前
企业 AI 推理:告别黑箱决策,4 步构建可解释 AI 体系
大数据·人工智能
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-02-18
大数据·数据库·人工智能·经验分享·搜索引擎
量子-Alex2 小时前
【大模型思维链】COT、COT-SC、TOT和RAP四篇经典工作对比分析
人工智能·深度学习·机器学习
分享牛3 小时前
大模型结合BPMN语言,下一代BPM产品的雏形
人工智能·搜索引擎·llm·bpmn
MoonOutCloudBack3 小时前
VeRL 框架下 RL 微调 DeepSeek-7B,比较 PPO / GRPO 脚本的参数差异
人工智能·深度学习·算法·语言模型·自然语言处理
量子-Alex3 小时前
【大模型智能体】Agent-as-a-Judge
人工智能
AI架构全栈开发实战笔记3 小时前
AI应用架构师教你:如何用AI自动化数据仓库的测试?
数据仓库·人工智能·ai·自动化