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

几天前,我偶然发现了一个代码,其中广泛使用从指针到类型到指针到类型数组的转换,以提供内存中线性向量的二维视图.为清楚起见,下面报告了这种技术的一个简单例子:
#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 *进行的转换确实具有保证语义,但这种可能性极小.从指向数组类型的指针到指向基类型的指针时,最好间接指针:

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

优化器可以假设访问[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个元素.

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

相关文章

本程序的编译和运行环境如下(如果有运行方面的问题欢迎在评...
水了一学期的院选修,万万没想到期末考试还有比较硬核的编程...
补充一下,先前文章末尾给出的下载链接的完整代码含有部分C&...
思路如标题所说采用模N取余法,难点是这个除法过程如何实现。...
本篇博客有更新!!!更新后效果图如下: 文章末尾的完整代码...
刚开始学习模块化程序设计时,估计大家都被形参和实参搞迷糊...