配列インデックスが範囲外の場合にC
が差別化する理由
_#include <stdio.h>
int main()
{
int a[10];
a[3]=4;
a[11]=3;//does not give segmentation fault
a[25]=4;//does not give segmentation fault
a[20000]=3; //gives segmentation fault
return 0;
}
_
_a[11]
_または_a[25]
_の場合にプロセスまたはスレッドに割り当てられたメモリにアクセスしようとしていること、および_a[20000]
_の場合にスタック境界から出ていることを理解しています。
コンパイラまたはリンカがエラーを出さないのはなぜですか、配列サイズを認識しないのですか?そうでない場合、sizeof(a)
はどのように正しく機能しますか?
問題は、C/C++が実際に配列に関する境界チェックを行わないことです。有効なメモリにアクセスしていることを確認するのはOSに依存します。
この特定のケースでは、スタックベースの配列を宣言しています。特定の実装に応じて、配列の境界外にアクセスすると、すでに割り当てられているスタックスペースの別の部分にアクセスするだけです(ほとんどのOSとスレッドはスタック用にメモリの特定の部分を予約します)。あなたがたまたま事前に割り当てられたスタックスペースで遊んでいる限り、すべてがクラッシュすることはありません(私は仕事を言わなかったことに注意してください)。
最後の行で起こっていることは、スタックに割り当てられたメモリの一部を超えてアクセスしたということです。その結果、プロセスに割り当てられていないか、読み取り専用で割り当てられているメモリの一部にインデックスを作成しています。 OSはこれを認識し、プロセスにsegフォールトを送信します。
これは、境界チェックに関してC/C++が非常に危険な理由の1つです。
セグメンテーション違反は、インデックスが範囲外であることを通知するCプログラムの意図したアクションではありません。むしろ、未定義の動作の意図しない結果です。
CおよびC++では、次のような配列を宣言する場合
type name[size];
0
からsize-1
までのインデックスを持つ要素にのみアクセスできます。その範囲外のものは、未定義の動作を引き起こします。インデックスが範囲に近かった場合、ほとんどの場合、独自のプログラムのメモリを読み取ります。インデックスの大部分が範囲外だった場合、おそらくプログラムはオペレーティングシステムによって強制終了されます。しかし、あなたは知ることができません、何かが起こる可能性があります。
なぜCはそれを許可するのですか?まあ、CとC++の基本的な要点は、パフォーマンスが犠牲になると機能を提供しないことです。 CおよびC++は、パフォーマンスが非常に重要なシステムの時代に使用されてきました。 Cは、メモリ内で隣接するオブジェクトへの高速アクセスを得るために、配列の境界外へのアクセスが役立つカーネルおよびプログラムの実装言語として使用されています。コンパイラにこれを禁止させるのは無意味です。
なぜそれについて警告しないのですか?さて、あなたは警告レベルを高くし、コンパイラの慈悲を期待することができます。これは、実装の品質quality(QoI)と呼ばれます。コンパイラーがオープンな動作(未定義の動作など)を使用して適切な処理を行う場合、その点で優れた実装品質を備えています。
[js@Host2 cpp]$ gcc -Wall -O2 main.c
main.c: In function 'main':
main.c:3: warning: array subscript is above array bounds
[js@Host2 cpp]$
代わりに、範囲外でアクセスされた配列を見てハードディスクをフォーマットする場合-これは合法です-実装の品質はかなり悪いでしょう。私は ANSI C Rationale 文書でそのようなものを読むのを楽しみました。
通常、プロセスが所有していないメモリにアクセスしようとすると、セグメンテーションフォールトが発生します。
a[11]
(およびa[10]
)の場合に見ているのは、プロセスdoesが所有しているがa[]
配列に属していないメモリです。 a[25000]
はa[]
から遠く離れており、おそらくあなたの記憶の外にあります。
a[11]
を変更すると、別の変数(または関数が戻ったときに別のセグメンテーションフォールトを引き起こす可能性のあるスタックフレーム)に静かに影響するため、はるかに潜んでいます。
Cはこれを行っていません。 OSの仮想記憶サブシステムです。
わずかに範囲外の場合、プログラムに割り当てられているis(この場合はスタックコールスタック上)という記憶に対処しています。範囲外の場合は、プログラムに割り当てられていないメモリをアドレス指定しており、OSはセグメンテーションフォールトをスローしています。
一部のシステムでは、OSが強制する「書き込み可能な」メモリの概念もあり、所有しているが書き込み不可とマークされた記憶に書き込みをしようとしている場合があります。
Litbが述べたように、一部のコンパイラは、コンパイル時に境界外の配列アクセスを検出できます。ただし、コンパイル時の境界チェックではすべてが捕捉されるわけではありません。
int a[10];
int i = some_complicated_function();
printf("%d\n", a[i]);
これを検出するには、ランタイムチェックを使用する必要がありますが、パフォーマンスへの影響のため、Cでは回避されます。コンパイル時のaの配列サイズ、つまり sizeof (a)を知っていても、ランタイムチェックを挿入しない限り、これを防ぐことはできません。
私は質問とコメントを理解しているので、境界外のメモリにアクセスするとなぜ悪いことcanが起こるのか理解できますが、なぜあなたの特定のコンパイラが疑問に思っています警告しなかった。
コンパイラは警告を発することが許可されており、多くのコンパイラは最高の警告レベルで警告を発します。しかし、この規格は、あらゆる種類のデバイスのコンパイラー、およびあらゆる種類の機能を備えたコンパイラーを実行できるように書かれているため、人々が有用な作業を行えることを保証しながら、最低限の要件を満たしています。
標準では、特定のコーディングスタイルで診断を生成する必要がある場合があります。標準が診断を必要としない他のいくつかの時間があります。診断が必要な場合でも、標準が正確な言葉遣いがどうあるべきかを言っている場所を知りません。
しかし、あなたはここで完全に寒いわけではありません。コンパイラが警告を表示しない場合、Lintはそうするかもしれません。さらに、ヒープ上の配列の(実行時に)このような問題を検出するツールがいくつかあります。最も有名なものの1つはElectric Fence(または [〜#〜] duma [〜#〜] です。 =)。しかし、Electric Fenceでさえ、すべてのオーバーランエラーをキャッチすることを保証していません。
他の人が言っていることを追加するだけで、これらの場合に単純にクラッシュするプログラムに頼ることはできません。「配列の境界」を超えてメモリ位置にアクセスしようとした場合に何が起こるかについての保証はありません。次のようなことをしたのと同じです。
int *p;
p = 135;
*p = 14;
それはただランダムです。これはうまくいくかもしれません。そうではないかもしれません。しないでください。この種の問題を防ぐためのコード。
Cの哲学は常にプログラマを信頼することです。また、境界をチェックしないと、Cプログラムをより高速に実行できます。
これはCの問題ではなく、オペレーティングシステムの問題です。あなたはプログラムに特定のメモリ空間が与えられており、その中で行うことは何でも問題ありません。セグメンテーション違反は、プロセス空間外のメモリにアクセスした場合にのみ発生します。
すべてのオペレーティングシステムがプロセスごとに個別のアドレススペースを持っているわけではありません。その場合、別のプロセスまたはオペレーティングシステムの状態を警告なしに破損する可能性があります。
JaredParが言ったように、C/C++は常に範囲チェックを実行するとは限りません。プログラムが割り当てられた配列の外側のメモリ位置にアクセスする場合、プログラムはクラッシュするか、スタック上の他の変数にアクセスしているためにクラッシュしない場合があります。
Cのsizeof演算子に関する質問に答えるには、sizeof(array)/ size(array [0])を確実に使用して配列サイズを決定できますが、これを使用してもコンパイラが範囲チェックを実行するわけではありません。
私の調査によると、C/C++開発者は、使用しないものにお金を払うべきではないと考えており、プログラマーが何をしているのかを信頼していると信じています。 (これに対する受け入れられた答えを参照してください: 範囲外の配列にアクセスしてもエラーは発生しません、なぜ? )
Cの代わりにC++を使用できる場合、ベクトルを使用できますか?パフォーマンスが必要な場合(ただし、範囲のチェックは不要)、vector []を使用できます。または、できればvector.at()(パフォーマンスを犠牲にして範囲をチェックする)を使用してください。 vectorは、容量がいっぱいの場合に容量を自動的に増加させないことに注意してください。安全のために、必要に応じて容量を自動的に増加させるPush_back()を使用します。