c - 指针转int - 数组指针赋值




是从一个指针类型转换为一个指向安全类型数组的指针吗? (3)

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

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

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


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

但是有一些警告:

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

  • 该标准只声明一个有效的结果指针的使用,这就是将其转换回原来的指针类型。 在这种情况下,标准保证后者(将结果指针转换回原始指针类型)将与原始指针进行比较。 使用所产生的指针作为其他任何东西,不在标准中。

  • 该标准在执行这种转换时需要进行明确的转换,这在您发布的代码中的print_matrix函数调用中是缺少的。

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


更新 删除线部分 是真实的,但无关紧要。

正如我在注释中发布的,问题是在二维数组中,子数组(行)是否包含内部填充。 因为标准定义了数组是连续的,所以每一行里面肯定没有填充。 另外,外层数组不应该引入填充。 实际上,通过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*


确保多维阵列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] ,只能使用double *来访问前n元素。

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





type-conversion