6

次のような配列があるとします。

int val[10];

そして、意図的に負の値から 9 より大きい値まですべてを使用してインデックスを作成しますが、結果の値を使用することはありません。これはパフォーマンス上の理由によるものです (おそらく、配列アクセスが行われた後に入力インデックスをチェックする方が効率的です)。

私の質問は次のとおりです。

  1. そうするのは安全ですか、それともある種のメモリ保護バリアに遭遇したり、特定のインデックスに対してメモリを破損したりするリスクがありますか?
  2. このように範囲外のデータにアクセスすると、まったく効率的ではないのでしょうか? (配列に範囲チェックが組み込まれていないと仮定します)。
  3. それは悪い習慣と見なされますか?(範囲外のインデックスを使用していることを認識していることを示すコメントが書かれていると仮定します)。
4

4 に答える 4

17

これは未定義の動作です。定義上、未定義は「何でも起こり得る」ことを意味します。あなたのコードはクラッシュする可能性があり、完全に機能する可能性があり、すべての人間に平和と調和をもたらす可能性があります。私は2番目または最後に賭けません。

于 2013-09-13T08:40:31.170 に答える
10

これは未定義の動作であり、実際にオプティマイザーに違反する可能性があります。

次の簡単なコード例を想像してください。

int select(int i) {
    int values[10] = { .... };

    int const result = values[i];

    if (i < 0 or i > 9) throw std::out_of_range("out!");

    return result;
}

次に、オプティマイザの観点から見てみましょう。

  • int values[10] = { ... };: 有効なインデックスは にあり[0, 9]ます。

  • values[i]:iはインデックスであるため、に含まi[0, 9]ます。

  • if (i < 0 or i > 9) throw std::out_of_range("out!");:iにあり[0, 9]、一度も使用されていません

したがって、オプティマイザによって書き換えられた関数は次のようになります。

int select(int i) {
    int values[10] = { ... };

    return values[i];
}

開発者が禁止されていることを何も行っていないという事実に基づいた仮定の順方向および逆方向の伝搬に関するさらに面白い話については、「すべての C プログラマーが未定義の動作について知っておくべきこと: パート 2 」を参照してください。

編集:

考えられる回避策: から-Mにアクセスすることがわかっている場合+N:

  • 適切なバッファで配列を宣言します。int values[M + 10 + N]
  • アクセスをオフセットします。values[M + i]
于 2013-09-13T09:19:12.777 に答える
5

詳細 が述べたように、これは未定義の動作をもたらします。もう少し精度が続きます。

5.2.1/1 によると

[...] 式 E1[E2] は (定義により) *((E1)+(E2)) と同じです。

したがって、val[i]と同等*((val)+i))です。valは配列であるため、加算が実行される前に配列からポインタへの変換 (4.2/1) が発生します。したがって、は に設定されている場所とval[i]同等です。*(ptr + i)ptrint*&val[0]

次に、5.7/2 は何ptr + iを指すかを説明します。それはまた言います(強調は私のものです):

[...]ポインター オペランド結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素の 1 つ後ろを指している場合、評価はオーバーフローを生成しません。それ以外の場合、動作は undefinedです。

の場合ptr + i、ptr はポインタ オペランドで、結果ptr + iです。上記の引用によると、どちらも配列の要素または最後の要素の 1 つ後ろを指す必要があります。つまり、OP の場合ptr + iは、すべての明確に定義された式ですi = 0, ..., 10。最後に、 *(ptr + i)は に対して明確に定義されていますが、 に対しては定義されて0 <= i < 10いませんi = 10

編集

val[10](または、同等に) が未定義の動作を生成するかどうかに困惑して*(ptr + 10)います (C ではなく C++ を検討しています)。状況によってはこれが正しい場合 (int x = val[10];未定義の動作など) もありますが、明確でない場合もあります。例えば、

int* p = &val[10];

これまで見てきたように、これはint* p = &*(ptr + 10);未定義の動作 (の最後の要素の 1 つ前のポインターを逆参照するため) である可能性があるものと同等であるか、明確に定義さvalれているものと同じです。int* p = ptr + 10;

この質問がいかに曖昧であるかを示す次の 2 つの参考文献を見つけました。

配列の 1 つ後ろの要素のアドレスを取得できますか?

添え字を介して 1 つ後ろの配列要素のアドレスを取得する: C++ 標準で合法かどうか?

于 2013-09-13T09:17:15.417 に答える
3

いくつかのパディング int を含む構造体に配置すると、安全であるはずです (ポインターは実際には「既知の」宛先を指しているため)。しかし、それは避けたほうがよいでしょう。

struct SafeOutOfBoundsAccess
{ 
    int paddingBefore[6];
    int val[10];
    int paddingAfter[6];
};

void foo()
{
    SafeOutOfBoundsAccess a;
    bool maybeTrue1 = a.val[-1] == a.paddingBefore[5];
    bool maybeTrue2 = a.val[10] == a.paddingAfter[0];
}
于 2013-09-13T08:49:56.997 に答える