多次元配列のメモリ レイアウトは、要素の総数が同じ 1 次元配列と同じT arr[M][N]
であることが保証されています。配列は連続しているため (6.2.5p20) 、配列内の要素数を返すことが保証されているため (6.5.3.4p7)、レイアウトは同じです。T arr[M * N]
sizeof array / sizeof array[0]
ただし、型へのポインターを型の配列へのポインターにキャストすること、またはその逆が安全であるということにはなりません。まず、アラインメントが問題です。基本アラインメントを持つ型の配列は基本アラインメントも (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;
}
f
が で呼び出された場合、これはもちろん予期しない結果をもたらしますn = 50
。
最後に、これが割り当てられたメモリに当てはまるかどうかを尋ねる価値があります。7.22.3p1 は、malloc
"によって返されるポインタは、基本的なアラインメント要件を持つ任意のタイプのオブジェクトへのポインタに割り当てられ、割り当てられた空間内のそのようなオブジェクトまたはそのようなオブジェクトの配列にアクセスするために使用される可能性があることを指定します"; 返されたポインターを別のオブジェクト型にさらにキャストすることについては何もないため、結論として、割り当てられたメモリの型は、返されたポインターがキャストされる最初のポインター型によって固定されます。にキャストした場合、さらに にキャストすることはできません。また、 にキャストした場合は、最初の要素にアクセスするためだけに使用できます。void
double *
double (*)[n]
double (*)[n]
double *
n
そのため、絶対に安全にしたい場合は、同じ基本型であっても、ポインターと配列型へのポインターの間でキャストしないでください。レイアウトが同じであるという事実は、ポインターmemcpy
を介したその他のアクセスを除いては関係ありません。char