Mas*_*ano 6 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;
}
Run Code Online (Sandbox Code Playgroud)
鉴于指向类型的指针与指向类型数组的指针不兼容,我想问一下这个转换是否存在风险,或者我是否可以假设此转换将在任何平台上按预期工作.
保证多维数组与单维数组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
}
Run Code Online (Sandbox Code Playgroud)
接下来,不能保证数组和非数组类型之间的转换实际上会产生指向正确位置的指针,因为转换规则 (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
}
Run Code Online (Sandbox Code Playgroud)
数组下标的语义是什么?众所周知,该[]
操作只是加法和间接寻址的语法糖,因此语义是+
操作符的语义;正如 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
Run Code Online (Sandbox Code Playgroud)
这是我们开始看到常见实现的实际问题,而不仅仅是理论的地方。因为优化器有权假设未定义的行为不会发生,所以可以假设通过基对象指针访问多维数组不会对第一个内部数组中的元素之外的任何元素进行别名:
int a[10][10];
void f(int n) {
for (int i = 0; i < n; ++i)
(*a)[i] = 2 * a[2][3];
}
Run Code Online (Sandbox Code Playgroud)
优化器可以假定对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;
}
Run Code Online (Sandbox Code Playgroud)
f
如果使用 调用,这当然会产生意想不到的结果n = 50
。
最后值得一问的是这是否适用于分配的内存。malloc
7.22.3p1 指定“可以将返回的指针分配给具有基本对齐要求的任何类型对象的指针,然后用于在分配的空间中访问此类对象或此类对象的数组”;没有什么可以进一步将返回的指针转换为另一个对象类型,因此结论是分配的内存的类型由返回的指针转换为的第一个指针类型固定;如果您强制转换为,则无法进一步强制转换为,并且如果强制转换为,则只能用于访问第一个元素。void
double *
double (*)[n]
double (*)[n]
double *
n
因此,我想说,如果您想绝对安全,则不应在指针和指向数组类型的指针之间进行转换,即使具有相同的基类型。memcpy
除了通过指针进行的其他访问之外,布局相同这一事实是无关紧要的char
。