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(内存泄漏)
相关推荐
70asunflower14 小时前
CUDA基础知识巩固检验练习题【附有参考答案】(6)
c++·人工智能·cuda
波动几何14 小时前
人工智能编程之复杂功能描述样本(待办任务)
人工智能
Flying pigs~~14 小时前
机器学习之数据挖掘时间序列预测
人工智能·算法·机器学习·数据挖掘·线性回归
东荷新绿14 小时前
【论文学习】ESEFR-GAN:一种不依赖先验信息的人脸复原框架
人工智能·生成对抗网络·人脸复原·eaai
Lim小刘14 小时前
【保姆级教程】在 AWS Lightsail 上快速部署 OpenClaw:开启您的个人 AI 助手
人工智能·云计算·aws
刘 大 望14 小时前
使用AI IDE从0到1开发五子棋对战项目(vibe coding)
java·人工智能·spring boot·redis·ai·java-rabbitmq·ai编程
液态不合群14 小时前
AI赋能下的中国低代码市场:从工具革新到产业数字化核心引擎
java·人工智能·低代码·架构
shuidaoyuxing14 小时前
在汽车领域,“辅助驾驶”与“自动驾驶”的区分及标准的讲解及介绍
人工智能·自动驾驶·汽车
李昊哲小课14 小时前
Python OS模块详细教程
服务器·人工智能·python·microsoft·机器学习
weiyvyy14 小时前
无人机嵌入式开发实战-飞控系统原理与架构
人工智能·嵌入式硬件·机器人·无人机