ポインタは常に何らかの値を保持しています。まだ指すべきオブジェクトがないとき(代入される前、指していたものが解放された後、あるいは関数が返すものを持たないとき)には、「ここには何もない」と紛れなく意味する値が必要です。その値がヌルポインタであり、書くときの名前が NULL です。
これにまつわる作法はごくわずかで、しかも絶えず元が取れます。対象がないポインタは NULL にする、参照外しの前に NULL を確認する、結果がないときは NULL を返す、それだけです。
NULL の正体
NULL はマクロで、<stddef.h> で定義され、<stdio.h>、<stdlib.h>、<string.h> などからも取り込まれます。展開されるとヌルポインタ定数、典型的には ((void*)0) か単なる 0 になります。
C 標準は肝心な性質を保証しています。ヌルポインタは、実在するどのオブジェクトや関数へのポインタとも等しくなりません。同じ型の 2 つのヌルポインタは、常に互いに等しくなります。
ほとんどのシステムはヌルポインタを (nil) か 0x0、つまりアドレス 0 として表示します。それはよくある実装であってルールではありません。標準はビット列がゼロであるとは決して約束していません。約束しているのは比較のほうです。ですから p == NULL と書き、表現に依存するものは決して書かないでください。
NULL の参照外しはクラッシュする
この世のあらゆる null チェックの理由がこれです。
int *p = NULL;
printf("%d\n", *p); // 未定義動作 - ほぼ確実にクラッシュ
Windows、macOS、Linux ではアドレス空間の最初のページが意図的に未マップのまま残されているので、CPU がアクセスをトラップし、OS がプログラムを終了させます。次のように表示されます。
Segmentation fault (core dumped) # Linux
zsh: segmentation fault ./program # macOS
この即座のクラッシュは機能です。3 週間後に見つかる静かな破壊ではなく、原因となったまさにその行での大声の失敗へと、ロジックのバグを変換してくれます。クラッシュの読み方と行の特定については セグメンテーション違反 を参照してください。
だからこそ、C において「ヌルポインタ」は捕捉できる例外ではありません。回復する仕組みはなく、あるのは事前にチェックすることだけです。
失敗しうるものをチェックする
失敗しうる関数は、それを NULL を返すことで知らせます。チェックは任意の礼儀ではありません。しなければ待っているのはクラッシュです。
標準ライブラリはこの種の関数だらけです。malloc、calloc、realloc、fopen、strchr、strstr、getenv、bsearch。どれも「結果なし」を NULL で答えます。確保まわりの全体像は 動的メモリ を参照してください。
チェックの書き方には、どちらもよく使われる 2 つの慣用句があります。
if (p == NULL) { /* 処理する */ } // 明示的 - 分かりやすさで推奨
if (!p) { /* 処理する */ } // 簡潔 - NULL が偽になることに依存
ヌルポインタは条件式で偽、有効なポインタは真になるので、どちらも動きます。第三者がバグを探してコードを走り読みするときは、明示的な形のほうが読みやすいでしょう。
NULL、0、未初期化
初心者がひとまとめにしがちな、3 つの異なる状況です。
int *a = NULL; 「何も指していない」 - 検査可能、安全にチェックできる
int *b; 未初期化 - ゴミを保持、検査は不可能
int *c = &x; 実在するオブジェクトを指している
危険なのは b です。そのバイト列はそのスタック領域に残っていた何かで、ゼロ(で、問題なさそうに見える)かもしれませんし、以前の呼び出しが残したもっともらしいアドレスかもしれません。有効なポインタと見分けられるチェックは存在せず、しかも挙動はデバッグビルドとリリースビルドで変わります。
すべてのポインタを初期化しましょう。 まだアドレスがないなら、NULL こそがそのアドレスです。-Wall -Wextra を付けてコンパイルすれば、コンパイラが未初期化の使用の多くを指摘してくれます。
0 について。ポインタの文脈では整数定数 0 がヌルポインタ定数そのものなので、p = 0; は正当な C です。それでも NULL を選びましょう。意図が伝わりますし、コンパイラが変換してくれない可変引数の呼び出しでは実害があります。
execl("/bin/ls", "ls", 0); // 危険 - ポインタが期待される場所に int が渡りうる
execl("/bin/ls", "ls", (char *)NULL); // 正しい
C23 は nullptr を追加しました。独自の型を持つキーワードで、この種のあいまいさを完全に回避します。NULL は今でもどこでも正しく、移植性があります。
防御的なパターン
ポインタを受け取る関数の冒頭でガードする。
解放した後は NULL にする。 free はあなたのポインタを変えません。メモリを返すだけです。残された古いポインタはダングリングポインタであり、それを使うのは未定義動作で、しかもすぐにはクラッシュしないことがよくあります。
この例に含まれる 2 つの事実は覚えておく価値があります。free(NULL) は何もしないと定義されているので、後片付けのコードでガードする必要は決してありません。そして free の後に p = NULL としておけば、黙ってデータを壊しうる解放後使用(use-after-free)が、即座でデバッグしやすいクラッシュに変わります。
「結果なし」を意味する NULL を返し、そう明記する。
// 一致した要素へのポインタを返す。見つからなければ NULL を返す
int *find(int *arr, int n, int target);
関数の上のコメントにそう書いておきましょう。NULL がありうると知っている呼び出し側はチェックを書きますし、知らない側は書きません。
手早いチェックリスト
- すべてのポインタを、実在のアドレスか
NULLで初期化する。 malloc、calloc、realloc、fopenの戻り値は、使う前にチェックする。- 不正な入力で呼ばれうる関数では、冒頭でポインタ仮引数をチェックする。
freeの直後にポインタをNULLにする。- ポインタを意図しているときは
0ではなくNULLと書く。 -Wall -Wextra付きでビルドし、可能ならサニタイザの下で実行する。gcc -fsanitize=address,undefinedは null の参照外しを正確なレポート付きで捕まえます。
よくある質問
C の NULL ポインタとは何ですか?
ヌルポインタ定数、つまり実在するどのオブジェクトのアドレスとも等しくならないことが保証された値を保持するポインタのことです。「このポインタはまだ何も指していない」と表す標準的な方法で、NULL はそのためのマクロです。<stddef.h> などいくつかのヘッダで定義されています。
C で NULL ポインタを参照外しするとどうなりますか?
未定義動作です。実際には、デスクトップやサーバーのシステムではアドレス 0 が意図的に未マップのままにされているため、セグメンテーション違反で即座にクラッシュします。メモリ保護のない組み込みシステムでは、代わりに黙って何かを読んだり壊したりすることがあり、そちらのほうがはるかに厄介です。
C で NULL と 0 は同じものですか?
ポインタの文脈では同じです。整数定数 0 がヌルポインタ定数なので、p = 0; も p == NULL も動きます。とはいえポインタには NULL、数値には 0 を使いましょう。どちらのつもりなのかが読み手に伝わりますし、裸の 0 がポインタではなく int として渡されうる可変引数の呼び出しでも安全を保てます。
NULL ポインタと未初期化ポインタの違いは何ですか?
NULL ポインタは確実に何も指しておらず、検査できます。未初期化ポインタはそのメモリにたまたま残っていたバイト列(有効に見えるアドレスかもしれません)を保持するので、それを捕まえられる検査は存在しません。ほかに入れるものがないときは、必ずポインタを NULL で初期化しましょう。