是从指针类型转换为指向类型为safe的数组的指针吗?

时间:2012-10-12 14:45:18

标签: c arrays pointers type-conversion

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

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

鉴于指向的指针与指向数组的指针不兼容,我想询问是否存在与此转换相关的风险,或者我可以假设这个转换将在任何平台上按预期工作。

4 个答案:

答案 0 :(得分:2)

更新删除线部分为true,但不相关。

正如我在评论中发布的那样,问题实际上是在二维数组中,子数组(行)是否包含内部填充。 每行内部肯定没有填充,因为标准定义了数组是连续的。此外,外部阵列不应引入填充。事实上,通过C标准扫描,我发现在数组的上下文中没有提到填充,所以我将“连续”解释为意味着在多维数组内的子数组的末尾永远不会有任何填充。sizeof(array) / sizeof(array[0])保证返回数组中元素的数量,不能有这样的填充。

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

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*类型。

答案 1 :(得分:1)

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

但有几点需要注意:

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

  • 标准仅声明对结果指针的一次有效使用,并将其转换回原始指针类型。在这种情况下,标准保证后者(转换回原始指针类型的结果指针)将与原始指针进行比较。使用生成的指针进行其他任何操作都不在标准范围内。

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

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

答案 2 :(得分: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;
}

如果使用f调用n = 50,这当然会产生意外结果。

最后值得问一下这是否适用于已分配的内存。 7.22.3p1指定由malloc返回的指针可以分配给指向具有基本对齐要求的任何类型对象的指针,然后用于访问此类对象或此类对象的数组。空间分配“;没有什么可以将返回的指针进一步转换为另一个对象类型,因此结论是第一个指针类型所返回的{{1}所分配内存的类型是 fixed 指针被强制转换为;如果您转向void,则无法进一步转为double *,如果您转为double (*)[n],则只能使用double (*)[n]访问第一个double * } elements。

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

答案 3 :(得分:0)

我首先想到的是C在创建2D数组时实际上使用了该实现 - 即它将内存线性延伸:

[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的指针(注意:对于指针不是true)指针! *),所以没有区别。

* A = malloc ( rows ); for_each_Ai ( Ai = malloc (columns) );

^显然所有代码伪代码

关于您的平台无关性部分,该代码应该没问题。但是,如果他们也在做其他偷偷摸摸的指针,请注意endian-ness