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

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

4

4 回答 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*

于 2012-10-12T16:24:32.063 回答
1

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

不过有一些注意事项:

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

  • 该标准仅规定了结果指针的一种有效用途,即将其转换回原始指针类型。在这种情况下,标准保证后者(转换回原始指针类型的结果指针)将与原始指针进行比较。标准未涵盖将结果指针用于其他任何内容。

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

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

于 2012-10-12T16:40:32.707 回答
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];
}

优化器可以假设 accessa[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可以将返回的指针分配给具有基本对齐要求的任何类型对象的指针,然后用于访问分配的空间中的此类对象或此类对象的数组”;没有关于进一步将返回的指针转换为另一个对象类型,因此结论是分配的内存的类型由返回的指针转换为的第一个指针类型固定;如果你投到然后你不能进一步投到,如果你投到你只能用来访问第一个元素。voiddouble *double (*)[n]double (*)[n]double *n

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

于 2012-10-12T22:17:48.243 回答
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 的指针(注意:指针指针不正确! *),所以没有区别。

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

^ 显然所有代码都是伪代码

关于您的平台无关部分,该代码应该没问题。但是,如果他们也在做其他偷偷摸摸的指针事情,请注意字节序

于 2012-10-12T14:52:39.203 回答