C セグメンテーションの不具合
このチュートリアルでは、C のセグメンテーション違反について説明し、このエラーの理由を説明するコード例をいくつか示します。 まず、プログラム セグメントと動的メモリについて説明します。
後で、セグメンテーション違反のさまざまな理由と考えられる解決策について説明します。
C のプログラム セグメント
コンピュータのメモリは、一次メモリと二次メモリに分けられます。 プログラムを実行するには、すべてのプログラムを一次メモリ (RAM) にロードする必要があります。
プログラムはさらにさまざまなセグメントに分割されます。
プログラムには 5つの主要なセグメントがあります。 これらのセグメントは次のとおりです。
-
テキスト セグメント - テキスト セグメントには、プログラムのコードが含まれます。
-
初期化されたデータ セグメント - 初期化されたデータ セグメントには、プログラムの初期化されたグローバル変数が含まれます。
-
初期化されていないデータ セグメント - 初期化されていないデータ セグメントには、プログラムの初期化されていないグローバル変数が含まれます。 このセクションは
bss
とも呼ばれます (スペースを節約するため)。int x[1000]
のような初期化されていない大きな配列を宣言することがありますが、これには4000
バイトが必要です。 ただし、このスペースは、配列が初期化されるまで必要ありません。したがって、このスペースは予約されていません。 ポインタのみが保存されます。 メモリは、配列が初期化されるときに割り当てられます。
-
ヒープ - ヒープ セクションには、実行時にメモリが要求されるデータが含まれます。これは、プログラマーがコーディング時に正確なサイズがわからないためです。
-
スタック - スタック セクションには、すべてのローカル変数が含まれます。 関数が呼び出されるたびに、そのローカル変数がスタックにプッシュされ (スタックが大きくなります)、関数が戻ると、ローカル変数がポップされます (スタックが縮小します)。
プログラム セグメントを視覚化して理解を深めるには、次のコードを参照してください。
int x = 5;
int y[100];
int main() {
int number = 5;
int *x = new int[5];
return 0;
}
このプログラムでは、x
はグローバルに初期化されたデータであり、y
はグローバルに初期化されていないデータです。 次に、number
はローカル変数です。 スタック領域に移動します。
x はポインタで、これもローカル変数で、スタック領域に移動します。 new int[5]
は領域をヒープ領域に割り当てます。
Unix ファミリーのオペレーティング システムでは、このプログラムのセグメントを簡単に確認できます。
C の動的メモリ
多くのプログラムでは、プログラマーは正確なメモリ要件を知りません。 このような場合、プログラマはユーザーまたはファイルから入力を取得してデータ サイズを取得し、入力に従って実行時にメモリを宣言します。
例を参照してください。
int main() {
int size;
cout << "Enter Size: ";
cin >> size
int *x = (int*) malloc(size] * sizeof(int) );
... return 0;
}
このプログラムでは、ユーザーがサイズを入力すると、プログラムは実行時にサイズに従ってメモリを割り当てます。
マルチプログラミング環境では、オペレーティング システムがメモリ保護を提供する必要があります。 これは、プログラムが意図せずにデータを共有することを制限することです。
したがって、すべてのオペレーティング システムは、プログラムが不正なメモリにアクセスするのを阻止するメカニズムを備えています。
プログラム用に予約されたメモリにのみアクセスできます。 プログラムのアドレス空間外のアドレスにアクセスしようとした場合、またはプログラムに割り当てられたメモリが動的割り当て要求を満たすのに不十分な場合、セグメンテーション違反が発生する可能性があります。
これについて詳しく説明しましょう。
C のセグメンテーション違反
プログラムの範囲外のメモリ ロケーションにアクセスしようとした場合、またはメモリにアクセスする権限がない場合、セグメンテーション フォールトが発生します。 以下でいくつかのケースについて説明しましょう。
初期化されていないポインタを参照してみてください
このエラーは混乱を招く可能性があります。コンパイラによっては、警告を表示してこのエラーを回避するのに役立つ場合と、そうでない場合があるためです。 以下は、紛らわしい興味深い例です。
int main() {
int* pointer;
printf("%d\n", *pointer);
return 0;
}
上記のコードでは、割り当てられていないポインターを逆参照しようとしています。つまり、アクセス許可のないメモリにアクセスしようとしています。
このプログラムを (cygwin
) GCC バージョン 9.3.0 でコンパイルすると、セグメンテーション エラー (コア ダンプ) が発生します。
g++ 9.3.0 でコンパイルすると、ゼロが出力されます。
ここで、このプログラムを少し変更して関数を追加すると、次のようになります。
void access() {
int* pointer;
printf("%d\n", *pointer);
}
int main() {
access();
return 0;
}
未割り当てのメモリにまだアクセスしようとしているため、混乱を招く出力として両方の印刷ガベージ値がコンパイルされます。
これを任意のオンライン コンパイラで試すと、同様の動作になります。 セグメンテーション違反は異常です。 小さな変更を加えることで、このエラーを追加または削除できる場合があります。
この種のエラーを回避するには、ポインターを初期化することを忘れないでください。逆参照する前に、ポインターが null でないかどうかを確認してください。
大きなメモリを割り当ててみる
このエラーは、2つの方法で発生する可能性があります。 1つはスタックで大きなサイズの配列を宣言するとき、もう 1つはヒープで大きなメモリを宣言するときです。
両方を1つずつ見ていきます。
#include <stdio.h>
int main() {
int largeArray[10000000]; // allocating memory in stack
printf("Ok\n");
return 0;
}
ゼロの数を減らすと、出力は Ok
になります。 ただし、ゼロを増やし続けると、ある時点でプログラムがクラッシュし、次のようになります。
Segmentation fault
その理由は、スタック領域が有限であるためです。 これは、この大きな配列に必要なメモリが利用できないことを意味します。
最終的に、プログラムはセグメントから出ようとしています。
より多くのメモリが必要な場合 (スタックで利用可能なメモリよりも大きい場合) は、ヒープを使用できます。 ただし、ヒープにも制限があります。 したがって、メモリ サイズを増やし続けると、エラーが発生します。
以下の例を参照してください。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *veryLargeArr;
long int size = 100000000000;
veryLargeArr = (int *)malloc(sizeof(int) * size);
if (veryLargeArr == NULL)
printf("Space is not enough.\n");
else
printf("memory allocation is successful\n");
return 0;
}
出力:
memory allocation is successful
ただし、サイズを大きくしていくと限界を超えてしまいます。 たとえば、次のサイズで問題が発生しました。
long int size = 1000000000000000; // 100000000000
上記のステートメントでは、さらにゼロを数えることができます。 このような場合、プログラムがクラッシュする可能性があります。 ただし、セグメンテーション違反を回避するために、ポインターが NULL
かどうかをチェックしました。
NULL
は、要求されたスペースが利用できないため、メモリが割り当てられなかったことを意味します。
出力:
Space is not enough.
チェックせずに動的に割り当てられたメモリを使用してこのコードを試すと、セグメンテーション違反が発生します。
無限ループまたは再帰呼び出し
プログラムに誤って無限ループを残すと、特にループ内に動的メモリを割り当てた場合に、セグメンテーション違反が発生します。
動的メモリ割り当てによる無限ループの例を示します。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p;
while (true) p = (int *)malloc(100000);
return 0;
}
ここでは、while (true)
を使用した無限ループを見ることができます。 ループ内のメモリ割り当てステートメントは、メモリを解放するために free
メソッドを呼び出さずにメモリ割り当てが繰り返し繰り返されるため、最終的にエラーを生成します。
同様に、基本ケースを追加せずに再帰関数を作成すると、スタックがオーバーフローする可能性があります。 以下の例を参照してください。
void check() { check(); }
int main() { check(); }
上記のコードでは、check
関数は自分自身を呼び出し続け、スタック上にそのコピーを作成します。これにより、プログラムで使用可能なメモリが消費されると、セグメンテーション フォールトが発生します。
まとめ
セグメンテーション違反は、プログラムがアクセスできないメモリや使用できないメモリにアクセスしようとすると発生します。 逆参照する前に、ポインタがメモリを指しているかどうかを確認してください。
大きなスペースが必要な場合は動的メモリを使用し、ポインタが NULL
かどうかを確認します。 scanf
では変数の前に &
を使用し、printf
では %
の後に正しい指定子を使用してください。
サイズ外の配列から値を割り当てたりアクセスしたりしないでください。 変数とポインターは、宣言時に必ず初期化してください。