保证多维数组与具有相同元素总数的单维数组具有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
“可以将返回的指针分配给具有基本对齐要求的任何类型对象的指针,然后用于访问分配的空间中的此类对象或此类对象的数组”;没有关于进一步将返回的指针转换为另一个对象类型,因此结论是分配的内存的类型由返回的指针转换为的第一个指针类型固定;如果你投到然后你不能进一步投到,如果你投到你只能用来访问第一个元素。void
double *
double (*)[n]
double (*)[n]
double *
n
因此,我想说,如果你想绝对安全,你不应该在指针和指向数组类型的指针之间进行转换,即使是相同的基类型。memcpy
除了通过char
指针进行的其他访问之外,布局相同的事实是无关紧要的。