Akio's Log

ソフトウェア開発、プロジェクトマネジメント、プログラミング、ランニングなどなど

プログラミングの禁じ手Web版 C言語編 - NULLに関するパターン(サルベージ版)

C Magazineの「プログラミングの禁じ手Web版 C言語編」を、InternetArchiveからサルベージしました。

プログラミングの禁じ手Web版 C言語編 - NULLに関するパターン
http://web.archive.org/web/20070104215659/www.cmagazine.jp/src/kinjite/c/null.html

インデックスはこちら
プログラミングの禁じ手Web版 C言語編(InternetArchiveよりサルベージ) - Akio’s Log



(PR)プログラミング作法
(PR)C言語ポインタ完全制覇 (標準プログラマーズライブラリ)
(PR)改訂新版 Cプログラミング診断室

ここから転載です。



NULLとゼロを間違える

深刻度:

★★(中程度)

[症状]

具体的な被害は表面化しにくいが,プログラムの保守や改造の段階で「あれ?」となるパターンです。筆者の経験では,あるひとりのプログラマが勘違いしている場合,同じ会社のほかのメンバも,やはり同じように誤解をし,一種の「文化」となっている場合があります。

[原因]

C言語の仕様に対する勉強不足,あるいはK&Rを始めとする参考書の説明不足。

[対策/予防]

勉強するしか,しょうがないです。しかし,こういう症状を呈する人たちって,たいてい不勉強なんですよね(苦笑)。

[例外]

なし。「事情がわかっていて,わざとやる場合はいいじゃないか」と反論したい人もいるだろうけど,あなた以外のプログラマが,あなた並に優秀である保証などないことをどうかお忘れなく。

[備考]

この変形パターン(というよりも同種パターン)として,「NULL」と「数値のゼロ」と「'\0'」と「""」の違いがわからないというのもあります。たとえばList 1のようなプログラムでは,f1〜f4のうちどれが正しい処理でしょうか?

List 1

    char 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 2

    void f0()
    {
        strcpy(gText,"ABC");
    }

答えとして意味的に正しいのは「f4」だけで,ほかはみんな間違いです。おそらくプログラムが停止したり,意味不明な文字列がセットされたり,暴走するでしょう。簡単にいえば「NULLは無効なポインタ」「数値のゼロは単なるゼロ」「'\0'は文字列の文末コード(つまり単なるゼロ)」「""は空っぽな文字列」です。

実際に機械語に落とされた結果をながめると一発でわかるのですが,NULLにしても0にしても'\0'にしても「ゼロという値」がstrcpyのふたつ目の引数にセットされます。するとstrcpyはゼロ番地(NULL領域)から文字列をひっぱりだそうとします。結果的にゼロ番地にあるデータを運よくひっぱりだしてきたり(意味不明な文字列がセットされる),あるいは例外が発生してプログラムが停止します。一方,

    strcpy(gText,"");

と記述した場合は,""によって有効な領域に空っぽな文字列が設置されるので,きちんと空っぽの文字列をひっぱりだすわけです。


NULL領域を読み書きする

深刻度:

★★★(重度)

[症状]

「原因不明のバグで悩まされる」「システムに落とされる」「移植性が極端に悪くなる」という症状が現れます。

[原因]

NULL領域に対する理解不足,無知,不勉強が原因となります。

[対策/予防]

NULL領域の読み書きをしないことです。

[例外]

なし。

[備考]

これは,ちょっとわかりにくいかもしれませんが,List 3のようなプログラムで発覚するパターンです。

List 3

    void 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 4

    void (*Restart)(void) = NULL;

    void f()
    {
        Restart(); /* リスタートする */
    }

List 4は極端に移植性が低い例で,暴走したり例外で落とされる例のほうが多いでしょう。そもそもNULLというのは「NG」という意味であって,NGと断わりのあるものをわざわざ使ってOKという結論を引っぱり出すのはかなり強引で無理な展開ではないでしょうか。