Menu

C#とC++の違い:メモリ、性能、ゲーム開発、学習の難しさ

C#はガベージコレクターを持つマネージド言語で、C++はネイティブコードにコンパイルされ、メモリを手動で制御できます。その違いが、安全性、性能、ゲームエンジン、ビルドの仕組み、それぞれの学びやすさにどう現れるかを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

C#とC++は文字と波かっこの構文を共有していますが、立ち位置が異なります。C++(1985年)はマシンコードに直接コンパイルされ、すべてのオブジェクトがどこに存在し、いつ消えるかを正確に制御できます。C#(2002年)は中間言語にコンパイルされ、.NETランタイムが実行時にそれをマシンコードに変換し、ガベージコレクターがメモリを回収してくれます。以下のほぼすべての違いは、この1つの選択から生じています。

ひと目でわかる比較

C#C++
実行マネージド:IL、CLRがJITコンパイルネイティブ:事前にマシンコードへコンパイル
メモリガベージコレクター手動、RAII、スマートポインター
ポインター参照。生のポインターは unsafe のコードのみ生のポインターと参照をどこでも
安全性境界チェック付きの配列、ダングリング参照なし範囲外アクセスや解放後の使用で未定義動作
ビルドの仕組みプロジェクトとアセンブリ、ヘッダーなしヘッダー、プリプロセッサー、翻訳単位、リンカー
ジェネリックジェネリック、一度だけ検査され、実行時に解決テンプレート、コンパイル時にインスタンス化
標準ライブラリ大きい:コレクション、HTTP、JSON、ファイル、スレッド小さい:コンテナー、アルゴリズム、スレッド
コンパイル速度速い大規模なコードベースでは遅い
ゲームUnity、GodotUnreal、ほとんどの社内AAAエンジン
その他の主な用途Webのバックエンド、デスクトップ、クラウド、ツールエンジン、ブラウザー、OS、組み込み、金融取引

メモリ:ガベージコレクターとRAII

C++では、自動記憶域を持つオブジェクトはスコープを抜けると破棄され、そのデストラクターがまさにその瞬間に実行されます。ヒープのオブジェクトはスマートポインター(std::unique_ptr、std::shared_ptr)が所有するか、new と delete で手動で管理します。このRAII(Resource Acquisition Is Initialization)というパターンにより、メモリやその他すべてのリソースが決定的に後片付けされます。

// C++
#include <memory>
#include <fstream>

void save() {
    std::ofstream file("log.txt");          // opened here
    auto buffer = std::make_unique<char[]>(4096);
    file << "saved\n";
}   // file closed and buffer freed here, in reverse order

C#では、すべてのクラスのインスタンスはマネージドヒープにあり、何からも参照されなくなった後のどこかの時点でガベージコレクターが解放します。delete を書くことはなく、使用中のものを解放してしまうこともありません。メモリは処理されますが、GCがすぐには処理しないのは、ファイル、ソケット、データベース接続などのその他のリソースです。そのためにC#には IDisposable と、ブロックの終わりで Dispose を呼ぶ using ステートメントがあり、これがデストラクターに最も近いものです。

出力:

open db
open cache
db <- SELECT 1
cache <- PING
close cache
close db
done

違いは、C++ではすべてのローカルオブジェクトとスマートポインターが所有するすべてのオブジェクトについて後片付けがスコープに結び付いているのに対し、C#ではメモリについては自動で、それ以外については(using によって)明示的に選ぶ、という点です。詳しくはusingステートメントのページを参照してください。

安全性:未定義動作の代わりに例外

C++で配列の末尾を越えて読むのは未定義動作です。プログラムはゴミを表示するかもしれないし、クラッシュするかもしれないし、壊れたメモリのまま動き続けるかもしれず、結果はビルドによって変わることもあります。C#で同じ間違いをすると、まさにその行で例外が投げられます。

// C++: compiles, and the behavior is undefined
int scores[3] = {90, 85, 77};
int x = scores[5];   // reads whatever is in memory there

出力:

Index 5 is outside an array of length 3
name was null

メモリ破壊のバグの一群(バッファーオーバーフロー、解放後の使用、二重解放、ダングリングポインター)は、安全なC#には存在しません。これが、C#のコードを速く書いてレビューできる大きな理由です。

ポインターとunsafeのコード

C#にもポインターはありますが、unsafe ブロックの中だけで、プロジェクトは <AllowUnsafeBlocks>true</AllowUnsafeBlocks> で明示的に有効にする必要があります。マネージドヒープのオブジェクトはガベージコレクションの間に移動することがあるので、アドレスを取る前に fixed で固定します。

// C#, requires AllowUnsafeBlocks
unsafe
{
    int[] data = { 1, 2, 3 };
    fixed (int* p = data)
    {
        *(p + 1) = 20;   // data is now { 1, 20, 3 }
    }
}

unsafeのコードは、ネイティブライブラリとの相互運用や、いくつかの性能が重要なループで使われます。今日の低レベルなC#のほとんどは、代わりに Span<T>、ref ローカル、stackalloc を使います。これらは境界チェックを保ちながら、ポインターに近い性能を出せます。

性能

C++はプログラム全体を事前にコンパイラに渡し、実行時には何も加えません。ガベージコレクターもJITもなく、求めない限り境界チェックもありません。そのため、1マイクロ秒や1バイトが重要で、停止が許されない場面で選ばれます。

C#は安全性の代償として、ランタイム、起動時のJITのウォームアップ、時々のGCの停止を払います。スループットでは通常C++からわずかな倍率の範囲にあり、.NETのリリースのたびに差は縮まってきました。プロファイルに基づく最適化を行う階層型JIT、割り当てを避ける構造体と Span<T>、SIMDのためのハードウェア組み込み関数、そして事前にコンパイルして単一のネイティブ実行ファイルにするNative AOTです。Web API、ツール、ビジネスロジックでは、データベースとネットワークが支配的で、言語の差が表に出ることはまれです。

ゲーム開発:UnityとUnreal

多くの人がこの問いに出会うのはここです。UnityはC#でスクリプトを書きます。ゲームプレイのコード、UI、ツールはゲームオブジェクトに付けるC#のクラスで、エンジンの中核はC++です。Unreal EngineはC++で書かれていて、ゲームプレイはC++とビジュアルスクリプトのBlueprintsで作ります。GodotはGDScriptとC#の両方をサポートしています。

UnityとC#は学びやすく試行錯誤もしやすいので、インディーやモバイルのゲームで非常によく使われています。UnrealとC++はAAAのスタジオで標準であり、どこでもエンジンのプログラマーはC++で仕事をしています。よくある道筋は、Unityから始め、Unrealやエンジンの仕事に移るときにC++を学ぶことです。

ビルドの仕組み

C++のプログラムは、ヘッダー(.h、宣言)とソースファイル(.cpp、定義)に分かれています。プリプロセッサーがヘッダーを各ソースファイルに貼り付け、各ファイルは別々にコンパイルされ、リンカーが結果をつなぎます。テンプレートはそれを使うすべてのファイルでインスタンス化され、これが大規模なC++のビルドが遅い理由の1つです。

C#にはそのどれもありません。プロジェクトは、まとめてアセンブリ(.dll)にコンパイルされる .cs ファイルの集まりです。宣言の順序もファイルの順序も関係なく、あるファイルの型はインクルードなしで別のファイルの型を使えます。ライブラリはNuGetパッケージとして提供されます。using ディレクティブがインポートするのは、ファイルではなく名前空間です。

気づく構文の違い

  • オブジェクト。 C++では auto p = std::make_unique<Player>(); と p->Jump();、C#では var p = new Player(); と p.Jump(); です。C#はすべてに . を使います。
  • 文字列。 std::string は変更可能な値で、C#の string は不変の参照型です。
  • 多重継承。 C++ではクラスが複数のクラスを継承できます。C#で継承できる基底クラスは1つで、それに加えて任意の数のインターフェースを実装できます。
  • テンプレートとジェネリック。 C++のテンプレートはコンパイル時のコード生成で、メタプログラミングもできます。C#のジェネリックは、明示的な制約(where T : IComparable<T>)とともに一度だけ型検査されます。
  • 標準ライブラリ。 C#の標準ライブラリにはHTTP、JSON、正規表現、ファイルI/O、圧縮、暗号化が含まれます。C++では、その多くをサードパーティのライブラリから得ます。

どちらを学ぶか

アプリケーション、Webのバックエンド、ツール、Unityのゲームを作り、すぐに成果を見たいならC#を学びます。ゲームエンジン、グラフィックス、組み込みシステム、オペレーティングシステム、ブラウザーなど、ハードウェアレベルの制御と予測可能な遅延が肝心なものを目指すならC++を学びます。C#は最初の言語としてより穏やかで、C++は2番目に学ぶ価値があります。C#のランタイムが代わりに何をしてくれているかを教えてくれるからです。

よくある質問

C#とC++の主な違いは何ですか?

C#はマネージドです。.NETランタイムがJITコンパイルする中間言語にコンパイルされ、ガベージコレクターがメモリを解放します。C++は直接マシンコードにコンパイルされ、オブジェクトをいつ作り、いつ破棄するかを自分で制御します。この違いにより、C++は制御性と予測可能性を、C#は安全性と開発の速さを得ています。

C#はC++より簡単ですか?

ほとんどの人にとってはそうです。C#には手動のメモリ管理も、ヘッダーファイルも、安全なコードにおける未定義動作もなく、コンパイラのエラーもわかりやすいです。C++はより大きな言語で、ダングリングポインター、バッファーオーバーフロー、解放後の使用など、コンパイラが検出しない微妙な間違いをする方法が多くあります。

C++はC#より速いですか?

よく書かれたC++は通常より速く、さらに重要なことに、ガベージコレクターの停止もJITのウォームアップもないので予測しやすいです。現代のC#は、構造体、Span<T>、SIMD、事前コンパイルで差を縮めていて、業務アプリケーションではこの差がボトルネックになることはまれです。エンジン、ドライバー、高頻度取引では、C++が今も標準です。

ゲーム開発にはC#とC++のどちらを学ぶべきですか?

最初のゲームなら、Unity(またはGodot)とC#のほうが速く作って公開できます。Unreal EngineはC++(とBlueprints)を使い、エンジンのコードを書くAAAのスタジオはC++のプログラマーを採用します。多くの開発者はUnityとC#から始め、エンジンレベルの仕事が必要になったときにC++を学びます。

C#にポインターはありますか?

あります。ただし unsafe のコードブロックの中だけで、プロジェクトの AllowUnsafeBlocks の設定で有効にする必要があります。通常のC#は参照を使い、参照はガベージコレクターが追跡し、解放されたメモリを指すことはありません。ポインターを使いたくなるほとんどの場面は、ref、Span<T>、stackalloc でまかなえます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める