如何为多维数组分配内存?

How do I allocate memory for a multidimensional array?

提问人:Cheatah 提问时间:9/22/2021 更新时间:9/22/2021 访问量:62

问:

如何使用为多维数组分配内存?malloc

例如,您希望使用 .arr[6][9]

您可能已经尝试了以下方法:

// Warning: broken example
int **arr = malloc(50 * sizeof(int));

arr[6][9] = 42; // dangerous! Segmentation fault (core dumped)

这显然是错误的。但是,为多维数组分配(和释放)内存的正确方法是什么?

C 多维阵列 malloc

评论


答:

-1赞 Cheatah 9/22/2021 #1

最基本的多维数组当然是二维数组。 它有两个维度,在此示例中,我将使用 size by 的数组。 为简单起见,我使用整数类型来存储数据。存储类型与要使用的常规技术无关。xy

为清楚起见,在前几个示例中跳过了任何错误检查。 后面的示例包括一些基本形式的错误检查。 该类型用于索引偏移量,以避免与存储在多维数组中的类型(整数)混淆。size_t

基本 2D 示例

/*
 * Warning: no error checks!
 */ 
int **create_2d(size_t x, size_t y)
{
    int *values = malloc(x * y * sizeof *values);
    int **index_x = malloc(x * sizeof *index_x);

    for (size_t i = 0; i < x; i++)
        index_x[i] = &values[i * y];

    return index_x;
}

您现在可以读取和写入 2D 数组中的所有位置,只要您不低于或超过,因为这将越界访问数组。0xy

int **arr = create_2d[20][24];

arr[6][9] = 42; // perfectly fine!

也许你对这段代码很满意,你把它复制/粘贴到你的项目中。 这完全没问题,但风险自负。我将提供进一步的解释和一些警告。


对这一切意味着什么的一些解释。最后,多维数组需要存储 类型的行和列。这意味着所需的存储大小至少是。 在此示例中,所有必需的存储都是一次性分配的。但是,由于这更容易维护,因此应该更改存储类型。这样就不容易出错了。xyintx * y * sizeof(int)sizeof(int)sizeof *values

现在,所有内存都是“连续的”,并且可以作为 to 的偏移量进行访问。实际上,通过使用一些简单的算术,这通常已经可以用作仿二维数组。例如,您可以说索引已经可以通过 访问。第一个值是 row ,下一个值是 row ,以此类推。values[0]values[x * y](i,j)values[i * y + j];y0y1

为了真正通过索引访问它,实际上还需要分配该索引。在这种情况下,我称之为.它必须能够指向不同的内存位置,特别是每个“行”的“第一个”值。[i][j]index_xxy

很多时候,你会看到人们在循环中执行分配。这实际上没有必要,并且在错误检查和解除分配方面使事情变得更加复杂。尽管如此,为 -rows 开头分配内存位置需要在循环中完成,我将其用作迭代器值,范围从 到 。因为需要指向指针,所以我们把 的地址放在 .yi0xindex_xvalues[i * y]index_x

需要注意的是,返回的也是,而不是.如果您确实需要访问 ,仍然可以通过 来完成。当我们需要释放内存时,这将很方便。index_xvaluesvaluesindex_x[0]

基本释放 2D 示例

以下函数将增加分配的内存:free

/*
 * Warning: no error checks!
 */
void destroy_2d(int **ptr)
{
    free(ptr[0]);
    free(ptr);
}

正如你所看到的,这里不需要循环。

现在可能还不清楚为什么 with 比 use in the loop 更可取。一旦你开始添加错误检查代码,或者当你需要分配大量项目或有大量嵌套时,它应该变得很明显。同样的原理也适用于三维数组。为了清楚起见,让我演示一下 3D 阵列:malloc

基本 3D 示例

int ***create_3d(size_t x, size_t y, size_t z)
{
    int *values = malloc(x * y * z * sizeof *values);
    int **index_y = malloc(x * y * sizeof *index_y);
    int ***index_x = malloc(x * sizeof *index_x);

    for (size_t i = 0; i < x; i++) {
        index_x[i] = &index_y[i * y];
        for (size_t j = 0; j < y; j++) {
            // remove ONE of the following two lines
            index_x[i][j] = &values[(i * y + j) * z]; // or, alternatively:
            index_y[i * y + j] = &values[(i * y + j) * z]; // this is exactly the same
        }
    }

    return index_x;
}

void destroy_3d(int ***ptr)
{
    free(ptr[0][0]);
    free(ptr[0]);
    free(ptr);
}

这是相同的原理,尽管算术更复杂一些。


让我通过添加非常基本的错误检查来向您展示为什么这很重要:

带错误检查的基本 3D 示例

int ***create_3d_e(size_t x, size_t y, size_t z)
{
    int *values = malloc(x * y * z * sizeof *values);
    if (!values)
        return NULL;

    int **index_y = malloc(x * y * sizeof *index_y);
    if (!index_y) {
        free(values);
        return NULL;
    }

    int ***index_x = malloc(x * sizeof *index_x);
    if (!index_x) {
        free(index_y);
        free(values);
        return NULL;
    }

    for (size_t i = 0; i < x; i++) {
        index_x[i] = &index_y[i * y];
        for (size_t j = 0; j < y; j++) {
            index_y[i * y + j] = &values[(i * y + j) * z];
        }
    }

    return index_x;
}

或者,如果您更喜欢不同的代码样式:

int ***create_3d_g(size_t x, size_t y, size_t z)
{
    int *values;
    int **index_y;
    int ***index_x;
    size_t i, j;

    values = malloc(x * y * z * sizeof *values);
    if (!values)
        goto err;

    index_y = malloc(x * y * sizeof *index_y);
    if (!index_y)
        goto err_y;

    index_x = malloc(x * sizeof *index_x);
    if (!index_x)
        goto err_x;

    for (i = 0; i < x; i++) {
        index_x[i] = &index_y[i * y];
        for (j = 0; j < y; j++) {
            index_y[i * y + j] = &values[(i * y + j) * z];
        }
    }

    return index_x;

err_x:
    free(index);
err_y:
    free(values);
err:
    return NULL;
}

然后是释放时的一些基本错误防止逻辑:

带错误检查的基本释放 3D 示例

void destroy_3d_e(int ***ptr)
{
    if (ptr) {
        if (ptr[0]) {
            free(ptr[0][0]);
            free(ptr[0]);
        }
        free(ptr);
    }
}

这是不在循环中分配内存的另一个优点!在这种情况下,“destroy”函数还应该知道循环中的维度和所有分配。当某些分配在嵌套多维数组的循环中途失败时,增加了复杂性。使程序崩溃并不总是一种选择,您可能希望或需要释放内存以防止讨厌的错误。也就是说,释放“连续”内存比“loop-malloc”方法容易得多。我没有为此提供示例,因为我认为这没有帮助。如果其他人想单独回答,请这样做,并有适当的保留意见。free


作为读者的练习:尝试为三维数组实现它。在构建阵列的中途检查故障,并在不发生内存泄漏的情况下优雅地拆除所有内容。

HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
    total heap usage: 3 allocs, 3 frees, 96,481,600 bytes allocated

All heap blocks were freed -- no leaks are possible

ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

我希望将来要求这种方法的人会少得多。我希望这些例子能让你更好地理解多维数组的内部工作原理。

评论

1赞 Eric Postpischil 9/22/2021
指针到指针对于实现多维数组效率低下,不应将其作为常规技术进行教授。(它们使处理器无法进行内存查找,干扰流水线等。C 标准定义了可变长度数组,并且许多编译器都支持它们,因此只需使用 .int (*p)[y] = malloc(x * sizeof *p);
0赞 Eric Postpischil 9/22/2021
失败的是,将所谓的“假”数组分配为具有手动地址计算的一维数组比指针到指针更可取。许多处理器都具有内置支持某些地址计算的指令,并且地址计算通常可以由编译器进行部分优化和/或由处理器计算,速度比指针查找更快。因此,程序员可能需要更多的代码(可以通过内联函数或宏来缓解),但这是值得的。
0赞 kaylum 9/22/2021
正确分配多维数组。这解释了指针到指针类型数组和真正的多维数组。