A「プログラムがStackOverflowで止まるんですけど。」
B「どうせ、どこかで無限に関数を呼び出しているんだろう。デバッガで追いかけてみたら?」
A「main関数の1行目で止まります。」
B「ちょっと見せてみろ…。」
B「むむ…。」
スタック(Stack)とは、データを管理する方法(データ構造)の1つで、データの追加(push)と取り出し(pop)が出来ます。データを追加すると、そのデータはスタックの中に下から順に積み上げられ、取り出す時はデータの一番上から取り出します。先入れ先出し(First In, First Out)と呼ばれます。
初期状態 ------------ X箱360 Wii ------------
PS3を追加 ------------ PS3 X箱360 Wii ------------
スタックから取り出す ------------ X箱360 Wii ------------ 取り出された値 PS3
スタックから取り出す ------------ Wii ------------ 取り出された値 X箱360
A「なるほど、こんなやり方があったんですか。今度使ってみます。」
B「データ構造とアルゴリズムといって、プログラムを書く上で重要な知識の1つだよ。」
A「でも、今回のプログラムではスタックなんか使っていないんですけど…。」
B「そんなことはない。見えないところで使っているだけだ。」
関数で宣言した自動変数(staticを付けずに宣言した局所変数)はどのようにメモリー上に配置されるのでしょうか。大域変数はプログラム開始時に値を格納するのに必要な領域が割り当てられます。自動変数はどうでしょうか。関数の場合は再帰呼び出しなど、ある関数が同時に呼び出されている状態になる場合があります。そのため、変数とメモリー上の領域を単純に1対1に割り当てることはできません。
さて、次のような関数を考えてみてください。
int kaijo(int i)
{
int ret;
if (i == 1) {
return 1;
} else {
return kaijo(i - 1) * i;
}
}
kaijo(3)を呼び出すと、その中でkaijo(2)が呼ばれ、さらにkaijo(1)が呼ばれ、kaijo(1)がreturnし、kaijo(2)がreturnし、kaijo(3)がreturnします。kaijo関数の変数iは、同時に3つメモリー上に存在することになります。
そこで、関数内の変数を格納するために関数フレームとコールスタックを用います。関数フレームは、1回の呼び出しの自動変数(引数を含む)を含みます。この関数フレームが、関数を呼び出す毎にコールスタックに積まれていきます。
A「あ、スタック出てきましたね。」
B「関数を呼び出すだけで、スタックの操作はコンパイラが勝手に用意してくれるわけだ。」
先程のkaijo関数の場合、kaijo(3)を呼び出すと、次のようにスタックが変化します。
--------------------- i = 3 kaijo(3) ---------------------
↓ kaijo(3)がkaijo(2)を呼び出す --------------------- i = 2 kaijo(2) i = 3 kaijo(3) ---------------------
↓ kaijo(2)がkaijo(1)を呼び出す --------------------- i = 1 kaijo(1) i = 2 kaijo(2) i = 3 kaijo(3) ---------------------
↓ kaijo(1)がreturnする。 --------------------- i = 1 kaijo(1) i = 2 kaijo(2) ---------------------
↓ kaijo(2)がreturnする。 --------------------- i = 3 kaijo(3) ---------------------
以上のことを踏まえて、プログラムが動かなかった直接の原因を考えてみましょう。まずはちょっとした実験。次のプログラムをコンパイル実行してみて下さい。おそらく、何らかのエラーで異常終了します。
int main(void) {
int a[500001];
a[500000] = 0;
printf("hello\n");
return a[500000];
}
このmain関数の関数フレームの大きさはどのくらいでしょうか。
4byte * 約5000000 = 2,000,000byte
およそ2Mバイトです。ところが、VC++2005では、デフォルトのスタックサイズ(コールスタックで使える総容量)は1Mバイトです。つまり、main関数を呼び出すのに必要な領域が足りません。
StackOverflowは、このようにコールスタックが大きくなりすぎてスタックサイズを超えてしまったときに発生します。
B「さて、君の問題のプログラムを見てみようか。」
class CEnemyBullet
{
double x;
double y;
double v;
…
}
class CEnemy
{
…
double x;
double y;
int hp;
CEnemyBullet bullets[200];
…
}
int main(void)
{
…
CGame game;
CEnemy enemys[100]; //自動変数
…
}
double型は64bitで、省略した部分も含めてCEnemyBulletsクラスの総容量は約60byteとします。CEnemyクラスの総容量は最低でも60byte * 200 = 1200byte。よって、main関数で必要なフレームの大きさは最低でも1200byte * 100 = 約1.2MByte。コールスタックの大きさを超えてしまいます。
A「でも、このPCはCore2Duo 6550で、2GのDDR2メモリーを載せているんですけど…。」
A「それでも1Mしか使えないというのはおかしくないですか?」
ここで、malloc関数/new演算子を思い出して下さい。(知らない人はC言語入門書を読む。)これらは、ヒープという領域にデータを保持するための場所を確保するものでした。ヒープはメモリーを使い果たすまで使うことが出来るので、大きなデータはヒープに確保します。
また、クラスのインスタンスを生成するときは、次のようにするのが一般的です。
CGame* pGame = new Game();
CEnemy* pEnemies[200];
for (int i = 0; i < 200; i++) {
pEnemies[i] = new CEnemy();
}
B「ただし、クラスの使い方とか、敵は出現する度にnewするとかはまた別の話だ。」