C++言語におけるスタックメモリとヒープメモリ
プログラマーの尾関です。
今回はC++言語を勉強中の学生に向けた、スタックメモリとヒープメモリの違いについて説明しようと思います。
スタックメモリ(スタック領域)
スタックメモリとは、LIFO(後入れ先出し)方式で管理されるメモリのことです。
具体的には、関数の呼び出し時や。関数の引数、戻り値、ローカル変数、呼び出し元への復帰アドレスなどが格納されます。
ヒープメモリ(ヒープ領域)
ヒープメモリとは、プログラム実行中に動的に確保・解放されるメモリ領域のことです。
具体的にはmallocやnewを使って明示的にメモリを確保し、freeやdeleteによって任意のタイミングでメモリを開放します。
プログラマーが好きなタイミングでメモリ確保できることから、柔軟なメモリ操作を行うことができますが、その反面、メモリの開放漏れであるメモリリークや、メモリ空間に無駄が生まれて使用されないメモリが作られてしまう断片化のリスクがあります。
スタックに関連する用語
この説明だけだと概念的でイメージがしにくいと思いますので、スタックに関連する具体例を示したいと思います。
コールスタックとスタックメモリ
例えば、Visual Studioの「呼出履歴」は「コールスタック」とも呼ばれ、スタックメモリの仕組みと深く関係しています。

プログラムは関数を呼び出すたびに、「戻り先アドレス」「引数」「ローカル変数」などの情報をスタックにPushします。関数の処理が終了すると、これらの情報はスタックからPopされ、呼び出し元に処理が戻ります。
この「後入れ先出し(LIFO)」の構造が、呼び出し履歴の“スタック”(コールスタック)という概念の由来になっています。
メモリの断片化が発生しないメモリ管理方法
スタックメモリは、メモリ領域を「上に積む」ように順番に使用することで、隙間なく効率的にメモリを利用できるという大きなメリットがあります。スタックは「LIFO(後入れ先出し)」の原則で動作し、メモリの出し入れは常に一番上(トップ)でのみ行うという制限がありますが、このシンプルなルールによって、メモリの断片化(隙間)が発生しません。

また、関数の呼び出しが終了すると、その関数で使われていたスタック領域はまとめて一度に開放(ポップ)されます。これにより、複雑な管理をせずにすべてのメモリを効率よく開放できるという利便性もスタックメモリの特徴です。
このように、スタックメモリは「出し入れの制限が大きい」反面、「隙間なく使える」「一括開放が容易」といった効率性と管理のシンプルさが強みです。
スタックオーバーフロー
プログラムを開始するときに、スタックメモリのサイズは固定サイズとなります。
Visual Studio 2022 のC++プロジェクトであれば、通常スタックサイズはおおよそ「1MB」となります。
#include <iostream>
struct BigData {
char data[1024 * 1024]; // 1MBのデータ
};
int main()
{
BigData bigData; // 大きなデータ構造を定義
}
例えば上記コードを実行すると、以下のような例外が発生します。

例外をよく見ると「Stack overflow」という文字があり、スタックオーバーフローの発生のため停止しました。前述したようにローカル変数はスタックメモリを使いますが、上記コードでは、1MBほどのローカル変数を確保してしまったためスタックメモリが不足したこととなります。
開発でよく見るスタックオーバーフローの例としては以下のものがあります。
- 大きなセーブデータをローカル変数にコピーし、スタックメモリを使い切る
- 再帰呼び出しを無限に行って、スタックフレームの積み重なりにより、スタックを使い切る
- スレッドに割り当てたスタックサイズが不足し、スタックオーバーフロー検出前にメモリを破壊する
最後の例は少し特殊ですが、マルチスレッドのプログラムでは、各スレッドに割り当てられるスタックメモリが十分でない場合、オーバーフロー検出が機能せず、隣接するメモリ領域を破壊しながら動作を続ける可能性があります。そのため、スレッド内で大量のローカル変数や再帰処理を行う場合は、スタックサイズの見積もりと管理に注意が必要です。
ヒープメモリに関する説明と用語
メモリ操作の柔軟性とメモリリーク
ヒープメモリは、プログラムの実行中に「自由に」サイズを指定してメモリ領域を確保できる柔軟性の高いメモリ管理方式です。必要なタイミングで必要なだけメモリを確保し、不要になったときに「自由に」開放することができます(C言語ならmalloc/free、C++ならnew/deleteなど)。
この「自由に」という特徴は、配列やオブジェクトのサイズが実行時に決まる場合や、長期間データを保持したい場合に非常に便利です。しかしその反面、メモリの確保と開放をプログラマー自身が責任を持って管理しなければならないため、
- メモリの開放を忘れる(メモリリーク)
- すでに開放したメモリを再び使ってしまう(ダングリングポインタ)
- 必要以上にメモリを確保する(メモリの無駄遣い)
などのバグが発生しやすいというデメリットもあります。
つまり、ヒープメモリは柔軟性と引き換えに、正確な管理が求められるメモリ領域であり、プログラマーのミスによるメモリリークなどのリスクが常につきまとう点に注意が必要です。
ガベージコレクションの仕組みと限界
別の言語の話となりますが、JavaやC#などの言語では、ガベージコレクション(GC)によってメモリリークの発生リスクを大きく減らすことができます。
GCは「プログラム中で参照されなくなったオブジェクト」を自動的に検出し、不要になったメモリ領域を自動で解放してくれます。
■メリット
- 明示的なメモリ解放が不要
プログラマーがfreeやdeleteを使って手動でメモリを解放する必要がありません。 - 多くのメモリリークを自動的に防止
参照がなくなったオブジェクトは自動的に回収されるため、解放漏れによる典型的なメモリリークが起きにくいです。
■限界・注意点
- 参照が残っていると解放されない
たとえば、使わなくなったオブジェクトでも、どこかの変数やコレクションから参照されている限り、「まだ使われている」とみなされ、GCによって解放されません。
→ これが「論理的なメモリリーク」と呼ばれる現象です。 - 不正なメモリアクセス(null参照など)は防げない
JavaやC#では、解放済みメモリへのアクセス(ダングリングポインタ)は起きませんが、null参照(NullPointerException)は発生し得ます。 - GCのタイミングは制御できない
いつメモリが解放されるかはGCに任されているため、メモリ使用量が一時的に増える場合もあります。
まとめとしては、
- ガベージコレクションは多くのメモリリークを防ぐが、「すべてのメモリリークを防ぐわけではない」
- 参照が残っている限り、不要なメモリが自動解放されない場合がある
- null参照などの不正なアクセスは、GCとは別の問題として注意が必要
→ GCは便利な仕組みですが、プログラマーの設計・実装次第で論理的なメモリリークやnull参照例外が発生するため、注意が必要です。
スマートポインタ
C++言語の話に戻ると、メモリリーク対策としてはスマートポインタを使うのが安全で一般的な方法です。
スマートポインタとは、「メモリの所有権」という概念に基づいて動的メモリを管理するクラスで、new や delete を明示的に記述する必要がなくなります。
プログラム上でオブジェクトが不要になったタイミングで、自動的にメモリが解放されるため、メモリリークやダングリングポインタのリスクを大きく減らせます。
スマートポインタの多くは、参照カウント(Reference Counting) という仕組みによって実装されています。これは、メモリが何箇所から参照されているかをカウントし、誰からも参照されなくなったときに自動的にメモリを解放する方式です。
使い方としては通常のnew /delete では以下のように記述します。
#include <iostream>
struct BigData {
char data[1024 * 1024]; // 1MBのデータ
};
int main()
{
// new / deleteを使用してBigDataのインスタンスを管理する場合
BigData* bitDataPtr = new BigData(); // BigDataのインスタンスを動的に確保
// 何らかの処理を行う...
delete bitDataPtr; // インスタンスを解放
bitDataPtr = nullptr; // ポインタをnullptrに設定してダングリングポインタを防ぐ
return 0;
}
スマートポインタを使うと以下のようになります。
#include <iostream>
#include <memory> // std::shared_ptrを使うために必要
struct BigData {
char data[1024 * 1024]; // 1MBのデータ
};
int main()
{
// std::shared_ptrを使用してBigDataのインスタンスを管理
std::shared_ptr<BigData> bigDataPtr = std::make_shared<BigData>();
// 何らかの処理を行う...
// 開放処理の記述は不要.
return 0;
}
この場合、bigDataPtr はローカル変数であり、main() 関数の終了とともに破棄されます。
その際にスマートポインタが自動的にメモリを解放してくれるため、delete
の記述が不要です。
他のスマートポインタ
C++ には、目的に応じていくつかの種類のスマートポインタがあります。
- std::shared_ptr<T>
→ 複数の所有者で共有するオブジェクトに使用します(→参照カウント方式。便利だけどやや重い) - std::unique_ptr<T>
→ 単一の所有者しか存在しないオブジェクトに使用します(→軽量で高速) - std::weak_ptr<T>
→ shared_ptr によって管理されているオブジェクトへの非所有参照の方式。循環参照を避けるために使用します
多くのケースでは、まずは shared_ptr<T> を使えば十分なことが多いですが、スマートポインタに慣れてきたら unique_ptr や weak_ptr の使い方も学んでおくと良いと思います。
おしまい
以上、C++言語におけるスタックメモリとヒープメモリについての違いをまとめてみました。
この情報がC++言語を勉強するのに役立てれば幸いです。