在 J 语言(APL 的一种方言)的实现中使用了一种数据结构,它可以容纳动态分配的任意维数组。它的数据结构使用了struct 和动态数组的混合体,这种技巧通常被称为struct hack。 (有关 J 实现的更多信息 here 和 here。)
要在一个简单的上下文中查看这个想法,请考虑一维情况:我们想要一个动态的一维数组,它带有它的大小。所以:
struct vec { int n; int p[]; };
由于p 成员是最后一个成员,并且C 没有内置的边界检查,它可以用于访问struct 的末尾的额外内存。当然,在分配时,我们需要提供这个额外的内存,而不是简单地分配struct 的大小。 struct 只是数组的 header。 C90 要求 p[] 数组的长度为一个数字(例如 1),但 C99 允许省略该数字,因此标头的大小更易于计算。
因此,具有更多维度的数组将需要更多值来保存每个维度的大小。为了让我们的结构适应不同维度的数组,这个维度向量也需要是可变长度的。
为了实现这一切,我们可以做的是两次应用 struct hack,递归地应用到自身上。这为我们提供了这样的内存布局,其中R 是我们称之为数组的 rank 的维数,D 值是每个维的长度,而V 值是实际的数组数据:
1 R Product(D)
--- -------------------- -----------------------------
R D[0] D[1] ... D[R-1] V[0] V[1] ... V[Product(D)-1]
用 C 来描述这个,
typedef struct arr { int r; int d[]; } *arr;
数组a 的元素紧跟在dims 向量D 的R 元素之后。所以V 元素可以在a->d[r+0], a->d[r+1], ... a->d[r+i] 访问(在将索引向量 减少到展平表示上的单个索引之后)。这些元素最容易按行优先顺序处理。实际元素的数量是所有维度的乘积,所有维度相乘。 编辑:这里的表达式可能写得更好:(a->d+a->r)[0], (a->d+a->r)[1], ... (a->d+a->r)[i]。
为了分配这些东西之一,我们需要一个函数来计算这个产品作为大小计算的一部分。
int productdims(int rank, int *dims){
int z=1;
for(int i=0; i<rank; i++)
z *= dims[i];
return z;
}
而要初始化,我们只需要填写成员。
arr makearr(int rank, int *dims){
arr z = calloc( (sizeof(struct arr)/sizeof(int)) +
rank + productdims(rank,dims), sizeof(int));
z->r = rank;
memmove(z->d,dims,rank*sizeof(int));
return z;
}
记住使用单个索引访问 2D 数据(例如 [m][n] 个元素的数组)的公式(它是问题中的典型动态数组)。元素 [i][j] 位于 i×n+j。对于 3D 数组 [m][n][o],元素 [i][j][k] 位于 i×(n×o)+j×o+k。
因此,我们可以从索引数组和维度数组中为线性布局的数据计算单个索引。
int *elem(arr a, ...){
va_list ap;
int idx = 0;
va_start(ap,a);
if (a->r){
idx = va_arg(ap,int);
for(int i=1; i<a->r; i++){
idx *= a->d[i];
idx += va_arg(ap,int);
}
}
va_end(ap);
return &a->d[a->r + idx];
}
不要忘记标题:
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
然后瞧:
int main() {
{
int i,n=6;
arr m = makearr(1, (int[]){n});
for (i=0;i<n;i++)
*elem(m,i) = i;
for (i=0;i<n;i++,printf(" "))
printf("%d",*elem(m,i));
}
puts("\n");
{
int i,j,n=4;
arr m = makearr(2, (int[]){n,n});
for (i=0;i<n;i++)
for (j=0;j<n;j++)
*elem(m,i,j) = i*n+j;
for (i=0;i<n;i++,printf("\n"))
for (j=0;j<n;j++,printf(" "))
printf("%d",*elem(m,i,j));
}
puts("\n");
{
int i,j,k,n=3;
arr m = makearr(3, (int[]){n,n,n});
for (i=0;i<n;i++)
for (j=0;j<n;j++)
for (k=0;k<n;k++)
*elem(m,i,j,k) = (i*n+j)*n+k;
for (i=0;i<n;i++,printf("\n"))
for (j=0;j<n;j++,printf("\n"))
for (k=0;k<n;k++,printf(" "))
printf("%d",*elem(m,i,j,k));
}
return 0;
}
输出:
0 1 2 3 4 5
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
0 1 2
3 4 5
6 7 8
9 10 11
12 13 14
15 16 17
18 19 20
21 22 23
24 25 26
此代码的进一步阐述以支持 2D 矩阵乘法和列切片已发布到 comp.lang.c in this thread。
Better-motivated question/more powerful data structure.