【C言語】「NULL」の意味とNULLを用いた「安全なポインタの使い方」

NULLの解説ページアイキャッチ

このページにはプロモーションが含まれています

C言語でポインタは非常に便利ですが、ポインタの状態によってはポインタの先にアクセスすることが危険な時があります。

これを防ぐのに非常に重要な役割を果たすのが「NULL」です。このページではまず NULL について解説し、その後ポインタの4つの状態と、安全なポインタの使い方を解説したいと思います。

NULL とは

NULL とは一般的には何もない空の状態を指す言葉です。

特にC言語においてはポインタが NULL を指すことで、そのポインタがどこも指していない状態を明示的に表す目的で使用されます。

具体的には、下記のように NULL が代入された p1 はどこも指していない状態として扱われます。逆に p2 は変数 a のアドレスが格納されているので、変数 a を指している状態となります。

ポインタ変数の利用例
int *p1;
int *p2;
int a = 100;
p1 = NULL;
p2 = &a;

各変数の状態を図で表すと下のようになります。

ポインタが NULL を指している状態

では NULL って実際に何なのでしょうか?これは printfを使って表示してみると分かります。

NULLの表示
printf("NULL = %p\n", NULL);

printf での表示結果は下記のようになりました。

おそらくほとんどの方は同じ結果になると思います。環境によってはもしかしたら違う値になるかもしれません。

NULL = 0x0

つまり、NULL は実際には単に 0 番地を表すアドレスの定数です。

通常ユーザーが作成するようなプログラムにおいては 0 番地のメモリは使用されませんので(OSの起動時等に用いられる)、そのアドレス 0 を便宜的に「どこも指していない」を表すアドレスとして扱っているのです。

NULL の意味としては、定義値の 0 よりも、「どこも指していない」を表すものとして捉えた方が良いです。

ですので、「NULL = どこも指していない」で覚えておきましょう!

この NULLはポインタを扱う上で非常に重要な定数となります。

これについては次のポインタの状態で解説していきたいと思います。

ポインタの状態

続いてC言語のポインタが取りうる「4つの状態」について解説します。

スポンサーリンク

不定アドレスを指している状態

1つ目の状態は不定アドレスを指している状態です。

これは、変数宣言直後のポインタの状態になります。

変数は、変数宣言直後には不定値が格納されています。つまり、どんな値が格納されていない状態です。

これは、ポインタ変数も同様です。つまり、ポインタは変数宣言直後ではどこを指しているかわからない状態です。

これがポインタの状態の1つ目の「不定アドレスを指している状態」です。

変数宣言直後のポインタ
int *p;

この状態を図示すると下記のようになります。

確保したメモリを指している状態

2つ目の状態は確保したメモリを指している状態です。

変数に & 演算子を付けてアドレスを取得し、それをポインタ変数に格納すると、ポインタはそのプログラム内で確保したメモリを指している状態になります(プログラムで使用可能なメモリを指している状態)。

また malloc 関数の戻り値をポインタ変数に格納するのも同じで、これも確保したメモリを指している状態になります。

mallocで確保したメモリを指す
p = (int*)malloc(sizeof(int) * N);

この状態を図示すると下記のようになります。 

解放したメモリを指している状態

3つ目の状態は解放したメモリを指している状態です。

malloc 関数で確保したメモリを指しているポインタを free 関数で解放すると、そのポインタの指すメモリが解放されます。

これにより、free 関数に指定したポインタは、解放したメモリを指している状態になります。

ポインタが指すメモリを解放
free(p);

この状態を図示すると下記のようになります。 

スポンサーリンク

NULL を指している状態

4つ目の状態は NULL を指している状態です。

前述の通り、NULL というのは「このポインタはどこも指していないですよ」というのを明示的に表す定数です。

ポインタに NULL をセットすると、そのポインタは NULL を指している状態になります。

ポインタにNULLを指させる
p = NULL;

この状態を図示すると下記のようになります。 

具体的に下記のプログラムでポインタの状態を色で示してみました。

各色に対応するポインタの状態は下記のようになります。

  • 緑:不定アドレスを指している状態
  • 青:確保したメモリを指している状態
  • 黄:解放したメモリを指している状態
  • ピンク:NULL を指している状態

安全にアクセス可能な状態

この4つの状態のうち、ポインタが指している先に安全にアクセスできるのは「確保したメモリを指している状態」のみです。

他の3つの状態では、ポインタで指しているだけであれば問題ありませんが、安全にアクセスすることはできません。

そのポインタの指すアドレスの値を取得しようとしたり、値を変更しようとしたりするとセグメンテーションフォールトが発生したり NULL ポインタアクセスが発生したりしてプログラムが落ちる可能性があります。

プログラムで判断できる状態

しかしその一方で、C言語のプログラム内ではポインタの下記の2つの状態しか判断することができません。

  • ポインタが NULL を指している状態
  • それ以外

つまり、ポインタが NULL 以外の場合に、そのポインタが安全にアクセス可能な「確保したメモリを指している状態」であるか、それともアクセスが危険である「不定アドレスを指している状態」 or 「解放したメモリを指している状態」かは判断ができないのです。

例えば下記のようなプログラムだとポインタが NULL の時はポインタの指す先にアクセスしないように制御することができますが、そのポインタが「確保したメモリを指している状態」であるかどうかを判断することができません。

NULL ではないからといって、安全にアクセスできるかどうかは分からないのです。

NULLかどうかの判断
if(p != NULL){
    printf("%d\n", *p);
}

スポンサーリンク

安全にポインタを使用する方法

ここまで解説してきた内容を簡単にまとめます。

  • 安全にポインタの指す先にアクセスできるのは、「確保したメモリを指している状態」のみ
  • プログラムで判断できるのは、「NULL を指している状態」か「それ以外」かのみ

つまり、状態が4つある以上、C言語プログラムでポインタの指す先に安全にアクセスできるかを保証することは不可能なのです。

ではポインタは安全に使用することはできないのか?と言うとそうではありません。

下記の2つさえ確実に行えば、ポインタの状態を「確保したメモリを指している状態」と「NULLを指している状態」の2つに限定することができ、安全にポインタを使用することが可能です。

  • 「不定アドレスを指している状態」になった瞬間にポインタに NULL を指させる
  • 「解放したアドレスを指している状態」になった瞬間にポインタに NULL を指させる

つまり、アクセスもできない、NULL 判断もできない状態になった瞬間にポインタに NULL を代入するのです。

これにより状態を「確保したメモリを指している状態」と「NULL を指している状態」に限定することができ、ポインタを安全に扱うことができるようになるのです。

プログラム的に言うと、ポインタ変数宣言時に必ず NULL で初期化すること、

不定値を指した状態になったらNULLを指させる
int *p = NULL;

と、free 直後に必ずポインタに NULL を代入すること、

解放したメモリを指した状態になったらNULLを指させる
free(p);
p = NULL;

この2つにより状態を2つに減らすことができ、安全にポインタを使用することができるようになります。

ですので、ポインタを使用する際には、下記の3つを心がけるようにしましょう!(私のプログラムでもできていないことが多いですが…)

  • 変数宣言時に必ず NULL で初期化する
  • free 直後に必ずポインタに NULL を代入する
  • ポインタの指す先にアクセスする前に NULL かどうかを判断する(NULL チェック)

是非日頃のプログラミングでこの3つを心がけ、癖のように NULL 代入と NULL チェックをするようにしましょう。

ただし、上記を行えば必ず安全なアドレスにアクセスできるようになるというわけではないです。具体的には下記の場合は危険なアドレスにアクセスする可能性があります。

  • ポインタにアドレスを直値で設定する
  • ポインタに加減算を行なったアドレスにアクセスする

前者に関しては、OS やデバイスドライバなどのアドレスを直で指定して動作させるプログラムを開発したりしない限りはやらないと思います(ポインタでアクセスしていいのは、変数や malloc 等で確保したメモリのアドレスだけ!)。

後者に関しては、配列や malloc で確保した大きなメモリをアクセスする時には結構行います。正直これに関しては、ポインタが指す先のメモリのサイズ(変数のサイズや malloc で確保したメモリのサイズ)を超えたアドレスにアクセスしないように、ポインタに加減算を行うように心がけるしかありません。

ただ、逆にいうと、下記の3つをしっかり実行していれば、セグメンテーションフォールトなどが発生した場合の原因は「ポインタへの加減算」と断定できるので、デバッグはやはりやりやすくなると思います!

  • 変数宣言時に必ず NULL で初期化する
  • free 直後に必ずポインタに NULL を代入する
  • ポインタの指す先にアクセスする前に NULL かどうかを判断する(NULL チェック)

まとめ

このページでは NULL とポインタの4つの状態について解説しました。

ポインタを扱う上で、ポインタの状態を下記の2つに限定することが、ポインタを安全に使用するポイントになります。

  • 確保したメモリを指している状態
  • NULL を指している状態

ぜひ日頃からポインタ変数宣言時の NULL 初期化と free 関数直後の NULL 代入を行いうようにし、ポインタの状態を2つに限定することを心がけるようにしてください!

オススメの参考書(PR)

C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!

この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。

  • C言語でのメモリの使い方
  • 配列とポインタの関係性
  • ポインタのよくある使い方
  • ポインタの効果的な使い方

一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。

また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。

ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。

ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。

入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示