第40回
プログラミングの周辺事項(3)~適切なコメントとは?

コメントの種類と注意点

先述したように、コメントにはいくつかの役割があります。それぞれの書き方と注意点を掲げておきましょう。

タイトルコメント

ソースファイルの先頭に「このソースファイルにはどのようなプログラムが定義されているか」を記述します。

1つのソースファイルから1つの実行形式ファイルを作る、いわゆるシングルモジュールの場合なら、実行形式ファイルの名前と機能、起動方法、オプションなどの書式を記述しておきます。

複数のソースから1つの実行形式ファイルを作るマルチモジュールの場合は、モジュールを定義しているソースファイルごとに、そのモジュールの役割――定義している関数や構造体、記号定数などの名前と機能を簡潔に記述しておくことになります。

以下の例は、テキストファイルを読み込んで複数のスペースをタブに、あるいは1個のタブを複数のスペースに変換するプログラムのタイトルコメントです。このように、コメントの各行の先頭に*記号を付けておけば、読む人がコメントかソースかで迷うことはなくなります。

例)ソースファイルの先頭に記述するタイトルコメント
/* ------------------------------------------------------------ *
 * stconv.c     テキストファイルのスペース<-->タブ変換          *
 * 書式 : stconv option <input file> <option> <input file> ...  *
 *        option の説明                                         *
 *         -Tn : スペースを「タブコード」に変換                 *
 *               1バイトだけのスペースはタブに変換されない      *
 *         -Sn : タブコードを「スペース」に変換                 *
 *        オプションに続く'n'でタブストップの間隔を指定         *
 *        <input file> で入力ファイルを指定                     *
 * ------------------------------------------------------------ */

関数へのコメント

関数を定義したら、必ずその説明をコメントとして記述します。短いソースではつい省略してしまうこともありますが、後から見直したときにその役割を理解するまで時間のかかることもあります。

2~3行程度の短いソースで内容が一目瞭然である場合を除き、最低限、引数と戻り値の意味は明示しておくべきです。

例)関数の役割を示すコメント
/* ----------------------------------------------------------- *
 * setdata : 構造体のメンバをセット                            *
 *   構造体のメンバtabの値をチェックし、妥当な値をセットする   *
 *   fnameを下にファイルをオープンし、ファイルポインタをfpに   *
 *   セットする--エラーならFALSEを返す                         *
 * ----------------------------------------------------------- */
int     setdata(STCONV *p)
{
    /* タブストップの調整 */
    if (p->tab < MIN_TAB)
        p->tab = DEFAULT_TAB;
    else if (p->tab > MAX_TAB)
        p->tab = MAX_TAB;

    /* ファイルのオープンとエラー処理 */
    if ((p->fp = fopen(p->fname, "r")) == NULL) {
        fprintf(stderr, "%s がオープンできません.¥n", p->fname);
        return (FALSE);
          ↑「失敗時に'FALSE'を返す」といったことは見ればわかるので省略
    }
    return (TRUE);
          ↑「成功時に'TRUE'を返す」といったことは見ればわかるので省略
}

変数・定数へのコメント

変数や記号定数を定義した箇所では、その役割(変数や定数の意味)をコメントとして記述します。

これも「見ればわかるだろう」と思い込んでしまいがちですが、時間が経つとわからなくなることもしばしばあります。

また、「見れば(ソースを読めば)わかる」ということは、ソースを読む人(自分の場合もあれば他者の場合もあります)に余計な負担を強いることにもなります。はじめてコメントを読む人に対して、その意味や役割をわかるようにしておくのが親切というものです。

以下の例ではすべての記号定数にコメントを付けていますが、SPACE、TAB、TRUE、FALSEなどはCに慣れていればそれこそ「読めばわかる」類なので、想定する他者のスキルによっては省略して構いません。

例)記号定数へのコメント
#define     DATMAX          8        /* オプションの最大数 */
#define     ARGMIN          2        /* 引数の最小値 */
                                     /* これ以下はエラーとする */
#define     BUFSIZE         1024     /* 文字列処理バッファのバイト数 */
#define     DEFAULT_TAB     8        /* 標準のタブストップ */
#define     MIN_TAB         2        /* 最小のタブストップ */
#define     MAX_TAB         16       /* 最大のタブストップ */
#define     SPACE           0x20     /* スペースコード */
#define     TAB             0x09     /* タブコード */
#define     _FF             0x0c     /* 改ページコード */
#define     TRUE            1        /* 論理値--真 */
#define     FALSE           0        /* 論理値--偽 */

構造体などへのコメント

構造体と共用体は独自の型を定義する機能なので、それがどのような意味と役割を持っているかを必ずコメントとして記述しましょう。

各メンバの役割も説明しておくべきです。

例)構造体の定義に対するコメント
/* 構造体 STCONV を定義 */
typedef struct {
          int     sw;     /* 0:TAB->SPACE 1:SPACE->TAB */
          int     tab;    /* タブストップ幅 */
          char    *fname; /* 入力ファイル名 */
          FILE    *fp;    /* ファイルポインタ */
        } STCONV;

部分的なコメント

関数の書式や働き、変数の役割などはそれぞれの定義・宣言箇所でコメントを記述しますが、その変数がなぜint型なのかといった説明や、関数内の1つの処理に対してなぜそのようにしたのか……といった説明は、都度、その箇所でコメントとして記述しなければなりません。

そして、実はこの部分的なコメントこそが、後でソースを読み返すときに非常に大きな意味を持ってきます。

特に、プログラムの改良や移植 ※1 のためにソースコードを書き換えるとき、想定される「なぜ?」に対する回答としてコメントを残しておかないと、無駄な労力を費やすことにもなりかねません。

たとえば、ある関数内でforループの繰り返し回数を「num-1回」にしたとします。
for (i=0; i< (num-1); i++) { ...
それはもちろん、ちゃんとした理由があってのことなのですが、普通に見れば「num回でいいんじゃないの?」と思われるかもしれません。

そして後々、別の誰か(ひょっとしたら自分自身)がこのソースを改良するとき、案の定「num回でいいんじゃないの?」と思って
for (i=0; i< num; i++) { ...
と、書き直したとします。そうやって実行形式ファイルを生成し、いざ動作テストをすると……そのときになってやっと「あそこでforループをnum-1回にした理由」が判明します。

そして結局、書き直す前の「num-1回」にまたまた書き直すことになります。実に無駄な労力です。このような無駄を避けるため、一般的な目で見たら「あれっ?」と思われそうな箇所には『なぜこのようにしたのか』をコメントとして記述しておくことが大切です。

同じプログラムを他のOSやハードウェアなど異なる実行環境で動作するよう作り替えること。OSやハードウェアに依存するライブラリを使っていた場合、ソースの先頭で取り込むヘッダファイルが異なることがあります。また、関数自体を作り直す必要が生じる場合もしばしばあります。その他、整数値にint型を採用している場合は、int型の実際の許容範囲が異なることがあるため注意が必要です
例)関数定義内に挿入した部分的なコメント
/* ----------------------------------------------------------- *
 * tb2sp : タブ->スペース 変換                                 *
 *   src  : 送り側文字列                                       *
 *   dest : 受け側文字列                                       *
 *   tab  : タブストップ数                                     *
 * ----------------------------------------------------------- */
void    tb2sp(char *src, char *dest, int tab)
{
    int     i, j, step;

    /* 送り側文字列の終端まで繰り返す */
    for (i = 0; *src != '\0'; i++) {
        /* タブコードが見つかったらスペースに変換 */
        if (*src == TAB) {
            /* 2バイト日本語への対処 */
            if ((i > 0) && (iskanji(*(src-1))))
                    *dest++ = *src++;
            else {
                /* タブコードをスペースに置き換える */
                step = tab - (i % tab);
                for (j = 0; j < step; j++)
                    *dest++ = SPACE;
                src++;
                i += (j - 1);
            }
        }
        else
            *dest++ = *src++;
    }
    *dest = '\0';
}