関数呼び出しとスタック
呼ぶたびに新しい箱ができる
前回、ローカル変数はスタックに置かれ、関数を抜けると無くなると書きました。今回はその積まれ方と降ろされ方を、自分の目で確かめます。
関数を呼ぶと、その関数のためのひとかたまりの領域がスタックに積まれます。これをスタックフレームと呼びます。中に入るのは引数とローカル変数、そして「戻ったらどこへ帰るか」の情報です。関数から return すると、そのフレームはまとめて降ろされます。
大事なのは、降ろされるのが最後に積んだものからという点です。main が a を呼び、a が b を呼んだなら、先に終わるのは b です。だから積み下ろしの順番が入れ替わることはありません。
再帰で見ると分かりやすい
同じ関数を自分の中から呼ぶと、フレームが何段も積み上がります。段ごとにローカル変数は別物なので、内側の呼び出しが自分の変数をいくら書き換えても、外側の変数には影響しません。
void dive(int depth) {
int local = depth * 10;
/* ここでもう1段深く呼ぶ */
/* 戻ってきたとき local は書き換わっていない */
}呼ぶ前と戻ったあとで同じ local を表示すると、この独立が出力の形で見えます。行が入れ子の形に並び、内側から先に閉じていくのが分かります。
消えた箱を指すポインタ
ここが本題です。ローカル変数のアドレスを return してしまうと、返ってきた時点でその領域はもう無いのに、ポインタだけが手元に残ります。これをダングリングポインタと呼びます。
int *make_score(void) {
int score = 82;
return &score; /* 危険。抜けた瞬間に score は無くなる */
}この関数はコンパイルが通り、呼んだ直後はたまたま82が読めることさえあります。降ろされた領域は消しゴムで消されるわけではなく、次に誰かが使うまで前の値が残っているからです。ですが次に別の関数を呼んだ瞬間、そこは上書きされます。たまたま動くという一番たちの悪い壊れ方をします。
配列でも同じで、char buf[32] を関数の中で作ってそのアドレスを返すのは同じ理由で誤りです。
ではどうするか
方法は3つあります。値をそのまま return する、呼び出し側が用意した領域のアドレスを引数で受け取って書き込む、そして次回学ぶ malloc でヒープに確保する、です。関数を抜けても残ってほしいものは、スタックに置いてはいけないと覚えるより、誰の寿命に合わせたいのかで選ぶと迷いません。
今回は再帰でフレームが積まれて降ろされる様子を出力し、寿命の順番を確かめます。
要件
- dive の中で depth * 10 をローカル変数に入れる
- 呼び出しの前に「入る」の行、あとに「出る」の行を表示する
- depth が 3 未満のときだけ 1 つ深く呼ぶ
- グローバル変数や static は使わない
入出力例
main("") → "入る depth=1 local=10
入る depth=2 local=20
入る depth=3 local=30
出る depth=3 local=30
出る depth=2 local=20
出る depth=1 local=10"