55

次のコードは、ランタイム エラーで C++ をクラッシュさせます。

#include <string>

using namespace std;

int main() {
    string s = "aa";
    for (int i = 0; i < s.length() - 3; i++) {

    }
}

このコードはクラッシュしませんが:

#include <string>

using namespace std;

int main() {
    string s = "aa";
    int len = s.length() - 3;
    for (int i = 0; i < len; i++) {

    }
}

私はそれを説明する方法がわかりません。この動作の理由は何ですか?

4

8 に答える 8

85

s.length()符号なし整数型です。3を引くとマイナスになります。の場合、非常に大きいunsignedことを意味します。

回避策 (文字列が INT_MAX までの長さである限り有効) は、次のようにすることです。

#include <string>

using namespace std;

int main() {

    string s = "aa";

    for (int i = 0; i < static_cast<int> (s.length() ) - 3; i++) {

    }
}

これはループに入ることはありません。

非常に重要な詳細は、おそらく「符号付きと符号なしの値を比較しています」という警告を受け取ったことです。問題は、これらの警告を無視すると、暗黙の 「整数変換」(*)という非常に危険なフィールドに入るということです。これには動作が定義されていますが、従うのは困難です。最善の方法は、これらのコンパイラの警告を無視しないことです。


(*) 「整数昇格」についても知りたいかもしれません。

于 2013-07-01T07:05:39.510 に答える
12

実際、非常に大きな数を含む符号なし 整数と比較iすると、最初のバージョンでは非常に長い時間ループします。文字列のサイズは (事実上)符号なし整数と同じです。その値からを差し引くと、アンダーフローして大きな値になります。size_t3

コードの 2 番目のバージョンでは、この符号なしの値を符号付きの変数に割り当てて、正しい値を取得します。

実際にクラッシュを引き起こすのは条件や値ではありません。文字列を境界外にインデックス付けする可能性が最も高く、未定義の動作のケースです。

于 2013-07-01T07:04:33.340 に答える
5

forループ内の重要なコードを省略したと仮定します

forここにいるほとんどの人は、クラッシュを再現できないようです (私も含めて)。ここでの他の回答は、ループの本体で重要なコードをいくつか省略し、不足しているコードが原因であるという仮定に基づいているようです。クラッシュ。

を使用iしてループの本体でメモリ (おそらく文字列内の文字) にアクセスしていてfor、最小限の例を提供するためにそのコードを質問から除外した場合、クラッシュは次の事実によって簡単に説明できs.length() - 3ます。SIZE_MAX符号なし整数型の剰余算術による 値。SIZE_MAXは非常に大きな数であるためi、segfault をトリガーするアドレスへのアクセスに使用されるまで、大きくなり続けます。

forただし、ループの本体が空の場合でも、コードは理論的にそのままクラッシュする可能性があります。クラッシュする実装については知りませんが、コンパイラと CPU が特殊なものである可能性があります。

次の説明は、質問でコードを省略したことを前提としていません。質問に投稿したコードがそのままクラッシュするという信念が必要です。クラッシュする他のコードの省略された代役ではないこと。

最初のプログラムがクラッシュする理由

最初のプログラムがクラッシュするのは、それがコード内の未定義の動作に対する反応だからです。(コードを実行しようとすると、未定義の動作に対する実装の反応であるため、クラッシュせずに終了します。)

未定義の動作は、オーバーフローによるものintです。C++11 標準は次のように述べています ([expr] 節 5 段落 4 内):

式の評価中に、結果が数学的に定義されていないか、その型の表現可能な値の範囲内にない場合、動作は未定義です。

サンプル プログラムでは、値 2s.length()の a が返されます。それから 3 を引くと、符号なし整数型size_tを除いて、負の 1 になります。size_tC++11 標準は次のように述べています ([basic.fundamental] 節 3.9.1 パラグラフ 4):

宣言された符号なし整数は、 2 nunsignedを法とする算術法則に従うものとします。ここで、nは、整数の特定のサイズの値表現のビット数です。46

46) これは、結果の符号なし整数型で表現できない結果が、結果の符号なし整数型で表現できる最大値よりも 1 大きい数値を法として減らされるため、符号なし算術演算がオーバーフローしないことを意味します。

これは、 の結果がs.length() - 3with size_tvalueであることを意味しますSIZE_MAX。これは非常に大きな数値であり、INT_MAX( で表現できる最大値int) よりも大きくなります。

s.length() - 3は非常に大きいため、 に到達するまでループ内で実行がスピンiしますINT_MAX。次の反復で をインクリメントしようとするとi、結果はINT_MAX+ 1 になりますが、それは の表現可能な値の範囲内ではありませんint。したがって、動作は未定義です。あなたの場合、動作はクラッシュすることです。

私のシステムでは、iが過去にインクリメントされたときの実装の動作INT_MAXは、ラップ ( に設定)iINT_MINて続行することです。-1にi達すると、通常の算術変換 (C++ [expr] 節 5 段落 9) によりi等号が発生するSIZE_MAXため、ループは終了します。

どちらの反応も適切です。これが未定義の動作の問題です。意図したとおりに動作したり、クラッシュしたり、ハード ドライブをフォーマットしたり、Firefly をキャンセルしたりする可能性があります。あなたは、決して知らない。

2 番目のプログラムがクラッシュを回避する方法

最初のプログラムと同様に、はvalue をs.length() - 3持つ型です。ただし、今回は値が に割り当てられています。C++11 標準は次のように述べています ([conv.integral] 節 4.7 パラグラフ 3):size_tSIZE_MAXint

宛先の型が符号付きの場合、宛先の型 (およびビット フィールド幅) で表現できる場合、値は変更されません。それ以外の場合、値は実装定義です。

SIZE_MAXが で表現するには大きすぎるintためlen、実装定義の値を取得します (おそらく -1 ですが、そうでない場合もあります)。に割り当てられた値に関係なく、条件i < lenは最終的に真になるlenため、プログラムは未定義の動作に遭遇することなく終了します。

于 2013-07-02T23:03:16.233 に答える
3

s.length() の型のsize_t値は 2 であるため、s.length() - 3 も符号なし型size_tであり、その値SIZE_MAXは実装定義 (サイズが 64 ビットの場合は 18446744073709551615) です。これは少なくとも 32 ビット タイプ (64 ビット プラットフォームでは 64 ビットになる可能性があります) であり、この高い数値は無限ループを意味します。この問題を防ぐには、次のようにキャストs.length()するだけintです。

for (int i = 0; i < (int)s.length() - 3; i++)
{
          //..some code causing crash
}

2 番目のケースlenでは、-1 でありsigned integer、ループに入らないためです。

クラッシュに関して言えば、この「無限」ループはクラッシュの直接の原因ではありません。ループ内でコードを共有すると、さらに説明が得られます。

于 2013-07-01T07:03:55.477 に答える
1

あなたが抱えている問題は、次のステートメントから生じます。

i < s.length() - 3

s.length() の結果はunsigned size_t 型です。2 のバイナリ表現を想像すると、次のようになります。

0...010

そして、これから 3 を代入すると、実質的に 1 を 3 回離すことになります。つまり、次のようになります。

0...001

0...000

しかし、問題が発生し、左から別の数字を取得しようとするため、アンダーフローした 3 番目の数字を削除します。

1...111

これは、符号なしまたは符号付きのタイプに関係なく発生しますが、違いは、符号付きタイプは最上位ビット (または MSB) を使用して数値が負かどうかを表すことです。アンデフローが発生すると、それは単に符号付き型の否定を表します。

一方、 size_t はunsignedです。アンダーフローすると、size_t が表現できる最大の数値を表すようになります。したがって、ループは事実上無限です (これは size_t の最大値に影響するため、コンピューターによって異なります)。

この問題を解決するために、いくつかの異なる方法でコードを操作できます。

int main() {
    string s = "aa";
    for (size_t i = 3; i < s.length(); i++) {

    }
}

また

int main() {
    string s = "aa";
    for (size_t i = 0; i + 3 < s.length(); i++) {

    }
}

あるいは:

int main() {
    string s = "aa";
    for(size_t i = s.length(); i > 3; --i) {

    }
}

注意すべき重要な点は、置換が省略され、代わりに、同じ論理評価で加算が別の場所で使用されていることです。最初と最後の両方がループi内で使用可能な値を変更しますが、2 番目は同じ値を維持します。for

コードの例としてこれを提供したくなりました:

int main() {
    string s = "aa";
    for(size_t i = s.length(); --i > 2;) {

    }
}

少し考えた後、これは悪い考えだと気づきました。読者の課題は、その理由を解明することです。

于 2013-07-01T13:22:07.390 に答える