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)つまり、このプログラムは、アルゴリズムが間違っているにもかかわらず、 たまたま今まで動作していたのである。 処理系によってはエラーが出て止まってしまっていただろう。
/* * int assert(int expression); * このマクロは、プログラムに診断を挿入するとき有効です。 * 実行中に expression が偽(ゼロ)になった場合、 assert() は、 * 標準エラー出力にエラーを表示し、強制終了します。 * -DNDEBUG を指定してコンパイルすると、アサーションは * プログラムにコンパイルされません。 */
assert はさまざまなバグの予防に使用できるが、 配列とポインタの操作に限って用いても、 多くの誤りを検出できるはずである。
配列やポインタの中身を操作する時に、変数でその要素番号を 指定している場合には、添字が正しい範囲を取っていることを、 assert を用いて積極的に確認して欲しい。
また、デバッグが終ったプログラムにも assert は残しておくこと。 その行の周辺を修正したときに、新たなバグが発生するかも 知れないからだ。
バグの検出よりも速度を重視する場合は -DNDEBUG オプションを つけてコンパイルし直せば、assert マクロは完全に無視される。 assert を残しておいて不利になることは何もない。
各自、不適切なメモリアクセスを防ぐための assert() マクロを 挿入し、たまたま動くプログラムではなく、論理的なエラーの ないプログラムを再度提出せよ。