将指向类型的指针转换为指向该类型数组的指针是否安全?

6
几天前,我偶然发现了一段代码,其中广泛使用了从指针类型类型数组的指针的转换,以在内存中呈现线性向量的二维视图。为了清晰起见,下面报告了这种技术的一个简单示例:
#include <stdio.h>
#include <stdlib.h>

void print_matrix(const unsigned int nrows, const unsigned int ncols, double (*A)[ncols]) {  
  // Here I can access memory using A[ii][jj]
  // instead of A[ii*ncols + jj]
  for(int ii = 0; ii < nrows; ii++) {
    for(int jj = 0; jj < ncols; jj++)
      printf("%4.4g",A[ii][jj]);
    printf("\n");
  }
}

int main() {

  const unsigned int nrows = 10;
  const unsigned int ncols = 20;

  // Here I allocate a portion of memory to which I could access
  // using linear indexing, i.e. A[ii]
  double * A = NULL;
  A = malloc(sizeof(double)*nrows*ncols);

  for (int ii = 0; ii < ncols*nrows; ii++)
    A[ii] = ii;

  print_matrix(nrows,ncols,A);
  printf("\n");
  print_matrix(ncols,nrows,A);

  free(A);
  return 0;
}

考虑到指向类型的指针与指向类型数组的指针不兼容,我想问一下这种转换是否存在风险,或者我能否假设这种转换在任何平台上都能按预期工作。

问题实际上是一个二维数组是否完全连续,即每行末尾是否没有填充。我在C11标准中找不到这样的保证。无论如何,这两种类型是不兼容的,因此你不能在没有指针转换(发抖)的情况下使用-Werror编译。 - Fred Foo
@larsmans 你说得对。实际上,真正的代码在调用函数时进行了显式转换,以便在编译时允许使用“-Wall -Werror”。我还查看了当前标准草案(第6.2节和6.3节),但没有找到任何相关内容。 - Massimiliano
阅读C11草案,我认为通过阅读6.5.2.1数组下标的内容,有一个连续性的假设基础。但是我不够精通语言法律,不能确定。 - user1697014
@Wegge:在我的回答中添加了对该部分的引用;不幸的是,据我所知,它并不支持我的解释。 - Fred Foo
4个回答

2

更新:删除线部分是真实的,但不相关。

如我在评论中发布的那样,问题实际上是一个二维数组是否包含内部填充。 每一行内肯定没有填充,因为标准定义数组是连续的。此外,外层数组也不会引入任何填充。事实上,在查阅C标准时,我发现在数组的上下文中没有提到填充,因此我解释"连续"意味着多维数组中子数组末尾永远没有填充。由于 sizeof(array) / sizeof(array [0]) 保证返回数组中元素的数量,因此不可能有这种填充。

这意味着nrows行和ncols列的多维数组的布局必须与nrows * ncols的一维数组相同。因此,为避免不兼容类型错误,您可以执行以下操作

void *A = malloc(sizeof(double[nrows][ncols]));
// check for NULL

double *T = A;
for (size_t i=0; i<nrows*ncols; i++)
     T[i] = 0;

然后传递给print_array。这应该避免指针别名的潜在陷阱; 不同类型的指针不允许指向同一个数组,除非它们中至少有一个具有void*char*unsigned char*类型。


2
有所帮助的是第6.5.3.4(7)段落的第二个例子:“ sizeof运算符的另一个用法是计算数组中元素的数量:sizeof array / sizeof array [0]”。诚然,例如脚注一样,只是提供信息而非规范,但意图明确。 - Daniel Fischer
@DanielFischer:我真应该为没想到那个点子而自责。谢谢! - Fred Foo
aligned_alloc 仅适用于超对齐类型;7.22.3p1 确保 malloc 的返回值对任何基本要求都进行了对齐。 - ecatmur

1

保证多维数组 T arr[M][N] 与具有相同元素总数的单维数组 T arr[M * N] 具有相同的内存布局。这是因为数组是连续的(6.2.5p20),并且因为 sizeof array / sizeof array[0] 保证返回数组中元素的数量(6.5.3.4p7)。

然而,将指向类型的指针强制转换为指向类型数组的指针,或者反过来,并不意味着这样做是安全的。首先,对齐是一个问题;虽然具有基本对齐方式的类型的数组也必须具有基本对齐方式(根据6.2.8p2),但不能保证对齐方式相同。因为数组包含基本类型的对象,所以数组类型的对齐方式必须至少与基本对象类型的对齐方式一样严格,但它可以更严格(我从未见过这种情况)。然而,对于分配的内存来说,这与相关性不大,因为malloc保证返回适当分配给任何基本对齐方式的指针(7.22.3p1)。这意味着您不能将指向自动或静态内存的指针安全地强制转换为数组指针,尽管反过来是允许的:

int a[100];
void f() {
    int b[100];
    static int c[100];
    int *d = malloc(sizeof int[100]);
    int (*p)[10] = (int (*)[10]) a;  // possibly incorrectly aligned
    int (*q)[10] = (int (*)[10]) b;  // possibly incorrectly aligned
    int (*r)[10] = (int (*)[10]) c;  // possibly incorrectly aligned
    int (*s)[10] = (int (*)[10]) d;  // OK
}

int A[10][10];
void g() {
    int B[10][10];
    static int C[10][10];
    int (*D)[10] = (int (*)[10]) malloc(sizeof int[10][10]);
    int *p = (int *) A;  // OK
    int *q = (int *) B;  // OK
    int *r = (int *) C;  // OK
    int *s = (int *) D;  // OK
}

接下来,不能保证在数组类型和非数组类型之间进行强制转换实际上会导致指针指向正确的位置,因为强制转换规则(6.3.2.3p7)不包括此用法。尽管这种情况发生的可能性非常小,但通过char *进行的转换确实具有保证的语义。当从指向数组类型的指针到指向基本类型的指针时,最好只是间接引用该指针:

void f(int (*p)[10]) {
    int *q = *p;                            // OK
    assert((int (*)[10]) q == p);           // not guaranteed
    assert((int (*)[10]) (char *) q == p);  // OK
}

数组下标的语义是什么?众所周知,[]操作只是加法和间接寻址的语法糖,因此其语义与+运算符相同;正如6.5.6p8所描述的那样,指针操作数必须指向足够大的数组成员,以使结果落在数组内或刚好超出末尾。这对于两个方向的强制转换都是一个问题;当转换为数组类型的指针时,加法是无效的,因为该位置不存在多维数组;而当转换为基础类型的指针时,该位置上的数组仅具有内部数组绑定的大小:

int a[100];
((int (*)[10]) a) + 3;    // invalid - no int[10][N] array

int b[10][10];
(*b) + 3;          // OK
(*b) + 23;         // invalid - out of bounds of int[10] array

这是我们开始看到实际的常见实现问题,而不仅仅是理论。因为优化器有权假定未定义的行为不会发生,所以通过基对象指针访问多维数组可以假定不会别名任何在第一个内部数组之外的元素:
int a[10][10];
void f(int n) {
    for (int i = 0; i < n; ++i)
        (*a)[i] = 2 * a[2][3];
}

优化器可以假设对于 a[2][3] 的访问不会与 (*a)[i] 产生别名,并将其提升到循环外部:
int a[10][10];
void f_optimised(int n) {
    int intermediate_result = 2 * a[2][3];
    for (int i = 0; i < n; ++i)
        (*a)[i] = intermediate_result;
}

如果使用n = 50调用f,这当然会产生意想不到的结果。

最后值得问一下,这是否适用于分配的内存。7.22.3p1指定malloc返回的指针“可以分配给具有基本对齐要求的任何类型对象的指针,然后用于访问在分配的空间中分配的此类对象或此类对象的数组”;没有关于将返回的指针进一步转换为另一个对象类型的内容,因此结论是分配的内存类型由返回的void指针转换为的第一个指针类型固定;如果你将其转换为double *,那么就不能进一步转换为double (*)[n],如果你将其转换为double (*)[n],则只能使用double *来访问前n个元素。

因此,我认为如果您想要绝对安全,即使基础类型相同,也不应该在指针和数组类型之间进行转换。布局相同这一事实除了通过char指针进行的memcpy和其他访问之外是无关紧要的。


1

C标准允许将指向对象(或不完整)类型的指针转换为指向不同对象(或不完整)类型的指针。

但是有一些注意事项:

  • 如果结果指针未正确对齐,则行为是未定义的。标准不保证在这种情况下的行为。实际上,这很不可能发生。

  • 标准仅规定了结果指针的一个有效用途,即将其转换回原始指针类型。在这种情况下,标准保证后者(将结果指针转换回原始指针类型)将与原始指针相等。将结果指针用于其他任何事情都不在标准范围内。

  • 标准要求在执行此类转换时进行显式转换,而您发布的代码中的print_matrix函数调用缺少此转换。

因此,根据标准的规定,代码示例中的使用超出了其范围。但是,在实践中,这在大多数平台上可能会正常工作-假设编译器允许它。


0

我的第一个想法是,当创建一个二维数组时,C确实使用了这种实现方式 - 它将内存线性地拉伸:

[11, 12, 13, 14, 15, 21, 22, 23, 24, 25....] // This is known as ROW-MAJOR form

它在你的代码中分配的方式

A = malloc(rows*columns);

因此,我认为这样做没有任何问题,因为A是指向double的指针,“inner-C”实际上将A [][ ]转换为指向double的指针(注意:针对指向指针的指针不成立!* ),所以没有区别。
* A = malloc ( rows ); for_each_Ai ( Ai = malloc (columns) );

关于您的平台无关部分,那段代码应该没问题。但是,如果他们还在做其他诡异的指针操作,请注意大小端。


我能得到一些C语言专家的验证吗? - im so confused
1
-1 抱歉,但那不是我想要的答案。我希望一些 C 专家能够通过“事实”(例如从标准中引用)确认在某些情况下转换可能是不安全的。此外,这个问题与二维数组的行主序或列主序排序关系很小。 - Massimiliano
@Massimiliano 好的,我也更喜欢这样的回答。 - im so confused

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接