Cでは、配列の最後の要素の1つを指すポインターを作成し、それを逆参照しない限り、ポインター演算で使用することは完全に適切です。
_int a[5], *p = a+5, diff = p-a; // Well-defined
_
ただし、これらはUBです。
_p = a+6;
int b = *(a+5), diff = p-a; // Dereferencing and pointer arithmetic
_
質問があります:これは動的に割り当てられたメモリに適用されますか?ポインター演算で参照を行わずに、最後の1つ前を指すポインターのみを使用し、malloc()
が成功すると仮定します。
_int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");
// Question:
int *p = a+5;
int diff = p-a; // Use in pointer arithmetic?
_
One-past-mallocを指すポインターを使用することは明確に定義されていますか?
p
が割り当てられたメモリの1つを指しており、逆参照されていない場合は、明確に定義されています。
n157 -§6.5.6(p8):
[...]結果が配列オブジェクトの最後の要素の1つを指す場合、評価される単項
*
演算子のオペランドとして使用されません。
2つのポインターの減算は、同じ配列オブジェクトの要素または配列オブジェクトの最後の要素の1つを指す場合にのみ有効です。そうでない場合、未定義の動作になります。
(p9) :
2つのポインターが減算されると、両方が同じ配列オブジェクトの要素、または配列オブジェクトの最後の要素の1つを指します[...]
上記の引用符は、動的および静的に割り当てられたメモリの両方に適しています。
int a[5];
ptrdiff_t diff = &a[5] - &a[0]; // Well-defined
int *d = malloc(5 * sizeof(*d));
assert(d != NULL, "Memory allocation failed");
diff = &d[5] - &d[0]; // Well-defined
コメント の Jonathan Leffler で示されているように、これが動的に割り当てられたメモリに有効であるもう1つの理由は次のとおりです。
§7.22.3 (p1) :
aligned_alloc
、calloc
、malloc
、およびrealloc
関数の連続した呼び出しによって割り当てられるストレージの順序と連続性は指定されていません。割り当てが成功した場合に返されるポインターは、基本的な位置合わせ要件を持つ任意のタイプのオブジェクトへのポインターに割り当てられ、そのようなオブジェクトまたはの配列へのアクセスに使用できるように適切に位置合わせされます割り当てられたスペース内のそのようなオブジェクト(スペースが明示的に割り当て解除されるまで)。
上記のスニペットでmalloc
によって返されるポインターはd
に割り当てられ、割り当てられるメモリは5つのint
オブジェクトの配列です。
C11のドラフトn4296は、配列の1つを指すことが完全に定義されていることを明示しています:6.5.6言語/式/加算演算子:
§8整数型の式がポインターに加算またはポインターから減算される場合、結果はポインターオペランドの型になります。 ...さらに、式Pが配列オブジェクトの最後の要素を指す場合、式(P)+1は配列オブジェクトの最後の要素を1つ指し、式Qが配列オブジェクトの最後の要素を1つ指す場合配列オブジェクト、式(Q)-1は配列オブジェクトの最後の要素を指します...結果が配列オブジェクトの最後の要素の1つ前を指す場合、単項*演算子のオペランドとして使用されません。評価されます。
メモリーのタイプはsub節で厳密に指定されることはないため、割り当てられたメモリーを含むあらゆるタイプのメモリーに適用されます。
それは明らかに次のことを意味します:
int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");
両方
int *p = a+5;
int diff = p-a;
完全に定義されており、通常のポインター算術規則が適用されるため、diff
は値5
。
はい、同じルールが動的および自動ストレージ期間を持つ変数に適用されます。単一要素のmalloc
要求にも適用されます(この点で、スカラーは1要素配列と同等です)。
ポインター演算は、配列内でのみ有効です(配列の末尾を過ぎたものも含む)。
逆参照では、1つの考慮事項に注意することが重要です。初期化int a[5] = {0};
に関して、コンパイラーは式a[5]
でdereferenceint* p = &a[5]
を試みてはなりません。これをint* p = a + 5;
としてコンパイルする必要があります。繰り返しますが、動的ストレージにも同じことが当てはまります。
One-past-mallocを指すポインターを使用することは明確に定義されていますか?
はい、それでもnotが明確に定義されているコーナーケースが存在します。
_void foo(size_t n) {
int *a = malloc(n * sizeof *a);
assert(a != NULL || n == 0, "Memory allocation failed");
int *p = a+n;
intptr_t diff = p-a;
...
}
_
メモリ管理関数 ...要求されたスペースのサイズがゼロの場合、動作は実装定義です。nullポインターが返されるか、サイズがゼロ以外の値であるかのように動作します。ただし、返されたポインタはオブジェクトへのアクセスには使用されません。 C11dr§7.22.31
foo(0)
-> malloc(0)
は、NULL
または_non-NULL
_を返す場合があります。最初の実装では、NULL
の戻り値は「メモリ割り当てエラー」ではありません。これは、コードが_int *p = NULL + 0;
_を_int *p = a+n;
_と組み合わせて試みていることを意味します。これはポインター計算に関する保証に失敗するか、少なくともそのようなコードを疑問視します。
移植可能なコードは、0サイズの割り当てを回避することでメリットがあります。
_void bar(size_t n) {
intptr_t diff;
int *a;
int *p;
if (n > 0) {
a = malloc(n * sizeof *a);
assert(a != NULL, "Memory allocation failed");
p = a+n;
diff = p-a;
} else {
a = p = NULL;
diff = 0;
}
...
}
_