【问题标题】:How can I work with dynamically-allocated arbitrary-dimensional arrays?如何使用动态分配的任意维数组?
【发布时间】:2015-07-13 11:15:30
【问题描述】:

典型的一维数组可以在声明中静态或自动分配。

enum { n=100 };
int arr1[n];

或者通过指针动态分配和访问。

int *arr1m=malloc(n*sizeof*arr1m);
int *arr1c=calloc(n, sizeof*arr1c);

这两种样式都使用相同的语法访问元素。

int i = n/2;
arr1[i] = arr1c[i] = arr1m[i] = 42;

但是当你添加第二个维度时,要实现相同的语法需要一些努力。

int arr2[n][n];
int *arr2c=calloc(n*n,sizeof*arr2c);
arr2[5][5] = arr2c[5*n+5] = 23;

如果将其构造为Iliffe-vector,则只能获得双括号。

int **arr2l=calloc(n,sizeof*arr2l);
for (int j=0; j<n; j++)
    arr2l[j]=calloc(n,sizeof**arr2l);
 arr2[6][6] = arr2l[6][6] = 72;

但是随着维度的增加,这变得越来越麻烦。

另一个困难是在访问元素之前检查动态数组的边界(这样您就不会触及未正确分配的内存)。 real 数组可以使用sizeof 运算符来确定边界,但这些动态数组中没有一个带有它们的大小。

如何定义一个结构,它具有像数组一样快速、连续的布局,但使用一致的语法来访问具有索引列表的元素,这对于 2D 数组和 3D 数组的工作方式相同;并且都是动态的,大小是动态可用的,因此它们可以传递给函数并从函数返回?

【问题讨论】:

  • 本材料是在comp.lang.c 的优秀伙伴的帮助下编辑的。
  • 拜托,拜托,不要使用alloca,这属于历史书籍。这在“堆栈”上进行分配,因此与标准化方法的可变长度数组 (VLA) 相比,它根本没有优势。
  • @JensGustedt 好的。我已经删除了它。不想给任何人意见。
  • @JensGustedt:VLA 从 C11 开始是可选的。可能有支持alloca 但不支持VLA 的C 实现。 (这并不一定意味着我不同意你的观点。)请注意,VLA 和 alloca 都存在不报告分配失败的缺陷。
  • @KeithThompson,是的,但我不知道没有 VLA 的 C11 的任何实现,我怀疑永远不会有。

标签: c arrays multidimensional-array


【解决方案1】:

不用重新发明轮子,C99以后就有了,叫做变长数组,VLA。它的语法与“普通” d 维数组一样,只是边界可能是可变的,并且在文件范围内是不允许的。

由于此类对象可能会变得相对较大,因此您不应将它们分配在堆栈上,而应使用 malloc 之类的东西

double (*A)[n][m] = malloc(sizeof(double[k][n][m]));

然后,编译器会帮助您毫无问题地进行所有索引计算。如果你想将这些动物传递给函数,你只需要先小心声明边界:

void func(size_t k, size_t n, size_t m, double A[k][n][m]);

这让人类读者和编译器都清楚您的意图。我更喜欢这个而不是等效的形式

void func(size_t k, size_t n, size_t m, double (*A)[n][m]);

【讨论】:

    【解决方案2】:

    如果您将a 定义为指向n 整数数组的指针,编译器将执行索引运算。

    #define N 7
    int (*a)[N];
    
    int main() {
      a = malloc(N*N*sizeof(int));
      a[2][3] = 0;
    }
    

    添加:

    同样,一个三维的例子:

    #include <stdio.h>
    #include <stdlib.h>
    
    #define N 7
    
    int (*a)[N][N];
    
    int main() {
        int i,j,k;
        a = malloc(N*N*N*sizeof(int));
        for(i=0; i<N; i++) {
            for(j=0;j<N;j++) {
                for(k=0;k<N;k++) {
                    a[i][j][k] = (i*10+j)*10+k;
                }
            }
        }
        for(i=0; i<N; i++) {
            for(j=0;j<N;j++) {
                for(k=0;k<N;k++) {
                    printf("%2d ", a[i][j][k]);
                }
                printf("\n");
            }
            printf("\n");
        }
    }
    

    【讨论】:

    • 嗯。但它看起来如何 3D?
    • 我认为分配看起来更简单。例如malloc(sizeof(int[n][n][n])).
    • 嗯。毕竟这还不错。我认为封装大小对于编写对这些数组进行操作的函数很有用,但我的示例没有显示这一点。 +1 向我展示了一些新东西。
    【解决方案3】:

    在 J 语言(APL 的一种方言)的实现中使用了一种数据结构,它可以容纳动态分配的任意维数组。它的数据结构使用了struct 和动态数组的混合体,这种技巧通常被称为struct hack。 (有关 J 实现的更多信息 herehere。)

    要在一个简单的上下文中查看这个想法,请考虑一维情况:我们想要一个动态的一维数组,它带有它的大小。所以:

    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 向量DR 元素之后。所以V 元素可以在a-&gt;d[r+0], a-&gt;d[r+1], ... a-&gt;d[r+i] 访问(在将索引向量 减少到展平表示上的单个索引之后)。这些元素最容易按行优先顺序处理。实际元素的数量是所有维度的乘积,所有维度相乘。 编辑:这里的表达式可能写得更好:(a-&gt;d+a-&gt;r)[0], (a-&gt;d+a-&gt;r)[1], ... (a-&gt;d+a-&gt;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&plus;j。对于 3D 数组 [m][n][o],元素 [i][j][k] 位于 i×(n×o)&plus;j×o&plus;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.

    【讨论】:

    • 由于 C99 和 C11 具有灵活的数组成员,因此您不需要像以前那样使用 struct hack。
    • 是的。您认为删除1 会更清楚吗?我在 main 中使用 C99 复合文字,但我认为如果与 C90 兼容,其他代码可能会更有用。
    • 这取决于您想向那些不得不在 Windows 上使用 Microsoft 编译器的人磕头的程度。 MSVC 不支持 C99,更不用说 C11,但它基本上是唯一的支持者。
    • 这不仅是标准化的 C99,因此更适合可移植性。这也是“结构黑客”可能会出错:根据您放在那里的数字,结构内的填充可能因编译器版本而异。
    • 我不认为 VLA 和 stdarg 有任何交互;他们是独立的。您可以编写 int ind[a-&gt;r]; 来定义 VLA,而不会出现任何问题。顺便说一句,不支持 VLA 的标准 C11 编译器将 __STDC_NO_VLA__ 定义为 1 以表示不支持(第 6.10.8.3 节)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-02
    • 2017-03-18
    • 1970-01-01
    • 2013-11-24
    • 2020-04-24
    • 1970-01-01
    相关资源
    最近更新 更多