第二十章、动态内存管理

动态内存管理是在程序运行时(而非编译时)根据程序的运行情况手动申请和使用所需要的内存空间来存储数据,在使用完毕后有可以释放这段内存的机制。

动态内存管理是在堆地址空间内分配内存,在有操作系统的程序中,堆地址空间的内存通常由操作系统统一管理供各个进程使用。在没有操作系统的嵌入式系统中,通常需要自己写代码来管理堆地址空间的内存。

我们先来总结一下 C 语言数据存储的方法和声明周期。

  1. 全局变量和静态局部变量,它们通常存储在数据段,它们的声明周期是在进程启动后一直存在,在进程退出后销毁。它们属于静态存储期。
  2. 在函数内部定义的局部变量,它们通常在栈上自动分配。它们在函数调用时才分配内存空间,在函数结束后自动销毁。它们属于自动存储期。
  3. 根据实际需要,使用 malloc 系列函数在堆地址空间内分配的内存,这个空间需要使用 free 函数手动释放。它们属于动态存储期。

有些程序在运行时才能确定具体需要的内存数量,并且这个内存数量往往比较大,不适合于在栈和数据段来分配内存,此时则可以使用堆上分配的内存。

说明:

栈的空间通常在进程启动后就已经确定,通常在 Linux 操作系统中,默认的栈空间是 8 兆字节。在很多的嵌入式系统中,栈空间可能只有几百字节到几千字节不等(实际可以软件控制)。因此使用栈空间来存储较大数据可能会带来系统的不确定性。通常在运行时需要较大的内存,我们常在堆上来动态分配内存。动态分配的内存需要使用指针来进行管理、使用和释放。

1. 动态内存分配和释放

C 语言的标准库提供了三个常用的动态内存分配的函数 malloccallocrealloc 函数和一个用于释放动态分配的内存的函数 free

这些个函数如下表所示:

函数
说明
void *malloc(size_t size);
申请 size 个字节的连续的内存。
void *calloc(size_t nmemb, size_t size);
申请 nmembsize 个字节的连续的内存(共nmemb ✖️ size个字节),并将内存中每个字节初始化为 0。
void *realloc(void * ptr, size_t size);
重新分配 ptr 指向的内存,将其改为 size 个字节,如果 size 小于之前的内存空间,则新内存的内容不变,如果 size 大于之前内存空间,则新增的内存空间不会被初始化。如果 ptrNULL 则等同于 malloc(size)
void free(void * ptr);
释放 ptr 指向的内存,还回给系统。

说明:

  1. 使用这些函数是需要包含头文件 stdlib.h
  2. malloccallocrealloc 函数时,如果内存分配成功则返回内存的起始地址,失败返回 NULL
  3. 内存分配失败的原因通常是没有足够的连续的内存空间可供分配。
  4. 上述三个函数返回内存的起始地址,类型是 void* 类型。在使用时需要强制类型转换到其它类型才能方便使用。
  5. 上述 malloccallocrealloc 分配的内存空间必须使用指针记录,并在使用完毕后必须由 free 函数释放,否则会引起内存丢失等问题,严重会引起宕机。
  6. free 函数的参数 ptr 必须是使用上述三个函数返回的内存地址,且每个地址只能释放一次。

示例:

动态在堆地址空间内分配 一段内存来存储 n 个学生的信息。然后读取 n 个学生的信息并打印。然后释放这段内存空间。

// filename: dynamic_alloc.c
#include <stdio.h>
#include <stdlib.h>

struct student {
    char name[32];
    int age;
};

int main(int argc, char * argv[]) {
    int n = 0;  // 用来记录学生个数。
    int i; // 循环变量
    struct student * all_stu = NULL;  // 用于记录学生数据的指针
    printf("请输入学生个数: ");
    scanf("%d", &n);

    all_stu = malloc(n * sizeof(struct student));
    // 上述语句也可以写成
    // all_stu = cmalloc(n, sizeof(struct student));
    if  (NULL == all_stu) {
        printf("内存空间不足,分配内存失败!");
        return 1;
    }
    // 分配内存成功
    for (i = 0; i < n; i++) {
        printf("请输入学生姓名: ");
        scanf("%s", all_stu[i].name);
        printf("请输入学生年龄: ");
        scanf("%d", &all_stu[i].age);
    }
    // 打印上述输入的信息
    for (i = 0; i < n; i++) {
        printf("%s 今年 %d 岁。\n", all_stu[i].name, all_stu[i].age);
    }
    // 释放内存
    free(all_stu);

    return 0;
}

编译和运行结果如下:

weimingze@mzstudio:~$ gcc -o dynamic_alloc dynamic_alloc.c
weimingze@mzstudio:~$ ./dynamic_alloc
请输入学生个数: 2
请输入学生姓名: zhangsan
请输入学生年龄: 18
请输入学生姓名: lisi
请输入学生年龄: 19
zhangsan 今年 18 岁。
lisi 今年 19 岁。

实验:

做一个具有破坏性的程序。在做此实验前请先保存你的所有文档。建议使用不用电脑或虚拟机进行实验。

在实验前,请提前打开任务管理器(Windows系统)、活动监视器(Mac OS 系统)或系统监视器(Ubuntu Linux 系统)等能够看到内存使用情况的工具软件。

写一个死循环,每次分为一定的内存空间,然后写入一些内容。当然每次内存并不释放。直至计算机内存耗尽而死机。

当然你也可以有限次数的循环来代替死循环来控制内存分配的最大值。

如:

while(1) {
    volatile int *p = NULL;
    p = (int*)malloc(sizeof(int));
    *p = 1;
}