バグのないプログラムのために


まずは次のようなプログラムを見てほしい。
void change_lights( int lights[5][5], int X, int Y )
{
  int A;
 
  for(A = Y-1;A <= Y+1;A++){    /* (X,Y)とその上下の0を1に1を0に */
    switch( lights[A][X] ){     
    case 0:
      lights[A][X] = 1; break;
    case 1:
      lights[A][X] = 0; break;
    default: break;
    }
  }
これは、何人かの学生から提出されたレポートである。 確かに動作する。しかし、lights[A][X] の添字がどうもおかしな範囲を 取っているように思える。

そこで、この関数に次のような行を追加してみた。

#include <assert.h>

void change_lights( int lights[5][5], int X, int Y )
{
  int A;
 
  for(A = Y-1;A <= Y+1;A++){    /* (X,Y)とその上下の0を1に1を0に */
      assert( 0 <= A && A <= 4 );
      assert( 0 <= X && X <= 4 );
    switch( lights[A][X] ){     
    case 0:
      lights[A][X] = 1; break;
    case 1:
      lights[A][X] = 0; break;
    default: break;
    }
  }
このプログラムはこの行で停止してしまうのだ。
Assertion failed: 0 <= A && A <= 4, file main.c, line 10
Abort (core dumped)
つまり、このプログラムは、アルゴリズムが間違っているにもかかわらず、 たまたま今まで動作していたのである。 処理系によってはエラーが出て止まってしまっていただろう。


ここで、assert() マクロについて簡単に説明する。 これは ANSI C の標準仕様に含まれているマクロである。

/*
 * int assert(int expression);
 *    このマクロは、プログラムに診断を挿入するとき有効です。
 *    実行中に expression が偽(ゼロ)になった場合、 assert() は、
 *    標準エラー出力にエラーを表示し、強制終了します。
 *    -DNDEBUG を指定してコンパイルすると、アサーションは
 *    プログラムにコンパイルされません。
 */

assert はさまざまなバグの予防に使用できるが、 配列とポインタの操作に限って用いても、 多くの誤りを検出できるはずである。

配列やポインタの中身を操作する時に、変数でその要素番号を 指定している場合には、添字が正しい範囲を取っていることを、 assert を用いて積極的に確認して欲しい。

また、デバッグが終ったプログラムにも assert は残しておくこと。 その行の周辺を修正したときに、新たなバグが発生するかも 知れないからだ。

バグの検出よりも速度を重視する場合は -DNDEBUG オプションを つけてコンパイルし直せば、assert マクロは完全に無視される。 assert を残しておいて不利になることは何もない。

レポートの提出期限を1週間延期し、1/27 とする。

各自、不適切なメモリアクセスを防ぐための assert() マクロを 挿入し、たまたま動くプログラムではなく、論理的なエラーの ないプログラムを再度提出せよ。


Takuya NISHIMOTO
Last modified: Mon Jan 20 16:33:38 1997