コンパイルをするときに指摘されるエラーは、主に 文法的エラーであり、これは文法に合うように書き換えることで 比較的すぐに修正することができる。
いっぽう、コンピュータに与えるプログラムに含まれる論理的誤りのことを バグ という。
俗な言い方で、プログラムにバグがある状態 のことを 「バグっている」という。ゲーム少年が、ゲームをやっているときに 画面が壊れたりすると「あ、バグった」のように表現することがあるが、 そもそも元々バグのあるプログラムは常に「バグっている」ので この表現はおかしい。ということをゲーム少年に指摘したところで どうにもならない。
プログラムにひそんだバグを取り除く作業のことをデバッグ とい う。デバッグするためには、プログラムが動くときのことを頭の中で追いかける 必要があるので、文法エラーを取り除くのよりはるかに時間がかかる。プログラ ムを作る過程では、最初に書き進めて行く時間よりも後から間違いを直す(デバッ グする)時間の方が長いと言っても良い。熟練するにつれ、デバッグにかかる時 間が短くなり、同時に最初からバグのないプログラムが作れるようになる。
授業で作るプログラムも徐々に複雑になっていくので、プログラミングに掛 ける時間を節約する意味で、
を覚えておこう。
関数本体や、制御構文のブロックを括る中括弧 {}
は
閉じ忘れでエラーになることが最初のうちは多い。閉じ括弧を
後で入れるから忘れるのである。開き括弧を打ったらすぐ閉じ括弧も
書くようにすると忘れない。
if (y%3 == 3) { ←閉じ括弧を書いてから中味を書き始める }
ファイルを開いて閉じる fopenとfclose
も先に
fclose()を書いてしまうようにすると良い。
FILE *fp;
fp = fopen("important.data", "r");
fclose(fp);
大事な意味を持つ変数を、x
とかx2
無味乾燥な名前にせず、maxninzu, goukei
といった
値の意味が想像しやすい名前にする。
また、変数名のスペルミスでエラーになるのは悲しいので、 積極的に補完を利用する。Emacsであれば単語を2〜3文字打った状態で M-/ を押すとバッファ内に同じ単語があったときに 単語の残り部分を補ってくれる。
制御構造や条件分岐にはかならず意味があるはずである。 意味を表すようなコメントを書いておく。
無意味なコメントの例
int point[100]; /* point配列を100個用意 */ for (i=0; i<maxninzu; i++) { /* 0からmaxninzu-1 までくり返す */ p = point[i]; /* p に point[i] を代入する */ }
意味のあるコメントの例
int point[100]; /* 得点を記憶する変数(最大100人分) */ for (i=0; i<maxninzu; i++) { /* 生徒全員に対してくり返す */ p = point[i]; /* i 番目の学生の得点 */ }
Emacsであれば、M-;(メタセミコロン)で簡単に
/* */
が入れられる。
printf
デバッグプログラミングの初歩の段階では、変数の値が未定義だったり、
変数の値を変えずにループを作って暴走させてしまったりと、
変数の値が予期せぬ状態になっていることが原因のバグが多い。
これを確認するために、現在の変数の値を要所要所で出力する
printf
をさし込むと良い。
/* 数当てゲーム */
int answer;
int times=0;
answer = random();
printf("debug: 答は%d\n", answer);
printf("さあいくつでしょう!: ");
このように、printf
を挟んで、そのときの変数状態を
表示してデバッグする方法を、printf
デバッグ
といい、初歩的ではあるが複雑なプログラミングのときにも
有効な方法である。
プログラムのソースとの対応を取りながら、プログラムを 少しずつ動かしたり止めたりしつつデバッグを支援するツールのことを ソースレベルデバッガという。
101/102教室環境では、GDB(GNU Debugger)というソースレベルデバッガ
が利用できる。コマンド名は gdb
である。GDBを使うためには
gccでコンパイルするときに -g
オプションをつける。
% gcc -g -o aisatsu aisatsu.c
これで、デバッガに必要な情報が組み込まれた実行プログラム
(aisatsu
)が生成される。
取り敢えず「これだけ覚えておけば何とか使える」レベルでの、
gdbを用いたデバッグの流れは以下のようになる。デバッグしたいプログラム
の名前を foo.c
とする。
gcc
に -g
オプションをつけてコンパイ
ルする。
% gcc -g -o foo foo.c
gdb の後に実行プログラムを指定する。
% gdb foo
(gdb)
すると、メッセージが出た後で (gdb)
のように
gdbのプロンプトに変わる。ここにはgdbのコマンドを打って行く。
gdbのコマンドには、run, step, print, continue, ...
など
様々なものがある。良く使うものは省略形が用意されている(たとえば、
step
コマンドは s
だけで良い)。
(gdb)
プロンプトで、リターンキーだけを押すと直前に打っ
たgdbコマンドがもう一度くり返される。
quit
コマンドで終了する(省略形 q
)。
C-d をタイプしても良い。
list
コマンドを打つとプログラムが10行ずつ
表示されるので「ここでプログラムを止めたい」と思う行の
行番号を調べる。
(gdb) list
list
コマンドの省略形は l
(エル)。
list 3
のように表示し始める行番号も指定できる。
break コマンドでプログラムの動作を
止めたい行番号を指定する。ブレークポイントを設定しないで実行すると
一気に終わりまでいって終了してしまうので普通はどこかで実行を止めて
そこから1行ずつ実行(ステップ実行)してデバッグする。
たとえば、7行目で止めたかったら
以下のようにする。break
コマンドは b
と省略しても良い。
(gdb) break 7 (または) (gdb) b 7
ブレークポイントには、関数名を指定しても良い。main
関数の実行をしょっぱなから止めたいときは
(gdb) break main
とする。
run
コマンドでデバッグ実行する。
run
コマンドは r
と省略できる。
(gdb) run (または) (gdb) r
もし、プログラムの起動に引数を与えたいときはrun
コマンドに引数を与える。たとえば、直接コマンドを起動するとき
% ./foo < data.txt
とするのであれば、gdbのなかでは、
(gdb) run < data.txt
のようにする。
b
で設定したブレークポイントの行で
プログラムが止まるのでそのあと「ステップ実行」、
「変数の値表示」などをして調査する
(gdb) s (またはstep) (gdb) p 変数 (またはprint)
quit
コマンド(省略形 q
)でgdbを終了する。
(gdb) q The program is running. Exit anyway? (y or n) y
Emacsのなかでgdbを動かすともっと分かりやすくなる(が、その分 覚えなければいけない操作も多い)。
まずプログラムをデバッガで動かすときに、ここで止めたいというところの 行番号を覚えておく。Emacsではモードラインに行番号が出ている
% gcc -g -o foo foo.c
Emacsで、デバッグしたいCプログラムを開いている状態で M-x gdb として、gdb を起動する。
Run gdb (like this): gdb
とgdbの起動方法を聞いて来るので、プログラム名を指定する。
Run gdb (like this): gdb foo
Emacsの中でgdbが起動し、gdbバッファに切り替わる。
*gdb-foo*
というバッファになるのでこのバッファ名も覚
えておく。元のプログラムも表示させたい場合は
C-x 4 b [Ret] と押すとウィンドウが2分割され元のソースが
選択される。C-x o で上下のウィンドウを行き来する(マウス
で選択しても良い)。
覚えておいた行番号、あるいは関数名にブレークポイントを設定する。
(gdb) b 7 (または) (gdb) b main
(gdb) run
隣のウィンドウにCのソースが現れて、実行中の行のところに △マークが表示される。
単純版の説明と同様 s, p
コマンドで
それぞれステップ実行、変数表示する。
gdbモードでは、ソースプログラムにポイントを合わせて操作することもできる。
以下はrun
したあとで、ふたたびCのソースが表示されている
ウィンドウに戻った後の操作である。
ポイントのところにある変数の値を表示する。
ポイントのある行にブレークポイントを設定する。
ポイントのある行にブレークポイントを解除する。
ステップ実行する(gdbのs
コマンド)
次のブレークポイントまで継続実行(gdbのc
コマンド)
1から0.1刻みでカウントダウンし、0になったら終了する プログラムを作ってみよう。ちょ簡単。
プログラムは以下のように作る。
float
型の変数を宣言し、1で初期化
while
ループを作る。0でない(!=0)間くり返す
printf
)
#include <stdio.h> int main() { float count=1.0; while (count != 0) { printf("%f です!\n", count); count -= 0.1; } puts("おしまい"); }
実際に実行してみよう。
このプログラムは暴走する! 暴走プログラムを止めるときは C-c をタイプして止める。
あとでgdbでデバッグするので、コンパイル時に -g
オプショ
ンをつけておく。
% gcc -g -o countdown countdown.c % ./countdown 1.000000 です! 0.900000 です! 0.800000 です! 0.700000 です! 0.600000 です! 0.500000 です! 0.400000 です! 0.300000 です! 0.200000 です! 0.100000 です! -0.000000 です! -0.100000 です! -0.200000 です! -0.300000 です! -0.400000 です! -0.500000 です! : : : (以下延々止まらない)
慌てず騒がず C-cで止めよう。 プログラムが止まらないのはなぜだろう。
while (count != 0)
で止まらないということは、
count == 0
にならないということである。ならば、
引き算(count -= 1
) がちゃんとできていないのではないか?
と、代替の怪しいところを予想して、
gdbで実行してみよう(コンパイル時に -g
をつけ忘れない)。
% gdb countdown (gdb) list 1 #include2 3 int main() 4 { 5 float count=1.0; 6 while (count != 0) { 7 printf("%f です!\n", count); 8 count -= 0.1; ← この行で止めたい 9 } 10 puts("おしまい");
8行目にブレークポイントを設定する。
(gdb) b 8
Breakpoint 1 at 0x106bc: file countdown.c, line 8.
実行する。
(gdb) run
Starting program: /home/irhome/c101/ta01002/C-lang/countdown
Breakpoint 1, main () at countdown.c:8
8 count -= 0.1;
count
変数の値を確認してみる。
(gdb) p count 変数名は [TAB] キーで補完できる $1 = 0.899999976
0.9であるはずの値が、0.899999976になっている。ステップ実行
(s
)を繰り返してcount
変数から
0.1を引いたときの値を表示すると予想外の値が表示される。
(gdb) s
9 }
(gdb) [RET]のみ
6 while (count != 0) {
(gdb) [RET]のみ
7 printf("%f です!\n", count);
(gdb) [RET]のみ
0.900000 です!
(gdb) [RET]のみ
8 count -= 0.1;
(gdb) p count
$2 = 0.899999976
0.8のときもやっぱりおかしい。継続実行(continue; 省略形c
)
をcount
が0になるはずの10回目まで繰り返してみよう。
(gdb) c Continuing. 0.800000 です! Breakpoint 1, main () at countdown.c:8 8 count -= 0.1; (gdb) [RET]のみ 0.700000 です! Breakpoint 1, main () at countdown.c:8 8 count -= 0.1; : : (以下リターンのみを「-0.000 です!」まで繰り返す) : -0.000000 です! Breakpoint 1, main () at countdown.c:8 8 count -= 0.1; (gdb) p count $3 = -7.30156913e-08
この値は -7.30156913×10-8 という意味で、 小数点以下0が7個続いた後に7301...とくる小さな負の値である。 コンピュータの内部で小数を表す浮動小数点数では2進数を基準にするのだが 2進数で10進数の0.1を表すと
0.000110011001100110011001100.... (2進数)
という循環小数になり、小数では正確に表せない。したがって、10進数の 0.1を足したり引いたりという計算はコンピュータはとても苦手とする のである。
もし0.1刻みに数を進めるループを作りたいときは、1〜10で繰り返す ループを作り、ループの中でループ変数を10で割って利用する。
今回のプログラム countdown.c
を直すには、
while
の終了条件に不等号を利用する。
#include <stdio.h>
int main()
{
float count=1.0;
while (count >= 0) {
printf("%f です!\n", count);
count -= 0.1;
}
puts("おしまい");
}
こうすることで、すくなくとも負の数になったら絶対に止まる
ようになる。一般的に、数値を変動させながら回すwhile
ループ
の条件には、== や !=
よりも 不等号を利用した方が
安全な場合が多い。
知らなくても何とかなるが、覚えるとより効率的にデバッグできる。 以下は、gdbのプロンプトで打つコマンドである。gdbのプロンプトでは gdbコマンドや変数名を打つときに [Tab] キーによる 補完が効く。
gdbコマンド ()内は省略形 | 意味 |
---|---|
info breakpoints |
ブレークポイント一覧表示 |
delete 番号 |
指定した番号のブレークポイントを削除する |
break 場所 if 条件式 |
指定した場所(行番号や関数名)で「条件式」が成り立つ場合のみ 停止する条件つきブレークポイントを設定する |
tbreak 場所 |
break と同じだが一度だけ停止する |
set 変数=値 | 変数の値を設定する |
call 関数 | 指定した関数を直接呼ぶ |
where | 今どこで止まっているかを表示 |
step [数] |
ステップ実行する。関数呼出しがあればその関数の中に入ってステップ実 行する; 「数」を指定するとその数だけ連続してステップ実行。「数」 を省略すると1行実行 |
next [数] |
ステップ実行と同じだが関数呼出しは飛ばす |
finish |
関数の最後まで継続実行する |
print 式 |
「式」の値を表示(変数の値を調べる) |
display 式 |
「式」の値を自動的に表示する。disp x*2 など
数式が書ける |
info display |
登録されているdisplay 一覧表示 |
delete display 番号 |
指定した番号のdisplay を削除する |