5章 ヌルポインター
5.1: そもそもこの悪名高いヌルポインターとは何か。
A:
C言語の定義によればどんなポインターの型にも特別な値、すなわち
「ヌルポインター」が存在する。このヌルポインターは他のどんなポ
インターの値とも区別可能で、「いかなるオブジェクトや関数へのポ
インターと比較しても等しくなることがないことを保証されている」。
すなわちアドレス演算子&を適用した結果がヌルポインターとなるこ
ともない。またmallocの呼び出しに成功した場合の戻り値がヌルポイ
ンターの場合もない(mallocは領域確保に失敗した場合にヌルポイン
ターを返す。これがヌルポインターの典型的な使い方である。その値
によりアドレス以外の意味をあらわす特別なポインターの値で、たと
えば「領域確保の失敗」とか、まだ「何も指していない」のような意
味を持つ)。
ヌルポインターは、初期化されていないポインターと考え方で異なる。
ヌルポインターは、何も指していないことを保証されている。 初期
化されていないポインターは、どこを指しているかわからない。質問
1.30 、7.1 、7.31 を参照のこと。
上の定義のところで述べたように、各ポインターの型ごとにヌルポイ
ンターが存在する。ヌルポインターの内部構造はポインターの型によっ
て異なるかもしれない。プログラマーは内部構造について知る必要は
ない。コンパイラは必要なら区別が付けられるように、どの型のヌル
ポインターが必要か知る必要がある(以下の質問5.2 , 5.5 , 5.6 を参照)。
References:
K&R1 Sec. 5.4 pp. 97-8; K&R2 Sec. 5.4 p. 102; ANSI
Sec. 3.2.2.3; ISO Sec. 6.2.2.3; Rationale Sec. 3.2.2.3; H&S
Sec. 5.3.2 pp. 121-3.
5.2: どうやればプログラムの中でヌルポインターを得ることができるのか。
A:
C言語の定義によれば、ポインターを書くべきところに現れた定数0は、
コンパイル時にヌルポインターに変換される。すなわち初期化・代入・
比較をするときに左辺/右辺のどちらかにポインター型の変数か式が
現れたときは、コンパイラはもう一方の側の定数0がヌルポインター
を要求していることを理解し、適切なデータ型のヌルポインターの値
を産み出す。したがって以下のCプログラムの断片は正しい。
char *p = 0;
if(p != 0)
(質問5.3 も参照)
しかし関数に渡される引数は必ずしもポインターを表しているかどう
か識別可能ではない。そのときコンパイラはキャストのついていない
定数0がヌルポインターを意味していることを判別できないかもしれ
ない。関数呼び出しの状況でヌルポインターを産み出すには、0がポ
インターを表わしていることを認識させるために明示的なキャストが
必要となるかもしれない。例えばUnixシステムコールのexeclの引数
は、文字列のポインターの可変個のリストで、ヌルポインターで終わ
る。正しい呼び出しは以下のようになる。
execl("/bin/sh", "sh", "-c", "date", (char *)0);
もしキャスト(char *)を省略するとコンパイラはヌルポインターを渡
すというふうには理解せず、整数0を代りに渡す(Unixのマニュアルで、
この例に誤解を招くような説明をしているものが多い)。
関数プロトタイプが有効範囲内なら、引数渡しの話は"代入の話"とな
り、たいていキャストを省略しても問題ない。なぜならキャストのつ
いていない0がポインターであることと、どの型のポインターが必要
であるということをプロトタイプがコンパイラに教えるので、コンパ
イラは0を正しくポインターに変換することができる。しかし関数プ
ロトタイプは可変個の引数をもつ関数の引数リストに型の情報を与え
ることが出来ない。そこで可変個の引数を持つ関数の場合は明示的な
キャストが必要である(質問15.3 参照)。可変個の引数を持つ関数や関
数プロトタイプを持たない関数については、すべてのヌルポインター
の引数を明示的にキャストすることが常に一番安全な方法である。こ
うすれば、vargsの関数やプロトタイプなしの関数が来てもいいし、
ANSI対応でないコンパイラを一時的に使うこともできるし、プログラ
マーがポインターについて理解してプログラムを書いたということを
コンパイラに伝えることができる(ちなみに、こう覚えておくのが一
番簡単である)。
要約すると
0にキャストをつけないでもよい場合: 明示的なキャストが必要が必要な場合:
初期化 プロトタイプがスコープにないときの関数呼び出し
代入 可変個引数の関数引数
比較
関数プロトタイプがスコープに入っているときの引数の数が固定の関数引数
References:
K&R1 Sec. A7.7 p. 190, Sec. A7.14 p. 192; K&R2
Sec. A7.10 p. 207, Sec. A7.17 p. 209; ANSI Sec. 3.2.2.3; ISO
Sec. 6.2.2.3; H&S Sec. 4.6.3 p. 95, Sec. 6.2.7 p. 171.
5.3: ポインターがヌルポインターでないかどうかのテストの省略形
「if(p)」は有効なのか? ヌルポインターの内部表現が0でない場合は
どうなるのか。
A:
C言語が式のブール値を必要とする場合(if、while、forやdo文におい
て、また&&、||、!、?:演算子と共に使う場合)、0と比較して等しい
場合は偽の値が産み出され、その他の場合は真が産み出される。すな
わち
if(expr)
と書いたらいつも、「expr」がどんな式かにかかわらずコンパイラは
必ず
if((expr) != 0)
と書かれたように基本的には動作する。ポインター式「expr」をpに
置き換えると、
if(p) は if(p != 0)と同じ
ということになる。比較をするので、コンパイラは(式では現れれな
いが)0がヌルポインターを表していると判断して正しいヌルポインター
の値を使う。インチキはない。コンパイラはこのように動き、どちら
の式についても同じコードを産み出す。ポインターの内部表現は関係
「ない」。
論理否定演算子!は以下のように記述することができる。
!expr は基本的に (expr)?0:1 と同じ
あるいは、((expr) == 0) と同じ
これから以下の結論が得られる。
if(!p) は if(p == 0) とおなじ。
if(p)のような"省略形"は、文法的には正しいけれど、よくない書き
方であると考える人もいる(よい書き方であると考える人もいる。質
問17.10を参照)。
質問9.2 も参照。
References:
K&R2 Sec. A7.4.7 p. 204; ANSI Sec. 3.3.3.3,
Sec. 3.3.9, Sec. 3.3.13, Sec. 3.3.14, Sec. 3.3.15, Sec. 3.6.4.1,
Sec. 3.6.5; ISO Sec. 6.3.3.3, Sec. 6.3.9, Sec. 6.3.13,
Sec. 6.3.14, Sec. 6.3.15, Sec. 6.6.4.1, Sec. 6.6.5; H&S
Sec. 5.3.2 p. 122.
5.4: NULLとは何で、どう#defineされているのか?
A:
書き方として、キャストのない0がプログラム内にあちこち散らばっ
ているのを好まない人がたくさんいる。そこでマクロNULLが
(<stdio.h>か<stddef.h>に)0として定義されている。(void *)でキャ
ストされているかもしれない(質問5.6 参照)。整数0とヌルポインター
定数の違いをはっきりさせたい場合は、NULLをヌルポインタが必要で
あるところならどこに使用してもよい。
NULLを使うことは書き方の約束事でしかない。プリプロセッサーは
NULLを0に戻し、その0はコンパイラによってポインターとして解釈さ
れる。それでも特に関数の引数ではNULLに(0にも必要であるのと同じ
で)明示的なキャストが必要かもしれない。質問5.2 の下の表は0だけ
でなくNULLにもあてはまる。(キャストの付いていないNULLはキャス
トの付いていない0と同等である)。
NULLはポインターとしてのみ使うべきである。質問5.9 を参照のこと。
References:
K&R1 Sec. 5.4 pp. 97-8; K&R2 Sec. 5.4 p. 102; ANSI
Sec. 4.1.5, Sec. 3.2.2.3; ISO Sec. 7.1.6, Sec. 6.2.2.3;
Rationale Sec. 4.1.5; H&S Sec. 5.3.2 p. 122, Sec. 11.1 p. 292.
5.5: ヌルポインターの内部表現に0でないビットパターンを使っているマシンでは、NULLはどう定義するべきかか。
A:
ほかのどんなマシンとも同じである。0 (または((void *)0))と定義
されている。
プログラマーがプログラム中に「0」や「NULL」と書いてヌルポイン
ターを要求したときに、そのマシンがどんなビットパターンをヌルポ
インターを表現するのに使っていたとしても、ヌルポインターを作り
出すのはコンパイラの仕事である。だからヌルポインターの内部表現
が0でないマシンでNULLを0に#defineするのは他のマシン上とおなじ
ように正当である。なぜならキャストのついていない0がポインター
を必要とする場所にあらわれた場合に、コンパイラは、そのマシンに
適切なヌルポインターを作り出すことができなければならないからで
ある。質問5,2 ,5.10 ,5.17 を参照のこと。
References:
ANSI Sec. 4.1.5; ISO Sec. 7.1.6; Rationale
Sec. 4.1.5.
5.6: もしNULLが以下のように定義されているとすると、
#define NULL ((char *)0)
キャストされていないNULLを引数として渡す関数呼び出しが動かなく
なるのでは?
A:
動かなくなる場合もある。ここで問題はデータの型が異なるとポイン
ターの内部表現が異なるマシンがあることである。上の定義は、キャ
ストなしのNULLをcharへのポインターを引数としてとる関数に渡すと
きはうまくいくが、その他の型のポインターを関数引数として取る場
合は問題がある。この場合は文法的に正しい、
FILE *fp = NULL;
のような例でさえうまくいかない場合がある。
にもかかわらずANSI CはNULLの定義方法としてその他に
#define NULL ((void *)0)
を許している。上の定義は、ポインターの型の扱いに関して間違いの
あるプログラムを動くようにするし(ポインターの内部表現がどんな
データ型でも同じマシンに限られる。そういう意味で役に立つとはちょっ
といいにくいが)、この定義により、NULLをポインターの意味以外で
使う間違いを見つけることができるかもしれない(たとえばASCIIのナ
ル文字(NUL)が本当は必要な場合など。質問5.9 参照)。
References:
Rationale Sec. 4.1.5.
5.9: もしヌルポインターを表わす数としてNULLと0が同じものを表すなら、
どちらを使えばよいのか。
A:
ポインターを表すすべての場面で、その値をポインターとして使って
いることの注意書きとしてNULLを使うべきだと信じているプログラマー
がたくさんいる。NULLと0を取り巻く混乱は、0を#defineの後ろに隠
してしまうことで輪をかけていると信じ、キャストのない0を代りに
使っている人もいる。この問題には唯一の正解というものは存在しな
い(質問9.2 と17.10 を参照)。Cプログラマーは、ポインターが必要な
状況ではNULLと0は交換可能で、キャストされていない0を使うことは
ぜんぜん問題ないことを理解しなければならない。NULLを使うことは
(0を使うのと違って)、ポインターが関係していることの親切な注意
書きでしかない。プログラマーはポインターの0と整数の0を区別する
必要があるときにはNULLに(自分で理解するかわり、あるいはコンパ
イラが解釈するかわりに)頼ってはいけない。
NULLを、ポインター以外の0が必要な場面に使ってはならない。プロ
グラムは動くかもしれないが、コンパイラに間違ったメッセージを送っ
ていることに違いはない(ANSIはNULLの定義に(void *)0を使うことを
許している。この場合ポインター以外が必要な場合は全然うまくいか
ない)。特にASCIIのナル文字(NUL)が必要な場合は、絶対にNULLを使っ
てはならない。必要なら自分で
#define NUL '\0'
を用意すること。
References:
K&R1 Sec. 5.4 pp. 97-8; K&R2 Sec. 5.4 p. 102.
5.10: でも0よりはNULLを使うほうが、NULLの値が将来代わることを考えると、特にヌルポインターの内部表現が0でないマシンについては優れているのでは。
A:
いや。(マクロNULLを使うことは好ましいかもしれないが、上の理由
からではない。) マクロを使って数を書くべきところをシンボルに置
き換えることは、値が将来変わるかもしれないから、よくやるけど、
これはNULLを0の代りに使う理由じゃない。もう一度説明しよう。C言
語は、ソースコード上の0が(ポインターを使う場面では)ヌルポイン
ターを作り出すことを保証している。NULLを使うのは、プログラミン
グの書き方の決まりでしかない。質問5.5 と9.2 を参照のこと。
5.12: 私は、データ型に応じたヌルポインターを作り出すのに以下のマクロ
を使っている。
#define Nullptr(type) (type *)0
A:
この技は、人気があるし見た目は魅力的だけれど、たいして役には立
たない。代入や比較の際には必要ない。質問5.2 を参照のこと。ソー
スの打ち込みの節約にも役立たない。こんなマクロを使っていると、
作者のヌルポインターの知識が怪しげであることをプログラムを読む
人に暗示し、読む人はこのマクロの定義されているところ、使われて
いるところ、どんな形でもポインターの使われているところをすべて
注意深くチェックしなければいけなくなる。質問9.1 と10.2 も参照の
こと。
5.13: 変だな。NULLは0となることが保証されている。けれどヌルポインターは0となることが保証されいないね?
A:
"ヌル"とか"NULL"とか"ナル"という単語が無造作に使われるときは以
下のどれかを意味する。
概念としてのヌルポインター。C言語内の抽象的な概念であっ
て質問5.1 で定義した。これは次のように実装されている。
ヌルポインターの内部(あるいは実行時の)表現、0ではない
かもしれないし、ポインターの型によって表現方法が異なる
かもしれない。実際の値はコンパイラの作成者しか関心を持
たないはずである。Cプログラムを書く人はそんなものを見
ない。なぜなら彼らが使うのは次のものである。
ヌルポインター定数。これは整数の定数0である(質問5.2 を
参照)。そしてこれはしばしば、次のマクロの後ろに隠れて
しまう。
NULLマクロ、これは「0」や「(void *)0」として#defineさ
れている。最後に、次のものは混同しやすいが、別のもので
ある。
ASCIIのナル文字(NUL)、これはすべてのビットが0である。
ただし名前が似ていることを除いてヌルポインターと共通点
はない。この文字がC言語では文字列を終了させるので、空
の文字列は次のように呼ばれる。
「ヌルストリング」。これは空の文字列("")の別名である。
この呼びかたを使うと混乱を招きそうである。なぜなら空の
文字列はナル('\0')文字と関係があるが、ヌルポインターに
は関係ない。ヌルポインターの話しをすると1.に戻る。
この資料ではヌルポインターという言葉を1の意味で、「0」という文
字を3の意味で、「NULL」という言葉を4の意味で使っている。
5.14: なぜヌルポインターに関する混乱が存在するのか。なぜこれらの問題がこんなに何度も出て来るのか。
A:
Cプログラマーは、昔から自分の書いたプログラムが走るマシンの実
装に関して必要以上に知りたがる。ヌルポインターがたいていのマシ
ンで、ソースコード上も多くの内部表現上も0であることが、保証さ
れない仮定を招いている。マクロ「NULL」を使うことで、その値が将
来変化するかもしれないことや、妙なマシン上では0でないことを指
しているように思えるかもしれない。「if(p==0)」と書くと、比較す
る前に0をポインター型に変換するというよりは、pを整数型に変換す
ることを必要とすると解釈されがちである。最後に(上の質問5.13 で
記述した)「ヌル」という言葉は、文脈によって異なる意味を持つの
に、違いを大目に見がちである。
混乱を避けるよい方法は、C言語にはキーワードがあって(Pascalの
nilのような)、それを使ってヌルポインター定数を要求すると考える
ことである。コンパイラはソースコードから型が決定できるときは、
"nil"を正しい型のヌルポインターに変換することができるし、でき
ないときは苦情を出す。実際にはヌルポインターのC言語のキーワー
ドは"nil"ではなく「0」である。0は"nil"とほとんど同じ働きをする。
違いは、ポインター以外が必要な状況ではキャストされていない0に
はエラーメッセージを出す代りに整数の0を作り出すことである。キャ
ストされていない0をヌルポインターのつもりで使うと、そのコード
はうまく動かないかもしれない。
5.15: 混乱している。このヌルポインターに関するごたごたが理解できない。
A:
以下の二つの簡単な規則に従え。
ソースコード内でヌルポインター定数が必要なら、「0」か
「NULL」を使え。
「0」や「NULL」を関数の引数に使うときは、呼ばれる側の
関数が想定しているポインター型にキャストしろ。
以下の議論は、ある種の誤解に答えるものか、ヌルポインターの内部
表現に関するものか(これは知らなくてよいものだ)、ANSI Cでの改良
点に関することである。質問5.1 , 5.2 , 5.4 を理解して質問5.3 , 5.9 ,
5.13 , 5.14 について考えれば、うまくやっていける。
5.16: ヌルポインターを取り巻くこれらの混乱を考えれば、単純にヌルポイ
ンタは内部的には0で表現されると決めてしまったほうが簡単なので
は。
A:
他に取り立てて理由がなければ、そうすることは望ましくない。なぜ
なら、そうすることはヌルポインターを特殊な0でない値で表現する
ほうが自然なマシンで、ヌルポインターの実装に必要以上の拘束を与
えることになる。例えば、ヌルポインターを不当アクセスだとしてハー
ドウェアで捕まえる仕組みになっていると、ヌルポインターを0以外
の特別なビットパターンで表現したほうが、むしろ自然である、
そのうえ、0に決め打ちにして何をもたらすのか。ヌルポインターを
理解することは、その内部表現が0であるかどうかの理解を必要とし
ない。ヌルポインターの内部表現が0であると仮定しても、コードは
少しも書きやすくならない(質問7.31 で述べる、あまりお薦めしたく
ないcallocの使い方を除く)。ポインターの内部表現が0であることを
保証したとしても、関数の引数内でのキャストが不要になるわけでは
ない。なぜならポインターの大きさは、intの大きさと違うかもしれ
ない。(もしヌルポインターを使うときに0の代りに質問5.14 で述べた
"nil"を代りに使っていれば、内部表現について仮定しようという気
さえ起きなかっただろう。)
5.17: ヌルポインターに0以外の値を使用するマシンや、異なる型のポイン
ターに異なる内部形式を持つマシンは本当に存在するのか。
A:
Prime50シリーズはすくなくともPL/Iでは、セグメント07777・オフセッ
ト0をヌルポインターの内部表現として使っていた。後のモデルはセ
グメント0・オフセット0をCのヌルポインターに使った。このために
TCNP(Test C NULL Pointer)のような新しい命令が、それまでに誤っ
た思い込みをして書かれた、ヘマなCプログラムを救済するために必
要となった。もっと古い(バイトアドレスではなく)ワードアドレス方
式のPrimeのマシンは、ワードポインター(int *)よりもバイトポイン
タ(char *)のほうが大きいことで悪名高かった。
Data GeneralのEclipse MVシリーズには、アーキテクチャが用意する
3つのポインターの形式(wordとbyteとbit)が存在した。このうち二つ
はCコンパイラによって使われる。byteポインターはchar *とvoid *
に、wordポインターは残りのすべてのポインター型に使われている。
Honeywell-Bullのメインフレームの中には、ビットパターン06000を
(内部の)ヌルポインターとして使っているものもある。
CDC社のCyber 180シリーズはリング・セグメント・オフセットからな
る48ビットのポインターを持っていた。多くのユーザー(リング11で
走る)はヌルポインターとして0xB00000000000を使う。古いCDCの1の
補数表現のマシンでは全ビット1のワードは、ありとあらゆる種類の
データの特別なフラグとして使われた。その中には違法アクセスも含
む。
古いHP 3000シリーズはバイトアドレスとワードアドレスで異なるア
ドレス指定の方法を使っていた。だから同じアドレスを指していても、
voidとcharのポインターは、intの(構造体なども)ポインターと違っ
た内部表現を持っていた。
Symbolics社のLISPマシンは、タグ付きアーキテクチャなので、そも
そもポインターを数値で表すという通常の概念さえ持たない。
(基本的には、存在しないのハンドル)をCの
ポインターとして使っている。
80*86プロセッサーは(PC互換機では)メモリモデルによっては、デー
タに16ビットポインターを使い、関数には32ビットポインターを使う。
また逆のこともある。
64-bitのCrayマシンの中にはint *の表現にワードの下48ビットを使
い、char *はワード内のバイトアドレスであることを示すために上16
ビットを使っているものもある。
References:
K&R1 Sec. A14.4 p. 211.
5.20: 実行時に出る「null pointer assignment(ヌルポインターによる代入)」
というエラーメッセージは何を意味するのか。どうやって問題をたぐっ
ていけばよいか。
A:
このメッセージは普通はMS-DOSのコンパイラーでしか発生しない(よっ
て19章参照)。ヌルポインターを使って(おそらく初期化しなかったか
ら) 0番地に書き込んだことを意味している(質問16.8 も参考)。
デバッガーは、0番地にブレークポイントを張ることや見張りを立て
ることなどを許しているかもしれない。もしくは0番地を先頭に20バ
イトかそこら別のバッファにコピーして、値が変化していないことを、
定期的に確かめるコードをチョイと書けばよい。