跳转到内容

C 编程/内存管理

来自维基教科书,开放世界中的开放书籍
前文:指针和数组 C 编程 后文:错误处理

在 C 中,您已经考虑过创建变量供程序使用。您创建了一些用于的数组,但您可能已经注意到了一些限制

  • 数组的大小必须事先知道
  • 数组的大小在程序运行期间不能更改

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.h 或 malloc.h 中定义,具体取决于您可能使用的操作系统。Malloc.h 仅包含内存分配函数的定义,而不包含 stdlib.h 中定义的其他函数。通常,您不需要在程序中如此具体,如果两者都支持,您应该使用 <stdlib.h>,因为这是 ANSI C,也是我们将在这里使用的。

释放分配的内存回操作系统的对应调用是 free

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

让我们看看如何将动态内存分配用于数组。

通常,当我们想要创建一个数组时,我们会使用以下声明

int array[10];

回想一下,array 可以被视为一个指针,我们将其用作数组。我们指定此数组的长度为 10 个 int。在 array[0] 之后,还有九个其他整数可以连续存储。

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

我们还需要知道一个 int 在内存中占多少空间,以便为它腾出空间;幸运的是,这并不难,我们可以使用 C 的内置 sizeof 运算符。例如,如果 sizeof(int) 返回 4,则一个 int 占用 4 个字节。自然,2*sizeof(int) 是我们为 2 个 int 需要多少内存,依此类推。

那么我们如何 malloc 一个与之前类似的包含十个 int 的数组呢?如果我们希望一次声明并腾出空间,我们可以简单地说

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

我们只需要声明指针;malloc 为我们提供了一些空间来存储 10 个 int,并返回指向第一个元素的指针,该指针被分配给该指针。

重要说明! malloc 不会 初始化数组;这意味着数组可能包含随机或意外的值!就像创建没有动态分配的数组一样,程序员必须在使用数组之前用合理的值对其进行初始化。确保你也这样做。(稍后看看函数 memset 以获得一种简单的方法。

在声明用于分配内存的指针后,不必立即调用 malloc。通常,在声明和调用 malloc 之间存在许多语句,如下所示

int *array = NULL;
printf("Hello World!!!");
/* more statements */
array = malloc(10*sizeof(int)); /* delayed allocation */
/* use the array */

动态内存分配的更实用的示例如下

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

一种简单的算法来删除重复元素

    int arrl = 10; // Length of the initial array
    int arr[10] = {1, 2, 2, 3, 4, 4, 5, 6, 5, 7}; // A sample array, containing several duplicate elements
    
    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--;
            }
        }
    }

由于我们新数组的长度取决于输入,因此它必须是动态分配的

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

上面的数组目前将包含意外的值,因此我们必须使用 memcpy 将我们的动态分配内存块设置为新值

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

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

  • calloc() 的许多实现都会仔细检查 x 和 y 参数,并在 "x*y" 可能溢出时返回 NULL。使用 malloc(x*y),乘法 "x*y" 可能溢出为 0 或其他太小的数字,通常会导致缓冲区溢出。[1][2][3][4]
  • calloc 确保缓冲区完全没有敏感信息,避免某些类型的安全漏洞[5](但不幸的是,这并不能阻止 Heartbleed 漏洞)。

错误检查

[编辑 | 编辑源代码]

当我们想要使用 malloc 时,我们必须注意程序员可用的内存池是有限的。即使现代 PC 至少具有 1GB 的内存,但仍然有可能并且可以想象用完它!在这种情况下,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 分配的内存必须在不再需要时释放回系统内存池。这样做是为了避免永久分配越来越多的内存,这会导致最终的内存分配失败。但是,未用 free 释放的内存将在大多数操作系统上当前程序终止时释放。对 free 的调用如下例所示。

int *myStuff = malloc( 20 * sizeof(int)); 
if (myStuff != NULL) 
{
   /* more statements here */
   /* time to release myStuff */
   free( myStuff );
}

free 与递归数据结构

[编辑 | 编辑源代码]

需要注意的是,free 既不智能也不递归。以下代码依赖于对struct 内部变量的 free 的递归应用,但不起作用。

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

// Later: ... 

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

// Later: ... 

free(temp); // WRONG! don't do this!

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

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

由于 C 没有垃圾回收器,因此 C 程序员有责任确保对于每个 malloc() 都有一个 free()。如果树是逐个节点分配的,那么它也需要逐个节点释放。

不要释放未定义的指针

[编辑 | 编辑源代码]

此外,当所讨论的指针最初从未分配时使用 free 通常会导致崩溃或在后续导致神秘的错误。

为了避免此问题,请始终在声明指针时初始化它们。要么在声明时使用 calloc(如本章中的大多数示例所示),要么在声明时将它们设置为 NULL(如本章中的“延迟分配”示例所示)。[6]

编写构造函数/析构函数

[编辑 | 编辑源代码]

获得正确的内存初始化和销毁的一种方法是模仿面向对象的编程。在这种范式中,对象在为它们分配原始内存后被构造,然后生存,当它们需要被销毁时,一个名为析构函数的特殊函数会在对象本身被销毁之前销毁对象的内部结构。

例如

#include <stdlib.h> /* need malloc and friends */

/* this is the type of object we have, with a single int member */
typedef struct WIDGET_T {
  int member;
} WIDGET_T;

/* functions that deal with WIDGET_T */

/* constructor function */
void
WIDGETctor (WIDGET_T *this, int x)
{
  this->member = x;
}

/* destructor function */
void
WIDGETdtor (WIDGET_T *this)
{
  /* In this case, I really don't have to do anything, but
     if WIDGET_T had internal pointers, the objects they point to
     would be destroyed here.  */
  this->member = 0;
}

/* create function - this function returns a new WIDGET_T */
WIDGET_T *
WIDGETcreate (int m)
{
  WIDGET_T *x = 0;

  x = malloc (sizeof (WIDGET_T));
  if (x == 0)
    abort (); /* no memory */
  WIDGETctor (x, m);
  return x;
}

/* destroy function - calls the destructor, then frees the object */
void
WIDGETdestroy (WIDGET_T *this)
{
  WIDGETdtor (this);
  free (this);
}

/* END OF CODE */
前文:指针和数组 C 编程 后文:错误处理

参考资料

[编辑 | 编辑源代码]
华夏公益教科书