オブジェクト指向プログラミング (OOP) を 内部の動き から理解する
実行前(設計)と実行後(メモリ上でのライブ挙動)を分けて追跡する解説です。C#/.NETを想定した説明ですが、JVM系や他のOOP言語でも共通する概念が多くあります。
0. ゴール
- 「クラス=設計図」「オブジェクト=実体」というお約束説明で終わらない。
- プログラムが実際に走り、メモリにインスタンスが載り、メソッド呼び出しがどのように解決されるかをイメージする。
- 「実行前に存在するもの」と「実行して初めて現れるもの」を区別する。
1. 実行前に存在する“静的世界” (型メタデータの段階)
レイヤ | 実体 | どこにあるか | いつ作られるか | 何を保持するか |
---|---|---|---|---|
ソースコード | class Player { int hp; void Attack() { … } } | .csファイル | 開発時 | 宣言(意図) |
中間言語 + メタデータ | IL, 型定義テーブル | アセンブリ(DLL/EXE) | ビルド時 | フィールド型、メソッド署名、アクセス修飾子など |
ランタイム型情報 | System.Type, メタデータ構造 | CLRロード領域 | ランタイムで初回使用時 | リフレクション情報、レイアウト、vtable等の解決ベース |
ポイント: クラス定義は「まだメモリ上に あなたが操作するPlayerの個体 は存在しない」。あるのは 型の説明書(=メタデータ)だけ。
2. 実行で何が変わる?— new の流れを分解
var p = new Player(); が走ると、概ね以下の工程(概念モデル)を辿ります。
- 型ロード確認CLRが Player 型のメタデータを既にロード済みか確認。未ロードならアセンブリから読み込む。型情報テーブルを内部に展開。
- メモリサイズ決定フィールドの型(例: int hp, string name など)からインスタンスサイズが決まる。アライメント・ランタイムヘッダ分を含む。
- ヒープ確保管理ヒープ(GCヒープ)上に必要バイト数の連続領域を確保。先頭にオブジェクトヘッダ(型ポインタ、GC用情報など)が付く。
- フィールド初期化(ゼロクリア → 既定値設定)まずゼロ初期化。その後、フィールドのデフォルト値や初期化子があれば適用。
- コンストラクタ呼び出し.ctor が実行され、フィールドへ値代入・イベント購読など、開発者記述の初期化コードが走る。
- 参照が返るローカル変数 p に「ヒープ上オブジェクトへの参照値」(アドレスに相当)が格納される。参照型なら変数は“ポインタ的ハンドル”。
3. メモリに展開されたオブジェクトの概形(概念図)
[ ローカル変数 p ] ----> [オブジェクトヘッダ | 型ポインタ | フィールド領域... ]
| +-- hp : int
| +-- name : string参照
+-- vtable間接(言語/CLR実装で差)

- オブジェクトヘッダ: GC管理用フラグ、同期ブロックインデックスなど。
- 型ポインタ: 「この実体は Player 型だ」とランタイムに伝える。動的ディスパッチ時の手掛かり。
- フィールド領域: 実際のデータ。値型は埋め込み、参照型フィールドはさらに他オブジェクトを指す参照。
4. メソッド呼び出しが内部的にどう解決されるか
4.1 非仮想 (non-virtual) メソッド
コンパイラが静的に解決。呼び先が明確。ILのcall命令で直接ターゲットを呼ぶ。
4.2 仮想 (virtual) メソッド
動的ディスパッチ。実インスタンスの型に応じて実行時に呼び先が決まる。
概念フロー:
- p.Attack() 呼び出し。
- JIT生成コードが p の型ポインタをたどる。
- 型ごとの仮想メソッドテーブル(vtable) 参照。
- 「Attack」スロットに登録されたメソッド実装アドレス呼び出し。
派生型 BossPlayer : Player が override Attack() すると、そのスロットに別アドレス。
5. 継承時のメモリレイアウト (単純化)
class Character { int hp; }
class Player : Character { int exp; }
new Player()
GCヒープ:
[ヘッダ][型=Player][hp][exp]
- 基底クラス分のフィールドが前方にレイアウトされる(言語/実装依存だが概念として)。
- キャスト Character c = p; のとき、参照アドレスは同じだが「見えるメンバ」が基底型に制限されるのはコンパイル時の静的型制約。メモリ自体は1塊。
6. 参照型 vs 値型 (C#特有の重要ポイント)
項目 | 参照型(class) | 値型(struct) |
---|---|---|
メモリ配置 | 通常GCヒープ | 場所に埋め込まれる(スタック/配列/他オブジェクト内) |
変数代入 | 参照コピー | 値コピー |
null | 可 | 不可(Nullable除く) |
ボクシング | 該当なし | object等にアップキャスト時、ヒープに箱詰め |
内部動き上の注意: 値型をメソッド引数で渡すとコピー(既定は値渡し)。大きいstructはコスト増。参照型はポインタ1本コピーで済む。
7. ガーベジコレクション (GC) とオブジェクト寿命
- ルート参照(スタック上のローカル、静的フィールド、CPUレジスタなど)からたどれるオブジェクトを「生存」とマーク。
- 到達不能なオブジェクト領域は回収対象。
- 世代別GC(Gen0/1/2)で短命オブジェクトを効率回収。
- コンパクションでメモリを詰め、参照更新(内部的にポインタ書き換え)を行うこともある。
実行中オブジェクトのアドレスがGCで動く可能性があるので、固定が必要な場合はpin。
8. アクセス修飾子はどう「効いている」のか?
private, public, protected などは基本的にコンパイル時チェック。
ランタイムは「やろうと思えばリフレクションで書き換え可能」なのが現実。
つまり、言語レベルのカプセル化は“開発者間の契約 + コンパイラ診断”が中心で、OSレベル権限制御のような強制とは異なる。
9. インターフェイス呼び出しの内部 (ざっくり)
インターフェイスは多重継承的契約を提供するが、実体はクラス。
実行時には「オブジェクトの型がこのインターフェイスを実装しているか」をマッピングするテーブルがあり、呼び出しはそのテーブルを通じて実装メソッドに解決される(JITが最適化して直接化する場合もある)。
10. ジェネリック型の内部
C#/.NETのジェネリックはリフレクション可能な再ified generics(型引数情報が実行時も残る)で、List<int>と List<string> はJIT時にそれぞれ最適化されたコードパスを得られる場合がある(値型最適化など)。
Javaの型消去(erasure)系とは内部挙動が異なる重要ポイント。
11. 実行前後を可視化して学ぶ(授業向けミニ手順)
11.1 手順概要
- WinFormsでボタン [生成] を押すまで何もない(インスタンス未生成)状態をUIで表示。
- ボタンクリックイベント内で new Player() を行い、生成直後のフィールド値をラベルに表示。
- 別ボタン [Attack] でメソッド実行し、インスタンスメソッド呼び出し=「内部でthis参照経由」動作を可視化。
- Player → BossPlayer に派生して仮想メソッド上書き、ボタンで型差を観察。
11.2 可視化アイディア
- 左パネル: 「型(設計図)情報」静的表示。
- 右パネル: 実行時「生成済みインスタンス一覧」グリッド。GC世代は疑似表示でOK。
- ログウィンドウ: Alloc Player(size=…) -> ref 0x1234 のような疑似メモリログ。
12. 「内部の動き」まとめフローチャート
ソース -> コンパイル -> アセンブリ(IL+メタデータ)
↓ 実行開始
CLRが型読み込み(Typeロード)
↓ new
[GCヒープ確保] [ゼロ初期化]
↓ .ctor呼び出し
参照を変数に格納
↓ メソッド呼び出し (静的 or 仮想ディスパッチ)
↓ 到達不能 → GC回収
13. よくある誤解と内部視点での修正
誤解 | 内部視点でどう正す? |
---|---|
「クラスはオブジェクトです」 | いいえ。クラスは“まだ作っていないオブジェクトの型情報”。実インスタンスは newして初めてメモリ上に確保される。 |
「変数にオブジェクトが入る」 | 参照型では変数に入るのは参照値(アドレス相当)。実体はヒープ。 |
「privateなら絶対アクセス不可」 | コンパイル時保護。リフレクション等で突破可能。設計上の契約と考える。 |
「GCだからメモリは無限」 | 到達不能にならなければ解放されない。大きな配列参照を保持すると回収されない。 |
14. 学習用ミニ実験コード(概念スケッチ)
public class Player
{
public int Hp;
public string Name;
public Player(string name, int hp = 100)
{
Name = name;
Hp = hp;
Console.WriteLine($"[ALLOC Player] this={GetHashCode()} Hp={Hp}");
}
public virtual void Attack()
{
Console.WriteLine($"{Name} attacks! (virtual)");
}
}
public class BossPlayer : Player
{
public BossPlayer(string name, int hp = 500) : base(name, hp) {}
public override void Attack()
{
Console.WriteLine($"{Name} unleashes BOSS attack!! (override)");
}
}
テスト:
Player p = new Player("Hero");
Player b = new BossPlayer("Demon King");
p.Attack(); // Player.Attack
b.Attack(); // BossPlayer.Attack (仮想ディスパッチ)
Player up = b;
up.Attack(); // 依然としてBossPlayer.Attack(実体に基づく)
ここで up の静的型は Player だが、実体は BossPlayer。内部では型ポインタを参照してvtableスロットを解決するため、オーバーライド版が呼ばれる。
15. 「内部動き」視点で授業に盛り込むチェック問題
Q1. class Enemy { } を定義しただけでは、メモリにEnemyは存在しますか?
Q2. var e1 = new Enemy(); var e2 = e1; の後、e1 のフィールドを書き換えると e2 に影響するのはなぜ?
Q3. virtual/override により実行時にメソッドが切り替わる仕組みを一言で言うと?
Q4. GCに回収される条件は?「参照到達性」の観点で。
Q5. struct と class をサイズの大きな配列で使ったとき、コピーコストで何が変わる?
番号 | 問題 | 解答 |
---|---|---|
Q1 | class Enemy { } を定義しただけでは、メモリに Enemy は存在しますか? | 存在しません。 まだインスタンスは生成されておらず、あるのはアセンブリに格納された「型メタデータ」だけです。実体が確保されるのは new Enemy() が実行された瞬間です。 |
Q2 | var e1 = new Enemy(); var e2 = e1;のあと、e1 のフィールドを書き換えると e2 に影響するのはなぜ? | e1 と e2 は 同じヒープ上のオブジェクトを指す参照値 をコピーしただけだからです。参照型の変数同士はアドレス(ハンドル)を共有するため、片方を通じた変更はもう一方からも見えます。 |
Q3 | virtual / override により実行時にメソッドが切り替わる仕組みを一言で言うと? | 動的ディスパッチ(仮想メソッドテーブル = vtable によるルックアップ) です。呼び出し時に実体型の vtable を参照して正しい実装アドレスへジャンプします。 |
Q4 | GC に回収される条件は?「参照到達性」の観点で。 | いずれの GC ルート(スタック、静的フィールド、CPU レジスタなど)からも参照されなくなったとき に「到達不能」と判定され、回収対象になります。 |
Q5 | struct と class をサイズの大きな配列で使ったとき、コピーコストで何が変わる? | – class 配列: 要素代入・引数渡しではポインタ(参照)1本のコピーだけなので軽量。- struct 配列: 各要素の実体データを丸ごとコピーするため、サイズが大きいほどコストが高く、パフォーマンスに影響しやすい。 |
16. 次に進むなら
- ILDASM / ILSpy でILレベルを覗いて「call」「callvirt」の違いを見る。
- SOS/WinDbgやdotnet-dumpでヒープ状態を観察。
- BenchmarkDotNetでstruct vs classのパフォーマンス差を計測。
- Unity環境なら new のタイミング・GCスパイク(フレーム落ち)を実測し、「内部動きがゲーム性能に直結」する体験に繋げる。
まとめ(40秒版)
クラスは「まだ存在しないオブジェクトの設計情報」。new が走るとランタイムが型情報をもとにヒープ領域を確保し、初期化して参照を返す。メソッド呼び出しでは静的/仮想/インターフェイスで解決経路が異なり、オーバーライドは実体型を参照して決まる。寿命は参照到達性で管理され、到達不能になればGCが回収する。— この「実行前後の落差」がOOP理解のカギです。
ディスカッション
コメント一覧
まだ、コメントがありません