プログラミングの禁じ手Web版 C言語編 - NULLに関するパターン /Top/今週のソースコード/プログラミングの禁じ手Web版 C言語編/ [←前] [次→] [C言語版一覧] [C++版一覧] |
|
|
|
NULLとゼロを間違える 深刻度:★★(中程度) [症状] 具体的な被害は表面化しにくいが,プログラムの保守や改造の段階で「あれ?」となるパターンです。筆者の経験では,あるひとりのプログラマが勘違いしている場合,同じ会社のほかのメンバも,やはり同じように誤解をし,一種の「文化」となっている場合があります。 [原因] C言語の仕様に対する勉強不足,あるいはK&R;を始めとする参考書の説明不足。 [対策/予防] 勉強するしか,しょうがないです。しかし,こういう症状を呈する人たちって,たいてい不勉強なんですよね(苦笑)。 [例外] なし。「事情がわかっていて,わざとやる場合はいいじゃないか」と反論したい人もいるだろうけど,あなた以外のプログラマが,あなた並に優秀である保証などないことをどうかお忘れなく。 [備考] この変形パターン(というよりも同種パターン)として,「NULL」と「数値のゼロ」と「'\0'」と「""」の違いがわからないというのもあります。たとえばList 1のようなプログラムでは,f1〜f4のうちどれが正しい処理でしょうか? List 1char gText[128]; void f1() { strcpy(gText,NULL); } void f2() { strcpy(gText,0); } void f3() { strcpy(gText,'\0'); } void f4() { strcpy(gText,""); } strcpyという関数は文字列をセットするための関数だということは,初心者プログラマでもよく知っていますし,わりあいに有名な関数です。だから「gTextに“ABC"という文字列をセットするにはどうしたらいいか?」という質問をしたら,誰もがList 2のように答えます。ところが「“ABC"の代わりに空っぽの文字列をセットするには?」と質問を変えると経験不足だったり勉強不足なプログラマは,とたんにボロを出します。 List 2void f0() { strcpy(gText,"ABC"); } 答えとして意味的に正しいのは「f4」だけで,ほかはみんな間違いです。おそらくプログラムが停止したり,意味不明な文字列がセットされたり,暴走するでしょう。簡単にいえば「NULLは無効なポインタ」「数値のゼロは単なるゼロ」「'\0'は文字列の文末コード(つまり単なるゼロ)」「""は空っぽな文字列」です。 実際に機械語に落とされた結果をながめると一発でわかるのですが,NULLにしても0にしても'\0'にしても「ゼロという値」がstrcpyのふたつ目の引数にセットされます。するとstrcpyはゼロ番地(NULL領域)から文字列をひっぱりだそうとします。結果的にゼロ番地にあるデータを運よくひっぱりだしてきたり(意味不明な文字列がセットされる),あるいは例外が発生してプログラムが停止します。一方, strcpy(gText,"");と記述した場合は,""によって有効な領域に空っぽな文字列が設置されるので,きちんと空っぽの文字列をひっぱりだすわけです。 NULL領域を読み書きする 深刻度:★★★(重度) [症状] 「原因不明のバグで悩まされる」「システムに落とされる」「移植性が極端に悪くなる」という症状が現れます。 [原因] NULL領域に対する理解不足,無知,不勉強が原因となります。 [対策/予防] NULL領域の読み書きをしないことです。 [例外] なし。 [備考] これは,ちょっとわかりにくいかもしれませんが,List 3のようなプログラムで発覚するパターンです。 List 3void f() { static char *theTxt; strcpy(theTxt,"TEST\n"); printf("%s",theTxt); } この場合,きちんとNULL領域に対する保護のきいたシステムでは,strcpyの段階で強制的にプログラムが停止させられます。ところが保護の甘いOS(たとえばMS-DOS)では,きちんと通ってしまうのが困ったところです(しかしプログラム終了時に「Null pointer assignment」という警告が出てくることがあります。これはNULL領域の破壊を警告しているわけです)。 これの変形パターンでList 4のようなプログラムを作って,うまくいったと喜ぶパターンがあります。List 4のようなプログラムを見せて「NULLは0番地だから,0番地がリスタートになるマイコンでリスタートをするのはこの手法でOKです」などと自慢するプログラマがいたら,相当のおっちょこちょいです。List 4が(たまたま)期待どおりに動作するのはあくまでこのプログラマが仕事をしているマイコンでの話であって,残念ながら0番地をどう使うかというのはCPUによって事情が変わってきます。 List 4void (*Restart)(void) = NULL; void f() { Restart(); /* リスタートする */ } List 4は極端に移植性が低い例で,暴走したり例外で落とされる例のほうが多いでしょう。そもそもNULLというのは「NG」という意味であって,NGと断わりのあるものをわざわざ使ってOKという結論を引っぱり出すのはかなり強引で無理な展開ではないでしょうか。 |