Akio's Log

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

プログラミングの禁じ手Web版 C言語編 - 過去をひきずるパターン(サルベージ版)

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

プログラミングの禁じ手Web版 C言語編 - 過去をひきずるパターン
http://web.archive.org/web/20070104215752/www.cmagazine.jp/src/kinjite/c/oldstyle.html

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



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

ここから転載です。



関数プロトタイプ宣言をしない

深刻度:★★★(重度)

[症状]

新しいコンパイラコンパイルしようとすると大量のwarningが出ることになります。また,「引数に正しい型が渡されない」「正しい戻り値を得られない」「リンクエラーでプログラムが作成できない」という症状も現れる場合があります。

[原因]

関数プロトタイプ宣言を知らない,理解していない,不勉強,怠慢が原因となります。

[対策/予防]

関数プロトタイプ宣言を勉強する以外ありません。関数プロトタイプ宣言を忘れるとコンパイル時に大量の警告が出されるだけではなく,予期しないパラメータの受け渡しによって発生する不可解なバグで苦しむこともあることを知るべきです。

[例外]

利用するコンパイラK&R第1版の仕様なら仕方がありません。ただ,その場合でも#defineでANSI C準拠のコンパイラを利用できるように工夫をしている例もあります。

[備考]

筆者の持っているコンパイラ(CodeWarrior Professional)でList 5のようなプログラムを実行しようとすると,

    theD = Double2(1);

の行でエラーメッセージを出して止まります。あきらかに関数プロトタイプ宣言がないのが問題なのですが,K&R第1版の知識しかないプログラマが,ここで怒りを感じて,なんとかコンパイラのエラーメッセージを止めてやろうとあがき,コンパイラの設定で強制的にプロトタイプ宣言のチェックを切ることもできます。こうすると,恐ろしいことに警告も出ずにそのままコンパイル・実行できるのです。ところが画面に表示されるのは期待していた1の2倍ではなく,

    6.000000

となってしまいます。あきらかにバグっているのです。ちなみにDouble2はList 6のようになっています。

List 5

    int main(void)
    {
        double theD;

        printf("hello\n");
        
        theD = Double2(1);
        
        printf("%lf\n",theD);
        
        return 0;
    }

List 6

    double Double2(double inD)
    {
        return inD + inD;
    }

これは,いったいどうしたのでしょう。デバッガでDouble2のinDの値を確認してみると,

    4.447232134992665e-291

とか,わけのわからない値が渡されていることが確認できます(処理系によってこの値は変化する)。実は,これこそ関数プロトタイプ宣言を付けなかった弊害です。おそらくは関数プロトタイプ宣言がなかったため,コンパイラは,

    double Double2(double inD)

ではなく,勝手に,

    int Double2(int inD)

とでも解釈したのでしょう。

関数プロトタイプ宣言はある意味,車のシートベルトのようなものです。安全性を確保し,あらかじめ関数プロトタイプ宣言をしておくことで,余計なデバッグ作業をなくしてくれます。反面,プロトタイプ宣言に慣れていないプログラマには「制約」としか感じられないのもシートベルトとにています。

ところで,こういう議論をすると,シートベルトをしたことによって,かえって悲惨な事故を導いた極端な例をあげて反論をする,うれしがりな人がいるのですが,しかしシートベルトで助かった数と悲惨になった数を比較すると,どちらが多いのでしょうかね? :-)


構造体の直接代入を使わない

深刻度:直接代入の使用を薦める

[備考]

K&R第1版とANSI Cの仕様において異なるもののひとつとして構造体の扱いがあります。K&R第1版のころの仕様では構造体の直接代入を認めていなかったので,List 7の最終行のようにmemcpyなどを使ってコピーをしたものです。

List 7

    f()
    {
        struct Hoge theA;
        struct Hoge theB;
        ...
        theA = theB; /* K&R第1版ではエラー */
        ...
        memcpy(theA,theB,sizeof(struct Hoge)); /* やむなくこう処置をした */
    }

ただ,筆者は直接代入できるのにあえてmemcpyを使うことは「禁じ手」と呼ぶほど悪いことだとは思えないので,あえて禁じ手パターンには入れていません。


「void *」を使わない

深刻度:void *の使用を薦める

[備考]

ANSI C規格では「void *」というのが使えるようになりました。これは「総称的な型のポインタ」というもので,特定の型のポインタに縛られることなく,ポインタとしての性格を強調したいときに便利なものです。

たとえば,最近の多くのコンパイラではメモリを動的に確保するmallocの関数プロトタイプ宣言を,

    void *malloc(size_t size);

としている場合が多いのですが,K&R第1版の仕様では,

    char *malloc();

としていました。すなわち,実際に確保するオブジェクトがcharポインタで指すものでなくても一律にcharポインタ扱いだったのです。

「void *」が使えることで,特定の型に束縛されない利点や,不自然な型キャストが防止できる(ただしC++の場合はそうともいえない)ようになります。

ただ,「void *」については,これを使わないことによる具体的な実害を聞いたことがないので禁じ手パターンには入れていません。