在 C 语言中,你已经考虑过为程序创建变量。你创建了一些数组以供使用,但你可能已经注意到了一些限制:

  1. 数组的大小必须事先知道;
  2. 数组的大小在程序执行过程中不能更改。

C 语言中的动态内存分配是一种绕过这些问题的方法。

malloc 函数

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
void free(void *ptr);
void *malloc(size_t size);
void *realloc(void *ptr, size_t size);

标准的 C 函数 malloc 是实现动态内存分配的手段。它在 stdlib.hmalloc.h 中定义,具体取决于你使用的操作系统。malloc.h 仅包含内存分配函数的定义,而不包含 stdlib.h 中定义的其他函数。通常,在程序中你不需要特别指定使用哪个头文件,如果两者都被支持,应该使用 <stdlib.h>,因为它是 ANSI C 标准的一部分,也是我们在这里使用的。

对应的调用 free 用于释放已分配的内存。

动态内存分配的释放

当动态分配的内存不再需要时,应该调用 free 将其释放回内存池。如果覆盖了指向动态分配内存的指针,可能会导致这些数据变得无法访问。如果这种情况发生得很频繁,最终操作系统将无法为该进程分配更多的内存。一旦进程退出,操作系统将能够释放与该进程相关的所有动态分配内存。

动态内存分配在数组中的使用

通常,当我们希望创建一个数组时,我们使用类似这样的声明:

int array[10];

记住,数组可以被视为一个指针,我们将其作为数组来使用。我们指定这个数组的长度是 10 个整数。在 array[0] 之后,其他九个整数将连续存储。

有时,在编写程序时无法知道某些数据需要多少内存;例如,当它取决于用户输入时。在这种情况下,我们希望在程序开始执行后动态分配所需的内存。为此,我们只需要声明一个指针,并在希望为数组中的元素分配空间时调用 malloc;或者,我们可以在初始化数组时告诉 malloc 为我们分配空间。这两种方法都可以接受且有用。

使用 sizeof 确定内存需求

我们还需要知道 int 类型在内存中占用多少空间,以便为其分配空间;幸运的是,这并不难,我们可以使用 C 语言的内建 sizeof 运算符。例如,如果 sizeof(int) 返回 4,那么一个 int 占用 4 字节。自然地,2 * sizeof(int) 就是我们为 2 个 int 分配的内存大小,依此类推。

那么,如何像之前那样使用 malloc 为 10 个 int 分配内存呢?如果我们希望声明并一次性为数组分配空间,可以简单地这样写:

int *array = malloc(10 * sizeof(int));

我们只需要声明一个指针;malloc 为我们分配了存储 10 个 int 的空间,并返回指向第一个元素的指针,这个指针被赋值给 array

重要提示!

malloc 不会初始化数组;这意味着数组可能包含随机或意外的值!就像没有动态分配的数组一样,程序员必须在使用数组之前初始化它。确保你也这样做。(稍后会介绍 memset 函数,这是一个简单的方法。)

延迟分配

并不一定要在声明指针后立即调用 malloc 来分配内存。通常在声明和调用 malloc 之间会有多个语句,示例如下:

int *array = NULL;
printf("Hello World!!!");
/* 更多语句 */
array = malloc(10 * sizeof(int)); /* 延迟分配 */
/* 使用数组 */

动态内存分配的实际例子

以下是一个更实际的动态内存分配示例:

给定一个包含 10 个整数的数组,去除数组中的所有重复元素,并创建一个不包含重复元素的新数组(即一个集合)。

#include <stdio.h>
#include <stdlib.h>

void remove_duplicates(int *arr, int *size) {
    int *temp = malloc(*size * sizeof(int));
    int temp_size = 0;
    int i, j;
    
    for (i = 0; i < *size; i++) {
        int duplicate = 0;
        for (j = 0; j < temp_size; j++) {
            if (arr[i] == temp[j]) {
                duplicate = 1;
                break;
            }
        }
        if (!duplicate) {
            temp[temp_size++] = arr[i];
        }
    }

    // 将去重后的数组拷贝回原数组
    for (i = 0; i < temp_size; i++) {
        arr[i] = temp[i];
    }

    *size = temp_size;  // 更新新数组的大小
    free(temp);  // 释放临时数组
}

int main() {
    int arr[] = {1, 2, 3, 2, 4, 5, 1, 6, 3, 7};
    int size = 10;

    remove_duplicates(arr, &size);

    printf("Array after removing duplicates: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

在这个示例中,我们通过动态分配一个临时数组来存储去重后的元素。最后,将去重后的元素拷贝回原数组,并更新数组的大小。

一个简单的算法,用于移除数组中的重复元素:

int arrl = 10; // 初始数组的长度
int arr[10] = {1, 2, 2, 3, 4, 4, 5, 6, 5, 7}; // 一个示例数组,包含一些重复元素

for (int x = 0; x < arrl; x++)
{
    for (int y = x + 1; y < arrl; y++)
    {
        if (arr[x] == arr[y])  // 找到重复元素
        {
            for (int s = y; s < arrl; s++)
            {
                if (!(s + 1 == arrl)) // 如果不是最后一个元素
                    arr[s] = arr[s + 1]; // 将元素左移
            }

            arrl--;  // 更新数组长度
            y--;  // 保持当前y位置,继续检查
        }
    }
}

由于新数组的长度依赖于输入,因此必须动态分配内存:

int *newArray = malloc(arrl * sizeof(int));

上面的 newArray 数组此时包含意外的值,因此我们必须使用 memcpy 来将新的值设置到动态分配的内存块中:

memcpy(newArray, arr, arrl * sizeof(int));

安全性建议

一些安全研究人员建议总是使用 calloc(x, y) 而不是 malloc(x * y),有两个原因:

  1. 许多 calloc() 的实现会仔细检查 xy 参数,如果 "x * y" 可能溢出,则返回 NULL。而使用 malloc(x * y) 时,乘法 "x * y" 可能会溢出,导致结果为 0 或其他过小的数字,通常会导致缓冲区溢出。

  2. calloc 确保缓冲区完全为空,避免了一些类型的安全漏洞(但不幸的是,这并没有防止 Heartbleed 漏洞的发生)。

错误检查

当我们使用 malloc 时,必须注意程序员可用的内存池是有限的。即使现代 PC 至少有一整个千兆字节的内存,但仍然可能耗尽内存!在这种情况下,malloc 会返回 NULL。为了防止程序因没有更多内存可用而崩溃,应该始终检查 malloc 是否返回了 NULL,我们可以通过以下方式进行检查:

int *pt = malloc(3 * sizeof(int));
if (pt == NULL)
{
    fprintf(stderr, "Out of memory, exiting\n");
    exit(1);  // 内存不足,退出程序
}

当然,如上例所示,突然退出程序并不总是合适的,这取决于你要解决的问题以及你所编程的架构。例如,如果程序是一个小型、非关键的桌面应用程序,退出程序可能是合适的。然而,如果程序是一个桌面上的编辑器,你可能希望给用户一个选项,保存他们费劲录入的信息,而不是直接退出程序。嵌入式处理器(如洗衣机中的处理器)中的内存分配失败可能导致设备自动重置。因此,许多嵌入式系统设计师完全避免使用动态内存分配。

calloc 函数

calloc 函数为一个对象数组分配空间,并将内存初始化为零。调用 mArray = calloc(count, sizeof(struct V))count 个对象分配空间,每个对象的大小足以包含一个 struct V 结构实例。该空间初始化为全零。该函数返回分配的内存的指针,或者如果分配失败,则返回 NULL

realloc 函数

void *realloc(void *ptr, size_t size);

realloc 函数改变由 ptr 指向的对象的大小为 size 指定的大小。对象的内容将保持不变,直到新旧大小中的较小者。如果新大小更大,则新分配部分的值是不确定的。如果 ptr 是空指针,realloc 的行为就像 malloc 一样,分配指定大小的内存。否则,如果 ptr 并不是之前通过 callocmallocrealloc 返回的指针,或者该空间已经通过 freerealloc 释放,则行为未定义。如果无法分配空间,则 ptr 指向的对象不变。如果 size 为零且 ptr 不是空指针,则该对象被释放。realloc 函数返回一个可能移动后的分配对象的指针,或者返回空指针。

free 函数

使用 mallocrealloccalloc 分配的内存必须在不再需要时释放回系统内存池。这是为了避免不断分配更多的内存,最终导致内存分配失败。未释放的内存通常会在程序终止时由大多数操作系统释放。然而,未释放的内存会导致内存泄漏。以下是释放内存的一个例子:

int *myStuff = malloc(20 * sizeof(int)); 
if (myStuff != NULL) 
{
   /* 这里更多的语句 */
   /* 释放 myStuff */
   free(myStuff);
}

free 和递归数据结构

需要注意的是,free 既不智能也不递归。以下代码依赖于递归地对结构体的内部变量应用 free,但无法正确工作:

typedef struct BSTNode 
{
   int value; 
   struct BSTNode* left;
   struct BSTNode* right;
} BSTNode;

// 后续代码...

BSTNode* temp = calloc(1, sizeof(BSTNode));
temp->left = calloc(1, sizeof(BSTNode));

// 后续代码...

free(temp); // 错误!不要这样做!

语句 free(temp); 不会释放 temp->left,导致内存泄漏。正确的做法是定义一个函数,递归地释放数据结构中的每个节点:

void BSTFree(BSTNode* node)
{
    if (node != NULL) {
        BSTFree(node->left);
        BSTFree(node->right);
        free(node);
    }
}

由于 C 语言没有垃圾回收器,C 程序员必须确保每次调用 malloc() 时,都会正确调用一次 free()。如果树是一个节点一个节点分配的,那么就需要一个节点一个节点地释放它。

不要释放未定义的指针

此外,当指针在第一次分配时就未分配任何内存,但又调用 free,通常会导致崩溃或在后续代码中引发神秘的 bug。

为了避免这个问题,声明指针时应始终对其进行初始化。可以在声明时使用 calloc(如本章中的大多数示例),或者将其设置为 NULL(如本章中的 "延迟分配" 示例)。

编写构造函数/析构函数

一种正确初始化和销毁内存的方法是模仿面向对象编程。在这个范式中,对象是在分配原始内存后构建的,活跃一段时间后,在对象销毁时,使用一个特殊的析构函数来销毁对象的内部数据。

例如:

#include <stdlib.h> /* 需要 malloc 和其他相关函数 */

/* 定义我们有的对象类型,包含一个整数成员 */
typedef struct WIDGET_T {
  int member;
} WIDGET_T;

/* 处理 WIDGET_T 的函数 */

/* 构造函数 */
void WIDGETctor(WIDGET_T *this, int x)
{
  this->member = x;
}

/* 析构函数 */
void WIDGETdtor(WIDGET_T *this)
{
  /* 在这个例子中,我实际上不需要做什么,
     但如果 WIDGET_T 包含内部指针,它们指向的对象
     会在这里被销毁。 */
  this->member = 0;
}

/* 创建函数 - 此函数返回一个新的 WIDGET_T */
WIDGET_T *WIDGETcreate(int m)
{
  WIDGET_T *x = 0;

  x = malloc(sizeof(WIDGET_T));
  if (x == 0)
    abort(); /* 没有内存 */
  WIDGETctor(x, m);
  return x;
}

/* 销毁函数 - 调用析构函数,然后释放对象 */
void WIDGETdestroy(WIDGET_T *this)
{
  WIDGETdtor(this);
  free(this);
}

/* 代码结束 */
Last modified: Sunday, 12 January 2025, 1:27 PM