C#のポインタとは?unsafe・fixedの使い方と必要な場面を初心者向けに解説
はじめに
C#は、メモリ管理を自動で行ってくれる安全性の高いプログラミング言語です。そのため、普段のアプリ開発ではC#のポインタを意識する場面はほとんどありません。
しかし、C#にもポインタを扱う機能は用意されています。CやC++のようにメモリ上のアドレスを直接扱うことで、ネイティブAPIとの連携や高速なメモリアクセスが必要な処理に対応できます。
一方で、C#のポインタは通常のコードよりも危険性が高く、使い方を誤るとメモリ破壊や予期しないクラッシュにつながる可能性があります。そのため、初心者がいきなり多用するものではなく、「どのような場面で必要になるのか」「unsafeやfixedは何のために使うのか」を理解したうえで扱うことが大切です。
この記事では、C#のポインタについて、基本的な考え方からunsafe、fixedの使い方、必要になる場面、代替手段まで初心者向けに解説します。
1. C#のポインタとは?
C#のポインタとは、メモリ上のアドレスを直接扱うための仕組みです。通常のC#コードでは変数やオブジェクトを安全に扱えるように言語やランタイムが管理してくれますが、ポインタを使うと、その管理の一部を開発者が直接扱うことになります。
1-1. ポインタはメモリ上のアドレスを扱う仕組み
プログラムで使う変数やオブジェクトは、コンピューターのメモリ上に配置されます。ポインタは、そのメモリ上の場所、つまりアドレスを保持する変数です。
たとえば、int型の変数がメモリ上のどこかに保存されているとします。通常のC#では、その場所を直接意識せずに変数名で値を読み書きします。
C#int number = 10;
Console.WriteLine(number);
一方、ポインタを使うと「numberが置かれているメモリ上の場所」を取得し、そのアドレスを経由して値を読み書きできます。
C#unsafe
{
int number = 10;
int* p = &number;
Console.WriteLine(*p);
}
この例では、pがnumberのアドレスを指すポインタです。*pと書くことで、そのアドレスにある値を参照できます。
1-2. C#では通常ポインタを使わなくても開発できる
C#では、通常ポインタを使わなくても十分に開発できます。
Webアプリ、デスクトップアプリ、業務システム、ゲーム開発、API開発など、多くの場面ではクラス、構造体、配列、List、Dictionary、参照型、ref、out、Span<T>などを使えば目的を達成できます。
C#はマネージド言語であり、メモリ確保や解放の多くをガベージコレクションが自動で行います。そのため、CやC++のようにメモリのアドレスを直接扱う必要は基本的にありません。
ポインタが必要になるのは、ネイティブコードとの連携や、高速なメモリアクセスが求められる一部の特殊な場面です。
1-3. C/C++のポインタとの違い
CやC++では、ポインタは非常に基本的な機能です。配列操作、文字列操作、動的メモリ確保、関数への値渡しなど、さまざまな場面でポインタが使われます。
一方、C#ではポインタは通常の機能ではなく、unsafeという特別な宣言をしたコード内でのみ使用できます。
C#のポインタには、主に次のような特徴があります。
C#unsafe
{
int value = 100;
int* pointer = &value;
}
C#では、ポインタを使うコードが「安全ではない可能性があるコード」として明示的に区別されます。これは、C#が本来持っている型安全性やメモリ安全性を一部外れる操作だからです。
C/C++ではポインタを使うことが一般的ですが、C#では例外的な機能と考えると理解しやすいです。
1-4. 参照型・ref・outとの違い
C#には、ポインタに似て見える仕組みとして、参照型、ref、outがあります。しかし、これらはポインタとは異なります。
参照型は、クラスのインスタンスなどを扱う仕組みです。
C#class Person
{
public string Name { get; set; }
}
Person p = new Person();
p.Name = "Taro";
参照型の変数はオブジェクトを参照しますが、開発者がメモリアドレスを直接操作するわけではありません。
refは、変数をメソッドに参照渡しするための仕組みです。
C#void Add(ref int value)
{
value += 10;
}
int number = 5;
Add(ref number);
Console.WriteLine(number);
outは、メソッドから複数の値を返したいときなどに使います。
C#bool success = int.TryParse("123", out int result);
これらは安全なC#の機能です。一方、ポインタはメモリ上のアドレスを直接扱うため、より低レベルで危険性も高い操作です。
2. C#でポインタを使う前に知っておくべき基礎知識
C#でポインタを使う前に、マネージドコード、アンマネージドコード、ガベージコレクション、型の制限などを理解しておく必要があります。
2-1. マネージドコードとアンマネージドコードの違い
C#で通常書くコードは、マネージドコードです。
マネージドコードとは、.NETランタイムによって管理されるコードのことです。メモリ管理、型安全性、例外処理、セキュリティなど、多くの部分をランタイムが支援してくれます。
一方、アンマネージドコードとは、.NETランタイムの管理外で動作するコードです。CやC++で作られたDLL、OSのネイティブAPI、外部ライブラリなどが該当します。
C#のポインタは、主にアンマネージドコードと連携する場面で使われます。
たとえば、Cで作られたライブラリがメモリアドレスを引数として要求する場合、C#側でもポインタやIntPtrを使って連携する必要が出てきます。
2-2. ガベージコレクションとメモリ管理の仕組み
C#では、使われなくなったオブジェクトをガベージコレクション、つまりGCが自動で回収します。
開発者が明示的にメモリを解放しなくても、.NETランタイムが不要になったオブジェクトを判断してメモリを整理してくれます。
ただし、GCはメモリを回収するだけでなく、状況によってはオブジェクトの配置場所を移動することがあります。これにより、メモリを効率よく使えるようになります。
通常のC#コードでは、オブジェクトの移動を意識する必要はありません。しかし、ポインタでオブジェクトのアドレスを直接保持している場合、GCによってオブジェクトが移動すると、ポインタが古い場所を指してしまう可能性があります。
この問題を防ぐために使うのがfixedです。
2-3. ポインタが使える型と使えない型
C#のポインタは、すべての型に対して使えるわけではありません。
基本的にポインタとして扱えるのは、アンマネージド型です。たとえば、次のような型です。
C#int
long
short
byte
float
double
char
bool
enum
ポインタ型
参照型フィールドを含まない構造体
一方、クラスやstring、object、配列そのものなどの参照型は、そのままポインタ型として扱うことはできません。
C#// string* p; // 基本的にはこのような使い方はできない
// object* p; // 参照型はポインタ型にできない
構造体であっても、参照型のフィールドを含んでいる場合は注意が必要です。
C#struct Sample
{
public int Id;
public string Name;
}
このような構造体は、内部にstringという参照型を含んでいるため、単純なアンマネージド型としては扱えません。
2-4. ポインタを使うと安全性が下がる理由
C#の通常のコードでは、範囲外アクセスや型の不一致などを防ぐ仕組みが多く用意されています。
しかし、ポインタを使うと、メモリ上のアドレスを直接扱えるため、誤ったアドレスを参照したり、想定外の場所に値を書き込んだりする危険があります。
たとえば、配列の範囲外にアクセスしても、ポインタ操作では通常の配列アクセスのような安全なチェックが働かない場合があります。
C#unsafe
{
int[] numbers = { 1, 2, 3 };
fixed (int* p = numbers)
{
// 危険な例:範囲外にアクセスしてしまう可能性がある
// Console.WriteLine(*(p + 10));
}
}
このようなコードは、予期しない動作やクラッシュの原因になります。そのため、C#でポインタを使う場合は、通常のコードよりも慎重な設計と確認が必要です。
3. unsafeとは?C#でポインタを使うための宣言
C#でポインタを使うには、unsafeキーワードが必要です。unsafeは、そのコードが通常のC#の安全性の範囲外にあることを明示するための宣言です。
3-1. unsafeキーワードの役割
unsafeキーワードは、ポインタを使用するコードブロック、メソッド、クラス、構造体などを宣言するために使います。
C#では、通常のコード内でいきなりポインタを使うことはできません。
C#int value = 10;
// int* p = &value; // unsafeコンテキストではないためエラー
ポインタを使うには、次のようにunsafeブロックで囲みます。
C#unsafe
{
int value = 10;
int* p = &value;
Console.WriteLine(*p);
}
unsafeは、危険なコードを無条件に許可するものではなく、「ここではポインタなどの低レベル操作を行う」とコンパイラに伝えるためのものです。
3-2. unsafeを使える場所
unsafeは、さまざまな場所に指定できます。
メソッド全体をunsafeにすることができます。
C#unsafe void PrintValue()
{
int value = 10;
int* p = &value;
Console.WriteLine(*p);
}
一部のコードブロックだけをunsafeにすることもできます。
C#void PrintValue()
{
unsafe
{
int value = 10;
int* p = &value;
Console.WriteLine(*p);
}
}
クラス全体にunsafeを付けることも可能です。
C#unsafe class PointerSample
{
public void Run()
{
int value = 10;
int* p = &value;
Console.WriteLine(*p);
}
}
ただし、初心者の場合は、必要な範囲だけをunsafeブロックにするのがおすすめです。unsafeの範囲を小さくすることで、危険な操作がどこにあるのか分かりやすくなります。
3-3. unsafeコードを有効化する設定方法
C#でunsafeコードを使うには、プロジェクト側でもunsafeを許可する必要があります。
Visual Studioを使っている場合は、プロジェクトのビルド設定から「アンセーフコードの許可」を有効にします。
.csprojファイルに直接設定する場合は、次のように記述します。
XML<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
コマンドラインでコンパイルする場合は、コンパイラオプションでunsafeを許可します。
Bashcsc /unsafe Program.cs
.NET CLIを使う場合は、プロジェクトファイルにAllowUnsafeBlocksを追加するのが一般的です。
unsafeを有効にしていない状態でポインタを使うと、コンパイルエラーになります。
3-4. unsafeを使ったポインタの基本構文
C#のポインタは、型名の後ろに*を付けて宣言します。
C#int* p;
double* d;
byte* b;
int*は「int型の値を指すポインタ」を意味します。
変数のアドレスを取得するには、アドレス演算子&を使います。
C#unsafe
{
int value = 10;
int* p = &value;
}
ポインタが指す先の値を取得するには、間接参照演算子*を使います。
C#unsafe
{
int value = 10;
int* p = &value;
Console.WriteLine(*p);
}
この*は、宣言時と参照時で意味が少し異なります。
C#int* p; // ポインタ型の宣言
*p = 20; // ポインタが指す先の値にアクセス
最初は混乱しやすい部分ですが、「宣言の*はポインタ型」「式の中の*は指している先の値」と考えると理解しやすくなります。
3-5. ポインタの取得・参照・書き換えの基本例
次のコードは、int型の値をポインタで参照し、書き換える例です。
C#using System;
class Program
{
static unsafe void Main()
{
int number = 10;
int* p = &number;
Console.WriteLine(number);
Console.WriteLine(*p);
*p = 20;
Console.WriteLine(number);
Console.WriteLine(*p);
}
}
実行結果は次のようになります。
10
10
20
20
pはnumberのアドレスを指しています。そのため、*p = 20;と書くと、numberそのものの値が20に変わります。
このように、ポインタを使うと変数のメモリ上の位置を経由して値を操作できます。
4. fixedとは?C#でポインタを安全に扱うための固定化
C#で配列や文字列などのマネージドオブジェクトに対してポインタを取得する場合、fixedキーワードを使うことがあります。
fixedは、ガベージコレクションによってオブジェクトの位置が移動しないように、一時的に固定するための仕組みです。
4-1. fixedキーワードが必要になる理由
C#では、配列や文字列などのオブジェクトはマネージドヒープ上に配置されます。これらのオブジェクトはGCの管理対象です。
GCはメモリを整理するために、オブジェクトの位置を移動する場合があります。通常のC#コードでは問題ありませんが、ポインタでオブジェクトのアドレスを直接保持している場合は問題になります。
たとえば、配列の先頭要素のアドレスをポインタで持っているとします。その後、GCによって配列が別の場所に移動すると、ポインタは古いアドレスを指したままになる可能性があります。
この問題を防ぐために、fixedを使ってオブジェクトを一時的に固定します。
4-2. GCによるオブジェクト移動とポインタの関係
GCは、不要なオブジェクトを回収するだけでなく、メモリの断片化を減らすためにオブジェクトを移動することがあります。
通常は、変数や参照はランタイムによって正しく更新されるため、開発者が意識する必要はありません。
しかし、ポインタは単なるメモリアドレスです。GCがオブジェクトを移動しても、ポインタが自動的に新しいアドレスへ更新されるとは限りません。
そこで、fixedを使います。
C#unsafe
{
int[] numbers = { 1, 2, 3 };
fixed (int* p = numbers)
{
Console.WriteLine(*p);
}
}
fixedブロックの中では、numbersのメモリ上の位置が固定されるため、安全にポインタでアクセスできます。
4-3. 配列にfixedを使う基本例
配列にfixedを使う基本例を見てみましょう。
C#using System;
class Program
{
static unsafe void Main()
{
int[] numbers = { 10, 20, 30 };
fixed (int* p = numbers)
{
Console.WriteLine(*p);
Console.WriteLine(*(p + 1));
Console.WriteLine(*(p + 2));
}
}
}
実行結果は次のようになります。
10
20
30
pは配列の先頭要素を指しています。p + 1は次の要素、p + 2はさらに次の要素を指します。
通常の配列アクセスで書くと、次のコードに近い意味です。
C#Console.WriteLine(numbers[0]);
Console.WriteLine(numbers[1]);
Console.WriteLine(numbers[2]);
ただし、ポインタ演算では配列の範囲チェックが通常の配列アクセスほど安全には働きません。そのため、要素数を超えたアクセスをしないように注意が必要です。
4-4. 文字列にfixedを使う基本例
文字列に対してもfixedを使うことができます。
C#using System;
class Program
{
static unsafe void Main()
{
string text = "ABC";
fixed (char* p = text)
{
Console.WriteLine(*p);
Console.WriteLine(*(p + 1));
Console.WriteLine(*(p + 2));
}
}
}
実行結果は次のようになります。
A
B
C
C#の文字列はcharの並びとして扱われます。そのため、char*を使って文字列の各文字にアクセスできます。
ただし、C#のstringは不変です。通常のコードでは文字列の内容を書き換えることはできません。ポインタを使って無理に書き換えようとするのは危険であり、避けるべきです。
文字列の内容を変更したい場合は、StringBuilderやchar[]を使う方が安全です。
4-5. fixedを使うときの注意点
fixedを使うときは、固定する範囲をできるだけ短くすることが大切です。
C#fixed (int* p = numbers)
{
// 必要な処理だけを書く
}
オブジェクトを固定している間、GCはそのオブジェクトを移動できません。固定時間が長くなると、メモリ管理の効率が下がる可能性があります。
また、fixedブロックの外でポインタを使ってはいけません。
C#int* p;
fixed (int* temp = numbers)
{
p = temp;
}
// fixedブロックの外でpを使うのは危険
fixedブロックを抜けると、オブジェクトの固定は解除されます。その後、GCによってオブジェクトが移動する可能性があるため、取得したポインタを使い続けるのは危険です。
5. C#のポインタでできる主な操作
C#のポインタでは、アドレスの取得、値の参照、値の書き換え、ポインタ演算、構造体メンバーへのアクセスなどができます。
5-1. ポインタ変数の宣言
ポインタ変数は、型名の後ろに*を付けて宣言します。
C#unsafe
{
int* p1;
double* p2;
byte* p3;
}
複数のポインタを宣言する場合は、書き方に注意が必要です。
C#unsafe
{
int* p1, p2;
}
この場合、p1もp2もint*です。
ただし、可読性を考えると、1行に複数のポインタ変数を書くよりも、1つずつ宣言した方が分かりやすいです。
C#unsafe
{
int* p1;
int* p2;
}
ポインタにはnullを代入することもできます。
C#unsafe
{
int* p = null;
}
ただし、nullポインタを間接参照すると危険です。
5-2. アドレス演算子&の使い方
アドレス演算子&は、変数のアドレスを取得するために使います。
C#unsafe
{
int value = 10;
int* p = &value;
}
&valueは、valueがメモリ上に置かれている場所を表します。
ただし、すべての変数に対して自由に&を使えるわけではありません。マネージドオブジェクトに対してポインタを取得する場合は、fixedが必要になることがあります。
ローカル変数のようにスタック上に置かれる値であれば、比較的シンプルにアドレスを取得できます。
C#unsafe
{
int x = 100;
int* px = &x;
Console.WriteLine((long)px);
}
このように、&はポインタの基本となる演算子です。
5-3. 間接参照演算子*の使い方
間接参照演算子*は、ポインタが指している先の値を取得したり、書き換えたりするために使います。
C#unsafe
{
int value = 10;
int* p = &value;
Console.WriteLine(*p);
}
*pは、pが指しているアドレスにある値を意味します。
値を書き換えることもできます。
C#unsafe
{
int value = 10;
int* p = &value;
*p = 99;
Console.WriteLine(value);
}
実行結果は次のようになります。
99
pはvalueを指しているため、*p = 99;によってvalueの中身が書き換わります。
5-4. ポインタ演算の基本
ポインタ演算では、ポインタに数値を足したり引いたりして、隣の要素に移動できます。
C#unsafe
{
int[] numbers = { 10, 20, 30 };
fixed (int* p = numbers)
{
Console.WriteLine(*p);
Console.WriteLine(*(p + 1));
Console.WriteLine(*(p + 2));
}
}
ここで重要なのは、p + 1が「アドレスを1バイト進める」という意味ではないことです。
int*の場合、p + 1はint型1個分だけ進みます。intが4バイトであれば、実際のアドレスは4バイト分進むイメージです。
byte*の場合は、1バイト単位で進みます。
C#unsafe
{
byte[] data = { 1, 2, 3 };
fixed (byte* p = data)
{
Console.WriteLine(*p);
Console.WriteLine(*(p + 1));
}
}
ポインタ演算は便利ですが、範囲外に進んでもコンパイラが必ず防いでくれるわけではありません。配列の長さを超えないように、開発者が責任を持って管理する必要があります。
5-5. 構造体ポインタとメンバーアクセス
構造体に対してポインタを使うこともできます。
C#using System;
struct Point
{
public int X;
public int Y;
}
class Program
{
static unsafe void Main()
{
Point point = new Point { X = 10, Y = 20 };
Point* p = &point;
Console.WriteLine((*p).X);
Console.WriteLine((*p).Y);
}
}
構造体ポインタからメンバーにアクセスする場合、(*p).Xのように書けます。
ただし、この書き方は少し読みにくいため、C#では->演算子も使えます。
C#unsafe
{
Point point = new Point { X = 10, Y = 20 };
Point* p = &point;
Console.WriteLine(p->X);
Console.WriteLine(p->Y);
}
p->Xは、(*p).Xと同じ意味です。
構造体のフィールドを書き換えることもできます。
C#unsafe
{
Point point = new Point { X = 10, Y = 20 };
Point* p = &point;
p->X = 100;
p->Y = 200;
Console.WriteLine(point.X);
Console.WriteLine(point.Y);
}
ポインタを使うことで、構造体のメンバーを直接操作できます。
5-6. void*と型変換の考え方
void*は、特定の型を持たないポインタです。
C#unsafe
{
int value = 10;
int* p = &value;
void* vp = p;
}
void*は、どの型のポインタでも受け取れる汎用的なポインタとして使えます。ただし、void*のままでは値を直接参照できません。
C#unsafe
{
int value = 10;
void* vp = &value;
int* p = (int*)vp;
Console.WriteLine(*p);
}
このように、実際に値を扱うときは適切な型のポインタにキャストする必要があります。
void*はネイティブAPIや低レベルな処理で使われることがありますが、型情報がないため、誤った型に変換すると危険です。
たとえば、本当はint*として扱うべきメモリをdouble*として扱うと、正しい値を読み取れないだけでなく、予期しない動作につながる可能性があります。
6. C#でポインタが必要になる場面
C#では、多くの場面でポインタは不要です。しかし、一部の処理ではポインタが有効な選択肢になることがあります。
6-1. ネイティブAPIやDLLと連携するとき
C#からWindows APIやC/C++で作られたDLLを呼び出す場合、ポインタが必要になることがあります。
ネイティブAPIでは、関数の引数としてメモリアドレスを渡す設計になっていることが多いためです。
C#では、P/Invokeを使ってネイティブ関数を呼び出せます。
C#using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("kernel32.dll")]
private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
}
このような連携では、IntPtrやポインタ、Marshalクラスなどを使ってメモリを扱うことがあります。
ただし、ネイティブAPI連携では必ずポインタを使うとは限りません。多くの場合、IntPtrや構造体、配列のマーシャリングで対応できます。
6-2. 画像処理・音声処理など高速なメモリアクセスが必要なとき
画像処理や音声処理では、大量のバイトデータを高速に読み書きする必要があります。
たとえば、画像のピクセルデータを1つずつ処理する場合、通常の配列アクセスでも実装できますが、処理量が非常に多いとパフォーマンスが問題になることがあります。
ポインタを使うと、メモリ上のデータに直接アクセスできるため、処理を高速化できる場合があります。
C#unsafe
{
byte[] pixels = new byte[1024];
fixed (byte* p = pixels)
{
for (int i = 0; i < pixels.Length; i++)
{
*(p + i) = 255;
}
}
}
このような処理は、画像のフィルター処理、音声波形の加工、バイナリデータの変換などで使われることがあります。
ただし、近年のC#ではSpan<T>やMemory<T>など、安全性とパフォーマンスを両立しやすい機能もあります。ポインタを使う前に、これらの代替手段も検討するべきです。
6-3. 大量データ処理でパフォーマンスを最適化したいとき
大量の数値データやバイナリデータを処理する場合、ポインタを使った最適化が有効になることがあります。
たとえば、配列のすべての要素を高速に処理したい場合です。
C#unsafe
{
int[] data = new int[1000000];
fixed (int* p = data)
{
for (int i = 0; i < data.Length; i++)
{
*(p + i) = i;
}
}
}
ただし、ポインタを使えば必ず速くなるわけではありません。
現在の.NETは最適化が進んでおり、通常の配列アクセスやSpan<T>でも十分に高速なことが多いです。パフォーマンス目的でポインタを使う場合は、必ずベンチマークを取り、本当に効果があるか確認する必要があります。
6-4. 既存のC/C++ライブラリと連携するとき
既存のC/C++ライブラリをC#から利用する場合、ポインタが必要になることがあります。
たとえば、Cの関数が次のような形になっている場合です。
Cvoid ProcessData(unsigned char* data, int length);
この関数をC#から呼び出す場合、byte[]を固定してbyte*を渡す、またはIntPtrを使うといった対応が必要になります。
C#unsafe
{
byte[] data = new byte[256];
fixed (byte* p = data)
{
// ネイティブ関数にpを渡すイメージ
// ProcessData(p, data.Length);
}
}
このように、C#のポインタは、C/C++との橋渡しとして使われることがあります。
6-5. 通常のアプリ開発ではポインタを避けるべき理由
通常のアプリ開発では、ポインタはできるだけ避けるべきです。
理由は、コードの安全性、可読性、保守性が下がるためです。
C#には、ポインタを使わなくても多くの問題を解決できる仕組みがあります。
C#ref
out
in
Span<T>
Memory<T>
IntPtr
Marshal
配列
List<T>
構造体
クラス
ポインタを使うと、メモリ破壊や範囲外アクセスなどのリスクが増えます。チーム開発では、他の開発者がコードを理解しにくくなる可能性もあります。
そのため、C#でポインタを使うのは、「どうしても必要な場合」に限定するのが基本です。
7. C#のポインタを使うときの注意点とリスク
C#のポインタは強力ですが、使い方を誤ると大きな問題につながります。ここでは、代表的な注意点とリスクを解説します。
7-1. メモリ破壊や予期しないクラッシュが起きる可能性
ポインタを使うと、メモリ上の任意の場所にアクセスできてしまう可能性があります。
間違ったアドレスに値を書き込むと、本来変更してはいけないデータを壊してしまうことがあります。
C#unsafe
{
int value = 10;
int* p = &value;
*(p + 1000) = 999; // 非常に危険
}
このようなコードは、プログラムのクラッシュやデータ破壊につながる可能性があります。
通常のC#コードであれば、配列の範囲外アクセスなどは例外として検出されやすいですが、ポインタ操作では問題が分かりにくいことがあります。
7-2. nullポインタや範囲外アクセスに注意する
ポインタにはnullを代入できます。
C#unsafe
{
int* p = null;
}
しかし、nullポインタを間接参照すると危険です。
C#unsafe
{
int* p = null;
// 危険
// Console.WriteLine(*p);
}
また、配列をポインタで操作する場合は、範囲外アクセスに注意が必要です。
C#unsafe
{
int[] numbers = { 1, 2, 3 };
fixed (int* p = numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(*(p + i));
}
}
}
このように、必ず配列の長さを使って範囲を管理しましょう。
7-3. セキュリティリスクが高くなる
ポインタ操作には、セキュリティ上のリスクもあります。
範囲外アクセスやメモリ破壊は、脆弱性の原因になることがあります。特に、外部から受け取ったデータをポインタで処理する場合は注意が必要です。
たとえば、ファイル、ネットワーク、画像、音声、バイナリデータなどを扱う場合、入力データのサイズや内容を正しく検証しないと、想定外のメモリアクセスにつながる可能性があります。
unsafeコードを書くときは、次の点を意識しましょう。
C#入力サイズを必ず確認する
nullチェックを行う
範囲外アクセスを防ぐ
fixedの範囲を最小限にする
外部データを信用しない
ポインタを使う部分は、通常のコード以上に慎重なレビューが必要です。
7-4. コードの可読性・保守性が下がる
ポインタを使ったコードは、通常のC#コードよりも読みにくくなりがちです。
C#*(p + i) = *(p + i) + 1;
このようなコードは、ポインタに慣れていない開発者にとって理解しにくいです。
通常の配列アクセスであれば、次のように書けます。
C#numbers[i] = numbers[i] + 1;
どちらが読みやすいかは明らかです。
保守性を考えると、ポインタを使う範囲はできるだけ小さくし、処理内容が分かるようにコメントやメソッド分割を行うことが大切です。
7-5. unsafeを使う前に検討すべき代替手段
unsafeを使う前に、本当にポインタが必要かを確認しましょう。
多くの場合、次のような代替手段があります。
C#ref
out
in
Span<T>
ReadOnlySpan<T>
Memory<T>
IntPtr
Marshal
Buffer
Array
たとえば、配列の一部を効率よく扱いたいだけであれば、Span<T>で対応できることがあります。
C#int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = numbers.AsSpan(1, 3);
foreach (int n in span)
{
Console.WriteLine(n);
}
このように、安全な方法で目的を達成できるなら、ポインタを使わない方がよいです。
8. ポインタを使わない代替手段
C#では、ポインタを使わなくても低レベルな処理や高速な処理を実現できる機能が多く用意されています。
8-1. ref・out・inを使う
ref、out、inは、メソッドに値を渡す方法を制御するためのキーワードです。
refは、呼び出し先で値を読み書きしたいときに使います。
C#void Increment(ref int value)
{
value++;
}
int number = 10;
Increment(ref number);
Console.WriteLine(number);
outは、メソッドから値を返したいときに使います。
C#bool success = int.TryParse("123", out int result);
inは、値をコピーせずに読み取り専用で渡したいときに使います。
C#void Print(in int value)
{
Console.WriteLine(value);
}
これらはポインタではありませんが、値の受け渡しを効率化できます。通常のC#開発では、ポインタよりもまずref、out、inを検討しましょう。
8-2. Span<T>・Memory<T>を使う
Span<T>は、配列やメモリ領域の一部を安全かつ効率的に扱うための型です。
C#int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = numbers.AsSpan(1, 3);
span[0] = 100;
Console.WriteLine(numbers[1]);
実行結果は次のようになります。
100
Span<T>を使うと、配列の一部をコピーせずに操作できます。
文字列の一部を効率よく扱う場合は、ReadOnlySpan<char>が使われることもあります。
C#string text = "Hello C# Pointer";
ReadOnlySpan<char> span = text.AsSpan(6, 2);
Console.WriteLine(span.ToString());
Memory<T>は、Span<T>と似ていますが、非同期処理などでも扱いやすい型です。
C#Memory<int> memory = new int[] { 1, 2, 3, 4, 5 };
高性能な処理をしたい場合でも、いきなりポインタを使うのではなく、まずSpan<T>やMemory<T>を検討するのがおすすめです。
8-3. IntPtrを使う
IntPtrは、ポインタやハンドルを表すための型です。
ネイティブAPIや外部ライブラリと連携するときによく使われます。
C#IntPtr ptr = IntPtr.Zero;
IntPtrはポインタそのものに近い役割を持ちますが、C#のポインタ型とは異なります。unsafeを使わずに扱える場面も多いです。
たとえば、ネイティブ関数から返されたハンドルを保持する場合などに使われます。
C#IntPtr handle = IntPtr.Zero;
ただし、IntPtrも低レベルな値を扱うため、安全というわけではありません。無効なハンドルや解放済みメモリを扱うと問題が起きる可能性があります。
8-4. Marshalクラスを使う
Marshalクラスは、マネージドコードとアンマネージドコードの間でデータを変換するために使います。
たとえば、アンマネージドメモリを確保するには、次のように書けます。
C#using System;
using System.Runtime.InteropServices;
class Program
{
static void Main()
{
IntPtr ptr = Marshal.AllocHGlobal(sizeof(int));
try
{
Marshal.WriteInt32(ptr, 123);
int value = Marshal.ReadInt32(ptr);
Console.WriteLine(value);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
}
この例では、Marshal.AllocHGlobalでアンマネージドメモリを確保し、Marshal.FreeHGlobalで解放しています。
ポインタを直接使わずにアンマネージドメモリを扱えるため、ネイティブ連携では有効な選択肢です。
ただし、メモリ解放を忘れるとメモリリークにつながります。Marshalを使う場合も、管理責任が増えることを理解しておきましょう。
8-5. 安全性とパフォーマンスのバランスで選ぶ
C#でメモリを効率的に扱いたい場合、選択肢はいくつかあります。
通常のアプリ開発であれば、まず配列、List、クラス、構造体などの基本機能で十分です。
メソッド間で値を書き換えたい場合は、refやoutが使えます。
配列や文字列の一部を効率的に扱いたい場合は、Span<T>やMemory<T>が候補になります。
ネイティブAPIと連携する場合は、IntPtrやMarshalが使えることがあります。
それでも対応できない場合や、明確なパフォーマンス上の理由がある場合に、C#のポインタやunsafeを検討します。
重要なのは、「ポインタが使えるから使う」のではなく、「安全な方法では目的を達成できないため、必要最小限で使う」という考え方です。
9. C#ポインタのサンプルコードで理解する
ここでは、C#のポインタを理解するためのサンプルコードを紹介します。実際に試す場合は、unsafeコードを有効化してから実行してください。
9-1. int型の値をポインタで参照する例
まずは、int型の値をポインタで参照する基本例です。
C#using System;
class Program
{
static unsafe void Main()
{
int value = 42;
int* p = &value;
Console.WriteLine(value);
Console.WriteLine(*p);
}
}
実行結果は次のようになります。
42
42
pはvalueのアドレスを保持しています。*pで、そのアドレスにある値を読み取っています。
次に、ポインタを経由して値を書き換えてみます。
C#using System;
class Program
{
static unsafe void Main()
{
int value = 42;
int* p = &value;
*p = 100;
Console.WriteLine(value);
}
}
実行結果は次のようになります。
100
*p = 100;によって、valueの値が変更されました。
9-2. 配列をfixedで固定して操作する例
次に、配列をfixedで固定してポインタ操作する例です。
C#using System;
class Program
{
static unsafe void Main()
{
int[] numbers = { 10, 20, 30, 40, 50 };
fixed (int* p = numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(*(p + i));
}
}
}
}
実行結果は次のようになります。
10
20
30
40
50
fixedによって配列が一時的に固定され、ポインタで各要素にアクセスできるようになります。
次のように、ポインタ経由で値を書き換えることもできます。
C#using System;
class Program
{
static unsafe void Main()
{
int[] numbers = { 1, 2, 3 };
fixed (int* p = numbers)
{
*(p + 0) = 10;
*(p + 1) = 20;
*(p + 2) = 30;
}
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
}
実行結果は次のようになります。
10
20
30
9-3. 構造体をポインタで扱う例
構造体をポインタで扱う例を見てみましょう。
C#using System;
struct Point
{
public int X;
public int Y;
}
class Program
{
static unsafe void Main()
{
Point point = new Point
{
X = 10,
Y = 20
};
Point* p = &point;
Console.WriteLine(p->X);
Console.WriteLine(p->Y);
p->X = 100;
p->Y = 200;
Console.WriteLine(point.X);
Console.WriteLine(point.Y);
}
}
実行結果は次のようになります。
10
20
100
200
p->Xやp->Yを使うことで、構造体のメンバーにアクセスできます。
このような書き方は、C/C++のポインタに慣れている人には分かりやすいですが、通常のC#コードと比べるとやや低レベルです。
9-4. ポインタを使った簡単なパフォーマンス比較
ポインタはパフォーマンス目的で使われることがありますが、必ず速くなるとは限りません。
次の例は、通常の配列アクセスとポインタアクセスを比較する簡単なサンプルです。
C#using System;
using System.Diagnostics;
class Program
{
static unsafe void Main()
{
int[] data = new int[10_000_000];
Stopwatch sw1 = Stopwatch.StartNew();
for (int i = 0; i < data.Length; i++)
{
data[i] = i;
}
sw1.Stop();
Stopwatch sw2 = Stopwatch.StartNew();
fixed (int* p = data)
{
for (int i = 0; i < data.Length; i++)
{
*(p + i) = i;
}
}
sw2.Stop();
Console.WriteLine($"Array: {sw1.ElapsedMilliseconds} ms");
Console.WriteLine($"Pointer: {sw2.ElapsedMilliseconds} ms");
}
}
このコードでは、配列アクセスとポインタアクセスの処理時間を比較しています。
ただし、この結果は環境や.NETのバージョン、最適化設定、実行状況によって変わります。ポインタを使ったからといって必ず高速になるわけではありません。
パフォーマンスを理由にunsafeを使う場合は、必ず実際の処理内容でベンチマークを取りましょう。また、Span<T>や通常の配列アクセスで十分に高速ではないかも確認することが重要です。
9-5. 初心者がつまずきやすいエラーと解決方法
C#のポインタで初心者がよくつまずくのが、unsafeに関するコンパイルエラーです。
たとえば、次のようなエラーが出ることがあります。
Unsafe code may only appear if compiling with /unsafe
これは、プロジェクトでunsafeコードが許可されていない場合に発生します。
解決するには、.csprojに次の設定を追加します。
XML<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
また、ポインタをunsafeブロックの外で使おうとするとエラーになります。
C#int value = 10;
// int* p = &value; // unsafeではないためエラー
解決するには、unsafeブロックで囲みます。
C#unsafe
{
int value = 10;
int* p = &value;
}
配列に対してポインタを取得しようとしてエラーになることもあります。
C#int[] numbers = { 1, 2, 3 };
// unsafe
// {
// int* p = numbers; // fixedが必要
// }
この場合は、fixedを使います。
C#unsafe
{
int[] numbers = { 1, 2, 3 };
fixed (int* p = numbers)
{
Console.WriteLine(*p);
}
}
C#のポインタでは、エラーの原因が「unsafeがない」「unsafe許可設定がない」「fixedがない」「ポインタにできない型を使っている」のいずれかであることが多いです。
10. C#のポインタに関するよくある質問
ここでは、C#のポインタについて初心者が疑問に感じやすい点をまとめます。
10-1. C#でポインタは必ず覚える必要がある?
C#でポインタを必ず覚える必要はありません。
通常のアプリ開発では、ポインタを使わなくても十分に開発できます。Webアプリ、業務システム、API、デスクトップアプリなどでは、ポインタを使う場面はほとんどありません。
初心者は、まず次の内容を優先して学ぶべきです。
C#変数
型
クラス
構造体
配列
List<T>
メソッド
例外処理
LINQ
非同期処理
参照型と値型
ポインタは、C#の基本を理解したあとで、必要に応じて学べば十分です。
10-2. unsafeを使うと危険なの?
unsafeを使うこと自体がすぐに危険というわけではありません。
ただし、unsafeコードでは、通常のC#が提供している安全性の一部が働かなくなります。そのため、メモリ破壊、範囲外アクセス、クラッシュ、セキュリティリスクなどが発生しやすくなります。
unsafeを使う場合は、次の点を守ることが大切です。
C#unsafeの範囲を最小限にする
入力値を検証する
配列の範囲を必ず確認する
fixedの外でポインタを使わない
代替手段を先に検討する
コードレビューを行う
unsafeは「使ってはいけないもの」ではなく、「必要な場面で慎重に使うもの」です。
10-3. fixedを使わないとどうなる?
マネージドオブジェクトに対してfixedを使わずにポインタを取得しようとすると、コンパイルエラーになることがあります。
fixedが必要な理由は、GCによってオブジェクトの位置が移動する可能性があるためです。
配列や文字列のようなマネージドオブジェクトに対してポインタを取得する場合は、fixedで一時的に固定します。
C#unsafe
{
int[] numbers = { 1, 2, 3 };
fixed (int* p = numbers)
{
Console.WriteLine(*p);
}
}
fixedを使わずに無理にポインタを保持すると、GCによる移動によって無効なアドレスを参照してしまう可能性があります。
10-4. C#のポインタとIntPtrは何が違う?
C#のポインタとIntPtrは、どちらもメモリアドレスやハンドルを扱う場面で登場しますが、役割が異なります。
C#のポインタは、int*やbyte*のように型を持ったポインタです。値を直接参照したり、ポインタ演算を行ったりできます。
C#unsafe
{
int value = 10;
int* p = &value;
Console.WriteLine(*p);
}
一方、IntPtrは、ポインタやハンドルを表すための構造体です。
C#IntPtr ptr = IntPtr.Zero;
IntPtrは、ネイティブAPIとの連携でよく使われます。ポインタ型と違い、通常はunsafeなしで扱えます。
ただし、IntPtrも低レベルな値を扱うため、誤った使い方をすると問題が起きます。
簡単にまとめると、ポインタは「型付きで直接メモリを操作するもの」、IntPtrは「アドレスやハンドルを値として保持するためのもの」と考えると分かりやすいです。
10-5. 初心者はどこまで理解すればよい?
初心者は、C#のポインタを完璧に使いこなす必要はありません。
まずは、次の内容を理解できれば十分です。
C#ポインタはメモリアドレスを扱う仕組み
C#では通常ポインタを使わない
ポインタを使うにはunsafeが必要
配列や文字列を固定するにはfixedを使う
ポインタは危険性が高い
代替手段としてref、out、Span<T>、IntPtr、Marshalがある
実務でポインタが必要になるのは、ネイティブ連携、画像処理、音声処理、大量データ処理など、比較的専門的な場面です。
まずは通常のC#コードをしっかり書けるようになり、その後で必要に応じてunsafeやfixedを学ぶのがおすすめです。
まとめ
C#のポインタは、メモリ上のアドレスを直接扱うための低レベルな機能です。通常のC#開発ではポインタを使わなくても十分に開発できますが、ネイティブAPIとの連携や高速なメモリアクセスが必要な場面では役立つことがあります。
C#でポインタを使うには、unsafeキーワードが必要です。unsafeコードを使うことで、int*やbyte*のようなポインタ型を宣言し、&でアドレスを取得し、*で値を参照できます。
また、配列や文字列などのマネージドオブジェクトに対してポインタを取得する場合は、fixedを使ってGCによる移動を一時的に防ぐ必要があります。
ただし、ポインタにはメモリ破壊、範囲外アクセス、nullポインタ、セキュリティリスク、保守性低下といった注意点があります。C#でポインタを使う場合は、必要最小限の範囲に限定し、代替手段がないかを先に検討しましょう。
多くの場合、ref、out、in、Span<T>、Memory<T>、IntPtr、Marshalなどを使えば、ポインタを直接扱わずに目的を達成できます。
C#のポインタは、初心者が最初に覚えるべき基本機能ではありません。しかし、unsafeやfixedの意味を理解しておくと、C#がどのようにメモリを扱っているのか、ネイティブコードとどのように連携するのかを深く理解できるようになります。

