是从指针到类型转换为指向安全类型数组的指针?

几天前,我偶然发现了一个代码,其中广泛使用了从指针到类型指针转换为指向类型 数组的指针,以便在内存中提供线性矢量的二维视图。 为了清楚起见,下面报道了这种技术的一个简单例子:

#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;
}

假设指向类型指针指向数组类型指针不兼容,我想问一下是否存在与此投射相关的风险,或者我是否可以假定此投射将在任何平台上按预期工作。


确保多维阵列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 *进行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和其他访问以外,布局相同的事实是无关紧要的。


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

正如我在评论中发布的,问题是在二维数组中,子阵列(行)是否包含内部填充。 因为标准定义数组是连续的,所以每行内部不应有填充。 而且,外部数组不应该引入填充。 事实上,通过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*


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

但有几点需要注意:

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

  • 该标准仅指出了所产生的指针的一个有效用途,并且将其转换回原始指针类型。 在这种情况下,标准保证后者(将结果指针转换回原始指针类型)将与原始指针进行比较。 使用结果指针作为其他任何事情,不在标准范围内。

  • 该标准需要在执行此类转换时进行明确的转换,这在您发布的代码中的print_matrix函数调用中不存在。

  • 因此,根据标准的字母,代码示例中的用法超出了其范围。 但实际上,这在大多数平台上都可以正常工作 - 假设编译器允许。

    链接地址: http://www.djcxy.com/p/64709.html

    上一篇: Is a conversion from a pointer to type to a pointer to array of type safe?

    下一篇: Chrome extension History API