最基本的多维数组当然是二维数组。
它有两个维度,在本例中,我将使用大小为x 的数组y。
为简单起见,我使用整数类型来存储数据。存储类型与使用的一般技术无关。
为清楚起见,在前几个示例中跳过了任何错误检查。
后面的示例包括一些基本形式的错误检查。
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;
}
您现在可以读取和写入二维数组中的所有位置,只要您不低于0 或超过x 和y,因为那样会越界访问数组.
int **arr = create_2d[20][24];
arr[6][9] = 42; // perfectly fine!
也许您对此代码感到满意,并将其复制/粘贴到您的项目中。
这完全没问题,但风险自负。我将提供进一步的解释和一些警告。
对这一切意味着什么的一些解释。最后,多维数组需要存储x 行和y 类型int 的列。这意味着所需的存储大小至少为x * y * sizeof(int)。
在这个例子中,所有需要的存储都是一次性分配的。但是,使用 sizeof *values 而不是 sizeof(int),因为这更容易维护,例如应该存储类型更改。这种方式更不容易出错。
现在,所有内存都是“连续的”,可以作为从values[0] 到values[x * y] 的偏移量访问。通过使用一些简单的算术,这实际上通常已经可以用作人造二维数组。例如,您可以说索引(i,j) 已经可以通过values[i * y + j]; 访问。第一个y 值是行0,接下来的y 值是行1,等等。
要通过索引[i][j] 真正访问该索引,还需要实际分配该索引。在这种情况下,我称之为index_x。它必须能够指向 x 不同的内存位置,特别是每个“行”的“第一个”y 值。
您经常会看到人们循环执行分配。这实际上是没有必要的,并且在错误检查和释放方面使事情变得更加复杂。尽管如此,为y-rows 的开头分配内存位置需要在循环中完成,其中我使用i 作为迭代器值,范围从0 到x。因为index_x需要指向指针,所以我们把values[i * y]的地址放在index_x里面。
需要注意的是,返回的也是index_x,而不是values。如果您实际上需要访问values,仍然可以通过index_x[0] 完成。当我们需要释放内存时,这会很方便。
基本释放 2D 示例
以下函数将free向上分配的内存:
/*
* Warning: no error checks!
*/
void destroy_2d(int **ptr)
{
free(ptr[0]);
free(ptr);
}
如您所见,这里不需要循环。
现在可能不清楚为什么 with 比在循环中使用 malloc 更可取。一旦你开始添加错误检查代码,或者当你需要分配很多项目或有很多嵌套时,它就会变得很明显。相同的原理适用于 3 维数组。为了清楚起见,让我演示一下 3D 数组:
基本 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);
}
}
这是不在循环内分配内存的另一个优点!在这种情况下,“销毁”函数还应该知道维度和free 循环中的所有分配。当嵌套多维数组的循环中某些分配中途失败时,增加了复杂性。崩溃您的程序并不总是一种选择,您可能希望或需要释放内存以防止令人讨厌的错误。那时释放“连续”内存比“循环-malloc”方法容易得多。我没有为此提供示例,因为我认为这不会有帮助。如果其他人想将其作为单独的答案提供,请提供相应的保留意见。
作为读者练习:尝试为 3 维数组实现它。在构建数组的中途检查失败,并优雅地拆除所有内容而不会出现内存泄漏。
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)
我希望将来要求这种方法的人要少得多。我希望这些示例能让您更好地了解多维数组的内部工作原理。