わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

デバッグしよう(4)〜論理エラーを見抜く

前回,リンクエラーに対処した時点でのソースは,次のようになります.

#include <stdio.h>

#define relatively_prime(a, b) (gcd((a), (b)) == 1)

int gcd(int a, int b);

int main(void)
{
  int x = 180, y = 120;
  int flag_rp = relatively_prime(x, y);

  if (flag_rp) {
    printf("%d and %d are relatively prime.\n", x, y);
  } else {
    printf("%d and %d are not relatively prime.\n", x, y);
  }

  return 0;
}

int gcd(int a, int b)
{
  int r = a % b;

  if (r == 0) {
    return a;
  }

  return gcd(b, r);
}

コンパイル,そして実行してみます.

$ env LC_ALL=C gcc relatively_prime.c -o relatively_prime
$ ./relatively_prime
180 and 120 are not relatively prime.

コンパイル時には,エラーも何も,メッセージが出ませんでした.これは,正常にコンパイルできたという意味です.
前回までは,実行ファイルの名前を指定していませんでしたが,gccの引数に「-o ファイル名」と書けば,実行ファイルの名前になります.「-O ファイル名」としないように.また,コンパイルするファイルと,実行ファイルの名前を同じにしないように.
そして,./(ドット,スラッシュ)に実行ファイル名をつけて,実行しました.180と120は互いに素「ではない」と出て,めでたしめでたしと,言いたいところです.
初期値を変えてみましょう.9行目の

  int x = 180, y = 120;

  int x = 180, y = 121;

に書き換えてみます.121 = 11 * 11なのに対し,180は11(素数)を約数に持ちませんから,互いに素と出るはずです.

$ env LC_ALL=C gcc relatively_prime.c -o relatively_prime && ./relatively_prime
180 and 121 are not relatively prime.

あれれ,互いに素「ではない」と出ます.
ではまたバグの原因を探しましょう.
プログラムファイルを丹念に見ていけば,原因が見つかるものですが,ここでは,「デバッグライト」または「printfデバッグ」と呼ばれる方法をとることにします.これは,処理の途中にprintfを使って,気になる値を出力させ,そこから,どこまでは大丈夫でどこからが意図しない値になっていると見定めていき,バグを発見する,というものです.
最大公約数を求めるgcdの戻り値が,ちゃんと最大公約数になっているかを確認してみましょう.main関数で,変数を宣言したあと,if (flag_rp)の前に,以下のコードを書きます.

  printf("gcd: %d\n", gcd(x, y));

xとyの初期値は,180,120に戻しておきます.
コンパイル・実行をすると…

$ env LC_ALL=C gcc relatively_prime.c -o relatively_prime && ./relatively_prime
gcd: 120
180 and 120 are not relatively prime.

あっと,おかしいですね.180と120の最大公約数は,60でないといけませんから.
x = 180, y = 121としたときは,「gcd: 2」と出ます.これも間違い.そしてそのために,関数形式マクロrelatively_primeの展開によって,(gcd((180), (121)) == 1)*1となり,==演算子の左辺が2,右辺が1となって,変数flag_rpには偽の値すなわち0が代入されるというわけです.
関数gcdの中身がおかしいようなので,ここにもprintfデバッグを試みてみましょう.このようにします.

int gcd(int a, int b)
{
  int r = a % b;
  printf("gcd(%d, %d) = \n", a, b);

  if (r == 0) {
    printf("%d\n", a);
    return a;
  }

  return gcd(b, r);
}

mainに埋め込んだ「printf("gcd...」の行は,コメントにしておきます.xとyの値は,180と121のままで.
ではまたコンパイル・実行.

$ env LC_ALL=C gcc relatively_prime.c -o relatively_prime && ./relatively_prime
gcd(180, 121) = 
gcd(121, 59) = 
gcd(59, 3) = 
gcd(3, 2) = 
gcd(2, 1) = 
2
180 and 121 are not relatively prime.

さて確認.

  • 180と121の最大公約数は,121と59(180を121で割った余り)の最大公約数に等しくて,
  • 121と59の最大公約数は,59と3(121を59で割った余り)の最大公約数に等しくて,
  • 59と3の最大公約数は,3と2(59を3で割った余り)の最大公約数に等しくて,
  • 3と2の最大公約数は,2と1(3を2で割った余り)の最大公約数に等しくて,
  • 2と1の最大公約数は,2.

各ステップ,いい感じに計算できています.
いえいえ,最後の「2と1の最大公約数は,2」は間違いです.2と1の最大公約数は,1でなければなりません.2と0の最大公約数なら,2となるのですが.
関数gcdの各処理を,じっくり見直すと,「aがbで割り切れるなら,aを最大公約数として返す」ということをしています.a = 2, b = 1を当てはめてみると,a=2を返します.
しかしここは,「aがbで割り切れるなら,bを最大公約数として返す」としないといけません.return a;(と,そのすぐ上のprintf出力)を書き換えましょう.

    printf("%d\n", b);
    return b;

これでOKです.x = 180, y = 120の場合や,他の値をいろいろ試してから,書き加えた3つのprintf呼び出しを削除すれば,デバッグ完了です.
このように,「プログラムの実行結果が意図した通りにならないエラー」*2のことを,論理エラーといいます.論理エラーを見つけるには,

  • 処理が本当に意図するもの(させたいこと)なのかを,コードを見てよく考えること

がよく説かれていますし,理屈としてはこれですべて見つかるのですが,いくらでもコンパイル・実行ができる現代のコンピュータ環境では,

  • 処理途中の変数の値を出力させ,期待する値と一致するかチェックすること

を積極的に取り入れるべきでしょう.
明日,続きを書きます.その後はまたギリシアのネタになります.

*1:はてな記法に引っかからないよう,全角のカッコに置き換えています.

*2:http://japan.zdnet.com/glossary/exp/%E8%AB%96%E7%90%86%E3%82%A8%E3%83%A9%E3%83%BC/