はじめに
プログラムが動くとき、CPU はメモリの中の「どの場所に何を置くか」を決めています。
その中でも特に重要なのが スタック領域 と ヒープ領域。
関数を使ったプログラム(C やアセンブラ)では、
「一時的に使うもの」はスタック、「長く残すもの」はヒープに置く、という住み分けがあります。
- スタック … CPUが関数の呼び出しを正しく行うための一時的な作業机
- ヒープ … プログラムが自由に使える長期保管スペース
スタック(stack)
関数を呼び出すとき、CPUは「戻り先アドレス」「関数の引数」「ローカル変数」などを一時的に記録します。
それらをまとめて置く場所がスタックです。
スタックが必要な理由(追記)
関数を呼び出すたびに「どこから来たのか」「何を持っていたのか」を記録しないと、
終わったあとに元の場所へ戻れません。
この「一時的な記録帳」こそがスタックです。
C では自動でやってくれますが、アセンブラでは自分でやる必要があります。
だから、スタックを理解することは「Cが裏でやっていることを理解する」ことでもあります。
アセンブラとのつながりを補足
Cで
func();
と書くだけで関数を呼べるのは、
コンパイラが自動的に「BSR(呼び出し)」「RTS(復帰)」「PUSH/POP(退避)」を生成してくれているから。
つまり、
- C言語 … スタック操作を「隠蔽」してくれる高級言語
- アセンブラ … すべて自分の手でやる低級言語
この違いが、Cとアセンブラの根本的な差です。
🔹 サブルーチン呼び出しの全体図
呼び出し側(main):
BSR sub_func ; サブルーチンへ(戻り先を SP が指す位置に PUSH)
↓
[スタック] ← 戻り先アドレスを自動で積む
↓
sub_func:
PUSH R1-R3 ; 使用するレジスタを一時退避
...
POP R1-R3 ; 戻す
RTS ; スタックから戻り先を取り出して帰る
つまりアセンブラの関数呼び出しは「3段構成」
- 戻り先をスタックに保存(BSR)
- サブルーチンで必要なレジスタを退避(PUSH)
- 終了時に復帰(RTS + POP)
これが「Cが裏でやってくれること」の正体です。
C言語とアセンブラの関係図
概念 | C言語での記述 | アセンブラでの動作 | 説明 |
---|---|---|---|
関数呼び出し | func(); |
BSR func |
戻り先をスタックに積む |
関数終了 | return; |
RTS |
スタックから戻り先を復帰 |
ローカル変数 | int x; |
SUB #size, SP など |
スタックに一時領域を確保 |
関数引数 | func(a, b) |
スタック経由またはレジスタ渡し | 呼び出し時にコピーされる |
保存レジスタ | ― | PUSH/POP |
関数内で壊さないために退避 |
ヒープ(heap)
Cでは malloc
などで自分で確保・解放する領域です。
スタックとは異なり、関数を抜けてもメモリが残るのが特徴です。
ヒープに確保されたデータは関数の外でも有効で、複数の関数から共有することもできます。
なぜヒープが必要か
スタックは「関数が終われば自動的に消える一時的な領域」なので、
関数の外でも使いたいデータを置くことができません。
たとえば、配列を関数内で作って返したいとき:
int* makeArray(int n) {
int ary[10]; // スタックに確保 → 関数を抜けると消える
return ary; // ❌ 無効なアドレスを返すことになる
}
これを避けるために、ヒープに確保します。
int* makeArray(int n) {
int *ary = malloc(n * sizeof(int)); // ヒープに確保
return ary; // ✅ 関数を抜けても残る
}
オブジェクト指向言語とヒープ
C やアセンブラでは、
「どのメモリに置くか」を自分で指定しなければなりません。
しかし、**オブジェクト指向言語(Java, C#, Python, PHP など)**では
メモリ管理を「オブジェクト単位」で自動化しています。
オブジェクト指向言語では、変数に実体そのものではなく「参照(アドレスの安全版)」が入る。
すべてのオブジェクトはヒープに置かれる
たとえば:
int[] ary = new int[5];
Person p = new Person();
このとき、
-
ary
やp
自体はヒープ上に作られ、 - 変数
ary
やp
は「ヒープ上の実体を指す参照(reference)」を持っています。
つまり:
C のポインタとほぼ同じ仕組みを、安全に・自動で扱っているのがオブジェクト指向言語。
GC(ガーベジコレクション)が登場する理由
Cでは、メモリの確保と解放をすべて自分で書く必要がありました。
→ その煩雑さをなくすために登場したのがGCです。
Cのように free()
を忘れるとメモリが残り続ける(リーク)問題が起こります。
オブジェクト指向言語ではこの煩雑さをなくすために、
使われなくなったオブジェクトを自動で検出・解放する仕組み(GC)が導入されました。
言語 | ヒープ確保 | 解放の仕組み |
---|---|---|
C | malloc() |
free() 手動 |
C++ | new |
delete 手動(スマートポインタで補助あり) |
Java / C# | new |
自動(GC) |
Python / PHP | オブジェクト生成 | 自動(参照カウント + GC) |
まとめ:C と オブジェクト指向言語の違い
観点 | C言語 | オブジェクト指向言語 |
---|---|---|
メモリ確保 |
malloc / free 手動 |
new / 自動 |
メモリ位置 | スタック or ヒープ | 基本的にヒープ |
データの扱い | 値そのものを操作 | 参照(ポインタ安全版)を操作 |
管理方式 | プログラマが管理 | ガーベジコレクタが管理 |
メモリ寿命 | 関数内で終わる | 参照がなくなるまで残る |
補足:アセンブラとの関係
- アセンブラ:メモリの確保も解放もすべて自分で命令を書く(物理アドレス単位)
- C:
malloc/free
で論理的に制御 - オブジェクト指向言語:
new
で論理的に生成、GCが自動解放
つまり、
アセンブラ → C → オブジェクト指向
と進むにつれて、「メモリ操作をどんどん自動化」していった流れになります。
全体のつながり(図でイメージ)
下図は、同じ「関数・メモリ管理」という仕組みが、言語の層ごとにどのように実装されているかを示したものです。
┌───────────────┐
│ 高級言語層 │ ← new, malloc, 関数呼び出し(自動)
├───────────────┤
│ Cコンパイラ出力 │ ← スタック操作命令(PUSH/POP, CALL/RET)
├───────────────┤
│ アセンブラ層 │ ← 実際のメモリ操作(ISP, SP, MOV, BSR, RTS)
├───────────────┤
│ CPU・RAM │ ← スタック領域, ヒープ領域が実体化
└───────────────┘