在C中有效地(并正确地!)动态分配2D数组
#c #cpp

介绍

像许多其他编程语言一样,C支持多维阵列(矩阵)。但是,它对它使用了非常规的语法:

int a2d[3][3];  // not: a2d[3,3]

也就是说,我们将每个维度放在其自己的[]对中。从概念上讲,将2D矩阵视为数组数组。在引擎盖下,记忆是(总是)一维的。为了实现二维阵列的syntactic sugar,编译器实际上是这样做的:

int *const a1d = &a2d[0][0];

a2d[i][j] = 0;  // really: a1d[ i * 3 + j ]

是在C中,所有元素都是连续分配的,以使a2d[i][0]a2d[i][1]在给定的行 i 的内存中相邻::

2d matrix layout

(这被称为row-major order。)鉴于,编译器可以通过将 i 乘以行的长度来计算a2d[i][j]的地址(在这种情况下为3),然后添加 j 所有由sizeof(T)缩放为t是元素类型。

这一切都可以很好地适用于静态分配的2D矩阵,其中尺寸在编译时已知,但是如果我们不知道编译时的尺寸怎么办?然后,我们必须动态分配它。但是C所拥有的只是malloc(),它只是分配给定的字节数。我们如何分配正确数量的字节使[][]语法仍然有效?

C中的数组和指针

在阵列和指针方面,C有几个古怪的特征:

  1. 表达式中的数组的名称衰减到指向其第一个元素的指针中。例如:

    int a1d[3];
    int *p = a1d;  // as if: p = &a1d[0]
    

    也就是说,a1d的名称是&a1d[0]的速记。

  2. 数组的a1d[i]语法只是*(a1d+i)的句法糖。也就是说,以a1d(这是其第一个元素的地址),将sizeof i 元素添加到该地址,然后返回 element。 >

  3. 对于任何指针p等效地,*p也可以写为p[0]。这种等效性允许动态分配一个维数数组和具有[]语法仍然有效:

    int *p3 = malloc( 3 * sizeof(int) );
    // ...
    p3[i] = 42;    // as if: *(p3 + i) = 42;
    

    这种等价是对新B(C的前体)如何指定指针的残余。参见The Development of the C Language,Dennis M. Ritchie,1993年4月。

这些古怪的功能允许通过动态分配的多维数组和具有[][]语法仍然可行。但是如何?

初始实现

窍门不是直接分配元素矩阵,而是分配一个 i pointers的数组(每行一个),其中每个指针指向 j 该行的元素:

2D matrix v1

执行此操作的代码是:

void** matrix2_new( size_t esize, size_t idim, size_t jdim ) {
  size_t const ptrs_size = sizeof(void*) * idim;
  size_t const row_size = esize * jdim;
  void **const rows = malloc( ptrs_size );
  for ( size_t i = 0; i < idim; ++i )
    rows[i] = malloc( row_size );
  return rows;
}

给定:

int **m2d = matrix2_new( sizeof(int), 3, 3 );
// ...
m2d[i][j] = 42;    // as if: *(*(m2d + i) + j) = 42

也就是说,m2d[i]产生了一个指向 ith int的行阵列,然后我们通过 j 。。

但是,虽然此实现很简单,但此实现的问题是它需要 i + 1呼叫malloc(),我们需要编写一个相应的matrix2_free()函数,该函数将free()均等称为次数。

更好的实施

如果您查看第一个插图,您可能会意识到,没有理由将行阵列融为一部分记忆。行指针只需要指向每行的[i][0]元素:

2D matrix v2

执行此操作的代码是:

void** matrix2_new( size_t esize, size_t idim, size_t jdim ) {
  size_t const ptrs_size = sizeof(void*) * idim;
  size_t const row_size = esize * jdim;
  void **const rows = malloc( ptrs_size );
  char *const elements = malloc( idim * row_size );
  for ( size_t i = 0; i < idim; ++i )
    rows[i] = &elements[ i * row_size ];
  return rows;
}

elements宣布为char*而不是void*是必要的,因为我们可以使用void*进行指针算术。我们需要char*,特别是因为我们根据字节来工作。

这是更好的,因为我们现在只需要对malloc()的两个调用,但是我们仍然需要编写一个相应的matrix2_free()函数。

最佳实施

如果您查看第二个插图,您最终可能会怀疑是否可以将ROW POINTERS 元素合并为一个记忆的一个。是的,他们可以,但是有一个陷阱:

2D matrix v3

陷阱显示为行指针和元素之间的阴影区域。这是填充,可以确保元素正确地为aligned

具体来说,&elements[0][0] 必须正确对齐 - 这意味着ptrs_size 必须被四舍五入为sizeof(T)的倍数。 (我看到的许多实现忽略了这一点,因为sizeof(T)sizeof(void*),碰巧碰巧工作了。)

执行此操作的代码与适当的对齐方式为:

size_t round_up_to( size_t n, size_t multiple ) {
  size_t const remainder = n % multiple;
  return remainder == 0 ? n : n + multiple - remainder;
}

void** matrix2_new( size_t esize, size_t ealign, size_t idim,
                    size_t jdim ) {
  // ensure &elements[0][0] is suitably aligned
  size_t const ptrs_size = round_up_to( sizeof(void*) * idim, ealign );
  size_t const row_size = esize * jdim;
  // allocate the row pointers followed by the elements
  void **const rows = malloc( char, ptrs_size + idim * row_size );
  char *const elements = (char*)rows + ptrs_size;
  for ( size_t i = 0; i < idim; ++i )
    rows[i] = &elements[ i * row_size ];
  return rows;
}

请注意,该函数采用了一个新的ealign参数,该参数是_Alignof(T)(或包括stdalign.halignof(T)):

int **m2d = matrix2_new( sizeof(int), alignof(int), 3, 3 );

这是最好的实现,因为我们现在只需要一个呼叫malloc(),我们不需要特殊的matrix2_free()函数:普通的free()会做。

结论

C中的数组和指针之间的古怪相互作用允许动态分配多维数组,而仍然允许[][]语法工作。

请注意,我们可以将matrix3d_new()编写,以动态分配3D矩阵,通过使用一系列指针来对元素的指针阵列进行分配;依此类推,以供任何数量的维度;甚至完全通用的matrixNd_new()函数将维度数作为参数。 (这些是读者的练习。)