メモリバグ演習
動いているのに壊れているコード
今回は自分でゼロから書きません。すでに動く成績管理CLI が渡されていて、そこに 3 種類のメモリバグ が仕込まれています。あなたの仕事は、それを見つけて直すことです。
メモリのバグがやっかいなのは、その場では落ちない ことです。文法の間違いはコンパイラが止めてくれますし、ゼロ除算はその行で落ちます。ところが解放し忘れは何も起こさずに進みますし、解放済みの領域を触っても、たまたま中身が残っていればしばらく正しく動いてしまいます。落ちるのは何万回か動かしたあと、しかも原因とまったく関係ない場所です。
数えれば見える
このコースでは valgrind のような検査ツールが使えないので、自分で数える道具 を最初から用意してあります。my_malloc と my_free という薄い包みで、確保した領域のアドレスを配列に控えておき、確保と解放の回数を数えます。プログラムの最後に 確保4回 解放4回 のように出るので、数が合わなければどこかで解放し忘れています。
my_free はもう 1 つ仕事をします。渡されたアドレスが控えの中に見つからなければ、それは すでに解放した領域か、そもそも確保していない領域 です。その場合は本物の free を呼ばずに警告を出します。実際の free を二重に呼ぶとプログラムはその場で異常終了しますが、この包みを通しておけば、二重解放が起きたという事実だけが 1 行の出力として残ります。
解放してもポインタは元のまま
3 つめのバグは、リストを全解放したあとの head です。free に渡すのはポインタの 値 であって、変数そのものではありません。ですから free_all(head) を呼んでも head の中身は書き換わらず、もう存在しないノードの番地を指し続けます。これが ダングリングポインタ です。
こわいのは if (head != NULL) という見慣れた確認が通ってしまうことです。中身は解放済みなので、この判定に従って走査すれば何が起きるか分かりません。解放したら、その領域を指していた変数に NULL を入れる。これが習慣として要る理由がここにあります。
3つのバグの見つけ方
リーク は数字で見つかります。確保の回数と解放の回数が合わなければ、どこかで解放していません。ノード 1 つにつき確保は 2 回起きているので、捨てる側も 2 回でなければ釣り合いません。片方だけしか書いていない場所を探します。
二重解放 は警告の行で分かります。ただし警告が出た時点ではもう遅く、原因は「同じ領域を解放する行が 2 か所に散らばっていること」です。よくあるのは、後始末を関数にまとめたのに、呼び出す側にも以前の解放が残っている形です。今回のコードもその形になっています。
ダングリング は出力からは直接見えません。全解放のあとに何をしているかを読んで、もう存在しない領域を指したままの変数を探します。
直す順番
まず一度そのまま動かして、出力がどう崩れているかを読みます。次に確保と解放の回数が合わない原因を探し、最後に警告が出た行をたどります。3 か所とも、直すのは 1 行から 2 行です。原因を見つけるまでが長く、直すのは一瞬という比率も、実際のデバッグとよく似ています。
要件
- my_malloc と my_free と確保・解放の数え上げには手を入れない。バグはリストを扱う側にある
- 1 は名前と点数を読んで末尾に追加し「田中を登録しました」と出す。2 は登録順に「田中 80点」を並べる。0人なら「学生がいません」
- 3 は名前を読んで一致するノードを外し「田中を削除しました」と出す。無ければ「田中は見つかりません」
- 終了時は残ったノードを全部解放し、「リストは空です」「確保4回 解放4回」「終了します」の3行を出す
- 「解放済みの領域をもう一度解放しました」が1度も出ないようにする
- 確保の回数と解放の回数を必ず一致させる。削除した学生の名前ぶんも解放する
- 全解放したあとの head を、もう使えない番地を指したままにしない
入出力例
main("1 田中 80
1 鈴木 95
2
3 田中
2
0
") → "田中を登録しました
鈴木を登録しました
田中 80点
鈴木 95点
田中を削除しました
鈴木 95点
リストは空です
確保4回 解放4回
終了します"
main("1 佐藤 70
3 高橋
2
0
") → "佐藤を登録しました
高橋は見つかりません
佐藤 70点
リストは空です
確保2回 解放2回
終了します"