こんにちは、ナナです。
メモリの種類、2つ目の「スタックメモリ」について紹介します。
スタックメモリは、実は皆さんがプログラムを作る上で欠かせないメモリなんです。
「スタックメモリなんて知りません!」と言うかもしれませんが、知らないだけで常にお世話になっているのです。
スタックメモリがいったいどんな場面で使われるかを、解説していきましょう。
本記事では次の疑問点を解消する内容となっています。
では、スタックメモリの使い方を学んでいきましょう。
スタックメモリの「スタック」とはいったい何か?
はーい。今日のテーマは「スタックメモリ」ですねー。さっそく、おーしえーてくーださーい。
うん、うん。「スタックメモリ」について語る前に、そもそも「スタック」というものが何なのかを先に解説しようね。
スタックメモリを学ぶ前に「スタック」というものについて学びましょう。
スタックとは「先入れ後出し方式」のデータ構造のこと
「スタック」は、データ構造と呼ばれるものの1つです。
データ構造にはいくつかの種類があるのですが、スタックはLIFO(Last In First Out)と呼ばれる「先入れ後出し方式」のデータ構造です。
「先入れ後出し」とは「最初に入れたものが、後で出される」という形のデータ構造です。
スタックはよくお皿に例えられます。お皿って重ねて置きますよね。
最初に置いた皿は最後に取り出されます。これがスタック構造です。
この仕組みをプログラミングに応用したものを「スタック構造」と呼ぶんです。
スタック構造を利用した「スタックメモリ」の特徴を知ろう
ほーい。スタックってお皿のことなんですね。つまり、お皿のメモリが「スタックメモリ」ってことですね。ふむふむ、なるほどー。完全に理解しました!
うん、全然わかってないね。データ構造がお皿を片付けるときのイメージだよってだけで、スタックはお皿じゃないよ。
このスタック構造をメモリを利用する際に応用したものが、今日のテーマ「スタックメモリ」だよ。
スタック構造を利用して、メモリを管理したものを「スタックメモリ」と呼びます。
スタックメモリがどのように使われているのかを学んでいきましょう。
スタックメモリの特徴
スタックメモリは、他のメモリと比べて次の特徴を持ちます。
種類 | 配置データ | Read/Write | 容量 | 説明 |
---|---|---|---|---|
プログラムメモリ | 関数・定数 | R | 大サイズ | 関数や定数が配置 |
スタックメモリ | 変数 | R/W | 小サイズ | 関数内で定義した変数が配置 |
静的メモリ | 変数 | R/W | 中サイズ | 関数外で定義した変数が配置 |
動的メモリ | 変数 | R/W | 大サイズ | メモリ確保関数で取得 |
他のメモリに対して、サイズが小さいのが特徴的です。
スタックメモリはローカル変数が配置されるメモリ
スタックメモリは、関数内で定義されたローカル変数が配置されるメモリです。
次の「num1」と「num2」はローカル変数であり、スタックメモリに配置されます。
#include <stdio.h>
int main(void)
{
// ローカル変数の定義
long num1 = 100;
long num2 = 200;
printf("%d %d", num1, num2);
return 0;
}
ローカル変数の特徴は、関数が呼び出されると変数ラベルが貼られ、関数が呼び終わると変数ラベルが自動的に破棄されることです。
なぜ、ローカル変数はこのようなラベルの生成・破棄が可能なのでしょう。
それは、ローカル変数がスタックメモリに配置されるからなのです。
ローカル変数は「スタックメモリ」に配置されるからこそ、関数呼び出しと終了に連動して生成・破棄がされるのです。
その仕組みを解説していきますよ!
スタックメモリをイメージ図で見ながら理解しよう!
ほほほーい。あんなに使っていたローカル変数が、実はスタックメモリに利用されているとはびっくり仰天ですー。
ででで、ローカル変数がスタックメモリに使われるってどういうことですか?
理解しているようで、まだ全然理解できてないね。じゃあ、具体的なメモリのイメージ図を使って解説しようね。
スタックメモリは利用するたびに伸縮するイメージのメモリです。図で解説していきましょう。
スタックメモリの簡易イメージ図
通常メモリは横並びでイメージするのですが、スタックメモリに関しては縦方向に並ぶイメージで捉えます。
メモリの番地に着目してください。「スタックメモリ」は大きな番地から小さな番地に向けて進んでいくんです。
変な感じがしますが、これがスタックメモリなんです。
スタックメモリが実際に使用されるイメージ図
スタックメモリは関数が呼ばれると伸び、関数が呼び終わると縮みます。
では、プログラムが実際に動く流れにおいて、どのようにスタックメモリが使われるかを示しましょう。
関数「funcA」「funcB」「funcC」が、順に呼ばれることを想定します。
関数が順に呼ばれると、次のようにスタックメモリは伸長します。
続いて関数「funcC」「funcB」「funcA」の順に関数呼び出しが終了すると、次のように収縮していきます。
プログラムが動作している間は関数呼び出しと呼び出し終了に伴い、スタックメモリはこのように伸縮しながらメモリを使用しているのです。
ローカル変数のラベルはスタック機構により、関数呼び出しによりスタックに貼られ、関数呼び出しが終わるとスタックから剥がされるのです。
お皿が順に積まれて、取り出される動きになっていますよね。これがスタックメモリです。
関数呼び出しに従ったローカル変数の生成・破棄のタイミングは、スタックメモリをイメージしながら捉えるとよいでしょう。
スタックオーバーフローに注意せよ
はい、はーーい。ローカル変数を定義すると、どんどんスタックメモリの背が高くなっていくんですねー。うらやましー。
ででで、どこまでもどこまでも伸び続けて最後はどこに行きつくんでしょうか?
どこまでも行けるわけじゃないんだよ。メモリは有限の資源だからね。
スタックメモリのサイズは環境によって決まってるからね。使う時はサイズの範囲内でしか利用できないんだよ。
スタックメモリを使用する際の注意点があります。スタックメモリの範囲外まで、伸長してはならないことです。
スタックオーバーフローとは
スタックメモリとして使用できるメモリサイズは、あらかじめ決まっています。
Windowsアプリケーションであれば、スタックメモリは1Mバイト程度と比較的大きく割り当てられるのですが、組み込み開発のスタックメモリは数キロバイトといったことも珍しくありません。
そのため、組み込み開発では不用意にローカル変数をたくさん確保すると、あっという間にスタックメモリを食いつぶしてしまいます。
このように決められたスタックサイズ以上に、スタックメモリを使用することを「スタックオーバーフロー」と呼びます。
スタックオーバーフローは本当にやばいやつなので、実際の職場でやらかすと即不具合として認定されます。
スタックオーバーフローが発生するとどうなるのか?
スタックオーバーフローはビルド時にエラー検出ができず、実行時に発生します。
もしもスタックオーバーフローが発生した場合、システムはクラッシュします。
とある関数だけなら大したサイズでなくとも、関数呼び出しの深さにより結果的にスタックメモリを食いつぶすこともありますので、スタックは慎重に扱ってください。
自分以外の誰かが定義した構造体を、ローカル変数で使用する場合は特に注意が必要です。
思わぬメモリサイズが消費されている可能性がありますので、事前にsizeof演算子で構造体サイズを確認するといったことは効果的な方法です。
関数の引数も、ローカル変数なのでスタックを利用します。
大きなサイズの構造体を引数で値渡しするとスタックを消費するため、意図的にポインタ渡しにすることでスタック消費を抑えるテクニックがあります。