19

別の質問の熱いコメント スレッドに続いて、C 配列に関する C99 標準で定義されているものと定義されていないものについて議論するようになりました。

基本的に、 のような 2D 配列を定義する場合int a[5][5]、標準の C99 は、それが int の連続したブロックに(int *)aなることを保証しますか? にキャストして、25 int の有効な 1D 配列があることを確認します。

私が標準を理解しているように、上記のプロパティは sizeof 定義とポインター演算で暗黙的ですが、他の人は同意しないようで、 (int*) へのキャストは未定義の動作を与えると言います (既存のすべての実装が実際に割り当てることに同意したとしても)連続した値)。

より具体的には、すべての次元の配列境界をチェックし、1D 配列にアクセスするときに何らかのエラーを返すように配列をインストルメント化する実装、または 1 行目より上の要素に正しくアクセスできない実装を考えるとします。そのような実装は標準に準拠できますか? この場合、C99 標準のどの部分が関連していますか。

4

3 に答える 3

18

int a[5][5] が実際に何であるかを調べることから始めるべきです。関連するタイプは次のとおりです。

  • 整数
  • int の配列 [5]
  • 配列の配列[5]

関連するintの配列[25]はありません。

sizeof セマンティクスが、配列全体が連続していることを暗示しているのは正しいことです。int の配列 [5] は 5*sizeof(int) でなければならず、再帰的に適用される a[5][5] は 5*5*sizeof(int) でなければなりません。追加のパディングの余地はありません。

さらに、sizeof を指定して memset、memmove、または memcpy に指定した場合、配列全体が機能している必要があります。(char *) を使用して配列全体を反復処理することも可能でなければなりません。したがって、有効な反復は次のとおりです。

int  a[5][5], i, *pi;
char *pc;

pc = (char *)(&a[0][0]);
for (i = 0; i < 25; i++)
{
    pi = (int *)pc;
    DoSomething(pi);
    pc += sizeof(int);
}

(int *) で同じことを行うと、定義されていない動作になります。前述のように、int の配列 [25] が含まれていないためです。クリストフの答えのようにユニオンを使用することも有効です。しかし、これをさらに複雑にする別の点があります。それは等値演算子です。

6.5.9.6 2 つのポインタが等しく比較されるのは、両方がヌル ポインタであり、両方が同じオブジェクト (オブジェクトへのポインタとその先頭のサブオブジェクトを含む) または関数へのポインタであり、両方が最後の要素の 1 つ後ろを指すポインタである場合のみです。同じ配列オブジェクト、または 1 つは 1 つの配列オブジェクトの末尾を過ぎたものへのポインターであり、もう 1 つは別の配列オブジェクトの先頭へのポインターであり、アドレス空間内の最初の配列オブジェクトの直後にたまたま続きます。91)

91) 2 つのオブジェクトがメモリ内で隣接している可能性があります。これは、それらがより大きな配列の隣接する要素または構造体の隣接するメンバーであり、それらの間にパディングがないため、または実装がそれらを配置することを選択したためです。以前の無効なポインター操作 (配列境界外へのアクセスなど) が未定義の動作を生成した場合、その後の比較でも未定義の動作が生成されます。

これは、次のことを意味します。

int a[5][5], *i1, *i2;

i1 = &a[0][0] + 5;
i2 = &a[1][0];

i1 は i2 と等しいと比較されます。ただし、(int *) を使用して配列を反復処理する場合、最初のサブ配列から派生しているため、まだ未定義の動作です。魔法のように 2 番目のサブ配列へのポインターに変換されるわけではありません。

これをしている時も

char *c = (char *)(&a[0][0]) + 5*sizeof(int);
int  *i3 = (int *)c;

役に立ちません。比較すると i1 および i2 と等しくなりますが、どの部分配列からも派生していません。これは、せいぜい単一の int または int の配列 [1] へのポインターです。

私はこれを標準のバグとは考えていません。これは逆です。これを許可すると、配列の型システムまたはポインター演算の規則、またはその両方に違反する特殊なケースが導入されます。定義の欠落と見なされる場合がありますが、バグではありません。

したがって、a[5][5] のメモリ レイアウトが a[25] のレイアウトと同一であり、(char *) を使用するまったく同じループを使用して両方を反復処理できる場合でも、実装は失敗する可能性があります。一方が他方として使用されている場合はアップします。なぜそれが必要なのか、またはそのような実装を知っているのかわかりません。また、標準には、これまで言及されていない単一の事実があり、それが明確に定義された動作になっている可能性があります。それまでは、未定義と見なし、安全側にとどまります。

于 2010-05-14T13:02:33.923 に答える
12

元のディスカッションにコメントを追加しました。

sizeofセマンティクスはそれが連続していることを意味int a[5][5]しますが、ポインタをインクリメントすることによって 25 の整数すべてにアクセスすることint *p = *aは未定義の動作です&a[2][1]&a[3][1]しないでください (C99 セクション 6.5.6 を参照)。

&a原則として、タイプを持つint (*)[5][5]キャストをにキャストすることで、これを回避できますint (*)[25]。これは、アライメント要件に違反していないため、6.3.2.3 §7 に従って合法です。問題は、この新しいポインターを介して整数にアクセスすることは、6.5 §7 のエイリアシング規則に違反するため、違法であることです。for 型のパニングを使用することで、これを回避できますunion(TC3 の脚注 82 を参照)。

int *p = ((union { int multi[5][5]; int flat[25]; } *)&a)->flat;

私が知る限り、これは標準に準拠した C99 です。

于 2010-05-14T09:28:46.433 に答える
2

配列が静的である場合、配列のように、int a[5][5]連続していることが保証されます。

于 2010-05-14T09:19:08.450 に答える