C++ の経験があれば、ポインターそのものは馴染み深い概念のはずです。C# では通常 .NET ランタイムのガベージコレクター(GC)がメモリを管理しますが、unsafe キーワードを使うことで C/C++ 同様にポインターを操作できます。
このシリーズは全 4 記事で構成されています。
- ① 本記事 — なぜポインターが必要か / unsafe の基本 /
*と&演算子 - ② —
->演算子 /stackalloc/fixed/sizeof - ③ 実践例:ビットマップ画像処理
- ④ 実践例:C++ DLL との相互運用
なぜ C# でポインターが必要か?
C# はマネージド言語です。.NET ランタイムが GC を通じてメモリの確保と解放を自動的に行うため、C/C++ のように malloc/free を手動で呼ぶ必要はありません。通常のアプリケーション開発では、ポインターを使う機会はほとんどありません。
では、なぜポインターが用意されているのでしょうか。主に次の 2 つの場面で必要になります。
(1) .NET ランタイムの管理外でメモリを直接操作してパフォーマンスを最大化したい
GC が管理するヒープを介さずにメモリを直接操作することで、特定の処理を大幅に高速化できます。典型的な例は画像処理です。ビットマップ画像を 1 ピクセルずつ処理する場合、安全な API(GetPixel / SetPixel)は 1 回ごとにオーバーヘッドが発生しますが、ポインターで生のバイト列に直接アクセスすれば数十倍速くなることがあります。
実際のコード例は ③ 実践例:ビットマップ画像処理 で紹介します。
(2) ポインター型を引数に要求する C ベースの DLL や COM サーバーのメソッドを呼び出したい
Windows API や既存の C/C++ ライブラリは、多くの場合ポインターを引数として受け取ります。P/Invoke(Platform Invocation Services)でそのような関数を呼び出すとき、C# 側でポインターを扱う必要が生じます。
ただし、この場合は System.IntPtr 型や System.Runtime.InteropServices.Marshal クラスを活用すれば、unsafe コードを使わずに済むことも多くあります。両方のアプローチの比較は ④ 実践例:C++ DLL との相互運用 で紹介します。
マネージドコードを基本に
C# の設計思想は「安全なコードを書きやすく」することにあります。ポインターは強力ですが、GC との干渉、メモリ破壊のリスク、デバッグの難しさを伴います。Span<T>、ref 引数、Memory<T> など、多くのケースでポインターなしに同等のパフォーマンスを達成できる手段が .NET に用意されています。ポインターはどうしても必要な場面に絞って使うのが原則です。
この記事(①)の内容
- ポインター専用の演算子とキーワード一覧
- unsafe コードの有効化
unsafeキーワード*と&演算子の使い方- unsafe なスワップ関数と安全版との比較
ポインター専用の演算子とキーワード一覧
C# でポインターを扱うために用意された演算子とキーワードを整理します。
| 記号 / キーワード | 意味 |
|---|---|
* |
間接参照(デリファレンス)またはポインター型の宣言 |
& |
アドレス取得(変数のアドレスをポインターに格納) |
-> |
ポインター経由でフィールドやプロパティにアクセス |
[] |
ポインターの添字アクセス(*(ptr + i) と等価) |
++, -- |
ポインターのインクリメント・デクリメント(型サイズ単位で移動) |
+, - |
ポインター演算(バイト数ではなく要素数単位) |
==, !=, <, >, <=, >= |
ポインターのアドレス比較 |
stackalloc |
スタック上にメモリを確保する |
fixed |
GC によるオブジェクトの移動を一時的に禁止してポインターを固定する |
C++ を知っている方には見慣れた記号ばかりですが、C# では unsafe ブロックの中でのみ使用できます。
unsafe コードの有効化
Visual Studio での設定
C# プロジェクトで unsafe コードを使うには、プロジェクトの設定でオプションを有効にする必要があります。
Visual Studio の場合:
- ソリューションエクスプローラーでプロジェクトを右クリック
- 「プロパティ」 を開く
- 「ビルド」 タブを選択
- 「アンセーフ コードを許可する(Allow unsafe code)」 にチェックを入れる
.csproj ファイルでの設定
.csproj ファイルを直接編集する場合は、<AllowUnsafeBlocks> を true にします。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
この設定なしで unsafe キーワードを使うとコンパイルエラーになります。
unsafe キーワード
unsafe はメソッド、クラス、ブロックの 3 つの単位で指定できます。
メソッド全体を unsafe にする
// メソッド全体を unsafe にする
static unsafe void ShowAddresses()
{
int x = 10;
int* pX = &x;
Console.WriteLine($"x の値: {x}");
Console.WriteLine($"x のアドレス: 0x{(long)pX:X}");
}
ブロック単位で unsafe にする
static void Main()
{
int value = 42;
// このブロック内だけ unsafe
unsafe
{
int* p = &value;
Console.WriteLine($"value = {*p}"); // 42
}
// ここでは p は使えない
}
クラス全体を unsafe にする
// クラス全体を unsafe にする(ポインター操作が多い場合)
unsafe class LowLevelProcessor
{
public void Process(int* data, int length)
{
for (int i = 0; i < length; i++)
data[i] *= 2;
}
}
ポインター操作が必要な範囲を最小限に絞るのが安全です。クラス全体を unsafe にするより、メソッドやブロック単位で指定するほうが推奨されます。
* と & 演算子の使い方
C++ と基本的に同じです。& でアドレスを取得し、* でデリファレンスします。
unsafe static void PointerBasics()
{
// int 変数の宣言
int value = 100;
// & でアドレスを取得し、int* 型のポインターに格納
int* p = &value;
Console.WriteLine($"value = {value}"); // 100
Console.WriteLine($"*p = {*p}"); // 100
Console.WriteLine($"&value = 0x{(long)p:X}"); // アドレス(実行ごとに変わる)
// * で値を書き換える
*p = 999;
Console.WriteLine($"value = {value}"); // 999(p 経由で変更された)
}
ポインターへのポインター
C++ と同様に、ポインターへのポインターも扱えます。
unsafe static void DoublePointer()
{
int x = 5;
int* p = &x;
int** pp = &p;
Console.WriteLine($"x = {x}"); // 5
Console.WriteLine($"*p = {*p}"); // 5
Console.WriteLine($"**pp = {**pp}"); // 5
**pp = 77;
Console.WriteLine($"x = {x}"); // 77
}
ポインター演算
ポインターの ++ / -- や + / - は、C++ 同様に型のサイズ単位で移動します。
unsafe static void PointerArithmetic()
{
int[] numbers = { 10, 20, 30, 40, 50 };
fixed (int* start = numbers) // fixed については後編で詳しく説明
{
int* p = start;
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"*(start + {i}) = {*(start + i)}");
// 添字アクセスも使える: start[i]
}
}
}
// *(start + 0) = 10
// *(start + 1) = 20
// *(start + 2) = 30
// *(start + 3) = 40
// *(start + 4) = 50
アドレス比較
ポインター同士のアドレスを ==、!=、<、> などで比較できます。
unsafe static void PointerComparison()
{
int[] arr = { 1, 2, 3 };
fixed (int* p = arr)
{
int* first = p;
int* last = p + arr.Length - 1;
Console.WriteLine(first < last); // True
Console.WriteLine(first == p); // True
}
}
unsafe なスワップ関数と安全版との比較
ポインターを使った swap は C のコードと全く同じ構造です。
unsafe 版
unsafe static void SwapUnsafe(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int x = 10, y = 20;
unsafe
{
SwapUnsafe(&x, &y);
}
Console.WriteLine($"x = {x}, y = {y}"); // x = 20, y = 10
安全版(ref を使う)
同じことは C# では ref キーワードで unsafe なしに実現できます。
static void SwapSafe(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10, y = 20;
SwapSafe(ref x, ref y);
Console.WriteLine($"x = {x}, y = {y}"); // x = 20, y = 10
ref を使うほうが安全で、GC との競合も起きません。ポインターは ref や Span<T> で代替できないケースに限定するのが C# での推奨スタイルです。
まとめ
| トピック | ポイント |
|---|---|
| 有効化 | .csproj の <AllowUnsafeBlocks>true</AllowUnsafeBlocks> が必要 |
unsafe キーワード |
メソッド・ブロック・クラスの単位で指定できる |
* と & |
C/C++ と同じ。& でアドレス取得、* でデリファレンス |
| ポインター演算 | 型サイズ単位で移動。添字アクセス ptr[i] も使える |
| unsafe スワップ | C と同じ構造で書ける。C# では ref で代替可能 |
② ポインター型入門 では -> 演算子によるフィールドアクセス、スタックへのメモリ確保(stackalloc)、GC によるオブジェクト移動の抑制(fixed)、型のサイズ確認(sizeof)を解説します。