関数を呼ぶと、なぜ元の場所に戻ってこられるのでしょうか。しかも、呼ばれた側のローカル変数はどこに置かれているのでしょうか。その舞台裏がコールスタックです。
食堂のお盆を想像してください。新しいお盆は必ずいちばん上に積み、取るときもいちばん上から取ります。関数の呼び出しもこれと同じ規則で動きます。
- 関数を呼ぶ → その関数専用の作業台(スタックフレーム)を積む
- フレームには、引数・ローカル変数・戻り先の住所が入る
returnする → いちばん上のフレームを丸ごと捨て、戻り先へジャンプする
#include <stdio.h>
int square(int x)
{
int r = x * x;
return r;
}
int calc(int a)
{
int s = square(a);
return s + 1;
}
int main(void)
{
int n = 3;
int ans = calc(n);
printf("%d\n", ans);
return 0;
}main → calc → square と3段積まれ、square が終わると上から順に降りてきます。降ろされたフレームの中身(x や r)は、そこで消えます。前のトピックの「ブロックを抜けると変数が消える」は、この仕組みの結果だったわけです。
下のビジュアライザで、フレームが積まれて降りる様子を1行ずつ確かめてください。
フレームに入っている「帰り道」
フレームの中でいちばん地味で、いちばん重要なのが戻り先の住所です。calc の中で square を呼ぶとき、「終わったら calc の続きに帰る」という情報も一緒に積まれます。これがあるから、同じ square を100か所から呼んでも、それぞれ正しい場所へ帰れます。関数が「行って帰ってこられる」のは、この一枚のメモのおかげです。
積みすぎると崩れる
#include <stdio.h>
void dig(int n)
{
int buf[1000]; // 呼ぶたびに約4KBを積む
printf("%d\n", n);
dig(n + 1); // 止まらない!
}
int main(void)
{
dig(1);
return 0;
}このプログラムは数千回から数万回で異常終了します(回数は環境によって変わります)。return に到達しないので、フレームは積まれる一方で降りてきません。積む場所には上限があるため、いつか限界に達します。
罠・注意
- スタックの広さには上限があります(数MB程度が一般的)。積みすぎるとスタックオーバーフローで異常終了します。原因の代表は、止まらない再帰と、
int buf[1000000];のような巨大なローカル配列です。 - 消えたフレームの場所を後から指し示してはいけません。「関数の中のローカル変数の住所を返す」は、Cで最も有名な事故のひとつです。この話は第2部のポインタで正面から扱います。
- 呼び出しの深さは、無料のトレース情報でもあります。エラー時に表示される「スタックトレース」は、この積み重なりをそのまま印刷したものです。どの関数から呼ばれてここに来たのかが一目で分かるので、デバッグの第一歩として必ず読む習慣を付けましょう。
まとめ: 呼べば積み、戻れば降ろす。コールスタックはCの実行の背骨で、スコープ・寿命・再帰・ポインタの事故まで、すべてここに繋がります。