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

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

#include  #include  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, ie 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; } 

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

保证多维arraysT arr[M][N]具有与具有相同元素总数T arr[M * N]一维arrays相同的存储器布局 。 布局是相同的,因为数组是连续的(6.2.5p20),并且因为sizeof array / sizeof array[0]保证返回数组中元素的数量(6.5.3.4p7)。

但是,并不是说将指针类型转换为指向类型数组的指针是安全的,反之亦然。 首先, 调整是一个问题; 虽然具有基本对齐的类型的arrays也必须具有基本对齐(通过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元素。

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

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

正如我在评论中发布的那样,问题实际上是否在二维数组中,子数组(行)包含内部填充。 每行内部肯定没有填充,因为标准定义了数组是连续的。 此外,外部arrays不应引入填充。 实际上,通过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 

然后传递给print_array 。 这应该避免指针别名的潜在缺陷; 不允许使用不同类型的指针指向同一个数组,除非它们中至少有一个类型为void*char*unsigned char*

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

但有几点需要注意:

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

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

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

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

我在这里首先想到的是,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) ); 

^所有代码伪代码显然

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