ちょっとtwitter上でNULLに関することをつぶやきましたが、ツッコミが帰ってきてしまったため気になって仕様書(C89およびC99のもの)を読み込んでみました。そしてすべての論理をつなげてみると次のような結論となりました。
整数値の0もしくはそれをvoid *にキャストしたものがNULLとして定義される、と。
仕様書にはどう書いてあったか、というと・・・
C89でもC99でも記述は同じなのでそれは置いておいて。まず、null pointerおよびそれに関わる用語の定義です。関わる部分だけざっくりと翻訳すると次のようになります。
- 整数値の0、もしくはそれをvoid *にキャストして表したものをnull pointer constant(ヌルポインタ定数)と呼びます
- null pointer constant(ヌルポインタ定数)をそれぞれのポインタに変換したものをnull pointerと呼びます
- null pointerはほかのどのオブジェクトおよび関数とも一致しないことが保証されています
- null pointerをほかのポインタ型に変換してもそれはその型でのnull pointerとなります
- null pointer constant(ヌルポインタ定数)はstddef.hでNULLとして定義されます
というわけで、この部分からNULLの定義について読み取るなら、それは1行目と5行目をつなぎ合わせることになり、「NULLは整数値0もしくは(void *)0として定義する」という結論が得られるわけです。ただし、3行目の説明を読んでみると、「何も指さないことが保証されている」という参考書の説明とは微妙に食い違っているように見えるのですが…。細かいことをいうなら特定のメモリ空間をオブジェクトに含めるのかどうか?というところでしょうか。
いくつかのシステムにおける例外について
なお、以下はWikipediaの記述(+リンクされていた参考資料)から読み取っている項目ですが、いずれもかなり古いシステムにおいて
- null pointerが指しているアドレスが0でないシステムもある
- (void *)0をキャストしてアクセスすると特定の読み書きとして成立するシステムもある
とのことでした。前者は48bitアドレスというなかなか変わった方式のものですし、後者は仮想メモリやメモリ保護の概念がない時代のものですので、今の時代には全くそぐわないものですから無視してもよいかもしれません。
ちなみにif(!p){ }の是非について
これが「ポインタ変数pがnull pointerのとき」の意味になるのか?については、仕様書から読み取ると
- !pという演算は0==pと同一となる
- ポインタとnull pointer constant(ヌルポインタ定数)が比較される場合はポインタの型に合わせたnull pointerとの比較として扱われる
- 0はnull pointer constant(ヌルポインタ定数)として扱われる
ため、if(!p){ } はちゃんと「ポインタ変数pがnull pointerのとき」の意味になるようです。
意外と細かい仕様があってびっくり
というわけで「C言語の仕様書に従っている限りはNULLは0だし、null pointerによるifの判定にも誤りはない」という結論になりました。仕様書を読んでみるといろいろな動作についてちゃんと定義が書いてあるので、これに従ったコンパイラを完全に作るのは確かに大変だな、という感想もあります。ただし、逆にNULLをポインタにキャストしてアクセスした場合常にメモリ保護例外などのエラーとなるとは限らない、というのは調べていてびっくりした点です。今のシステムではほぼあり得ないことですが、昔のメモリ空間がカツカツだったりする環境のためなのでしょうかね。
C++にはnull pointerを表す定数が追加されていることを初めて知った
しばらく技術書を細かく眺めていなかったので知らなかったのですが、nullptrが追加されていて、C++11以降ではこれがnull pointerとして扱われるようです。NULLは上記の定義より整数型としての値を持つことがあり、引数として整数型とポインタ型のオーバーロードがあった場合にNULLを指定すると誤って整数型を引数とする方が呼び出されてしまう、という問題が起こってしまいます。それを防ぐために明示的にnull pointerで呼び出していることを示すものということでした。そういえば昔この手の問題で悩まされたことがあったな~と懐かしみながら。