C#のIntPtrとは?使い方・IntPtr.Zero・32/64bitの違いを初心者向けに解説

はじめに

C#でWindows APIやネイティブDLL、メモリ操作について調べていると、IntPtrという型を見かけることがあります。

普段のC#開発では、stringintList<T>、クラス、インターフェースなどを使うことが多く、IntPtrは少し特殊に見える型です。そのため、「これはポインタなのか?」「IntPtr.Zeroとは何か?」「32bitと64bitで何が変わるのか?」と疑問に感じる人も多いでしょう。

結論から言うと、IntPtrはネイティブコードとのやり取りや、OSのハンドル、メモリアドレスのような低レベルの値を扱うために使われる型です。通常のC#アプリでは頻繁に使うものではありませんが、P/Invoke、Windows API、アンマネージメモリ、外部DLL連携では重要になります。

この記事では、C#のIntPtrについて、初心者にもわかるように基本から解説します。

1. C#のIntPtrとは?まず結論からわかりやすく解説

1-1. IntPtrは「ネイティブメモリやハンドルを表すための整数型」

IntPtrは、ポインタやハンドルの値を格納するために使われる、プラットフォーム依存サイズの整数型です。

Microsoftの公式ドキュメントでは、IntPtrはポインタと同じサイズになる整数型として設計されており、32bitプロセスでは32bit、64bitプロセスでは64bitになると説明されています。

簡単に言うと、IntPtrは次のような値をC#側で受け取るための入れ物です。

C#
IntPtr handle;
IntPtr pointer;

ここで重要なのは、IntPtr自体が安全なポインタ操作を提供するわけではないという点です。あくまで「ポインタやハンドルとして使われる数値を保持できる型」と考えると理解しやすくなります。

1-2. ポインタ・参照・int・longとの違い

C#には、似ているようで意味が異なる概念があります。

intは32bitの整数です。longは64bitの整数です。一方、IntPtrは実行環境によってサイズが変わります。32bit環境では4バイト、64bit環境では8バイトです。

C#の「参照」は、オブジェクトを指し示す仕組みですが、通常のC#コードではメモリアドレスを直接扱いません。たとえば、次のようなコードでは、objはオブジェクトへの参照ですが、開発者がそのアドレスを直接操作することはありません。

C#
object obj = new object();

一方、IntPtrはネイティブAPIから返されたハンドルや、アンマネージメモリのアドレスを表すために使われます。

C#
IntPtr ptr = Marshal.AllocHGlobal(100);

また、C#のポインタ型であるint*void*は、unsafeコードで使います。IntPtrunsafeを使わなくても宣言できますが、実際にアドレスの中身を直接読み書きする場合は、Marshalクラスやunsafeコードが関係してきます。

1-3. IntPtrが使われる代表的な場面

IntPtrがよく使われる場面は、主に次のようなケースです。

P/InvokeでWindows APIやC/C++のDLLを呼び出すとき、OSのウィンドウハンドルを受け取るとき、アンマネージメモリを確保して扱うとき、ネイティブライブラリから返されたポインタを管理するときなどです。

たとえば、Windows APIでは、ウィンドウを識別するためにHWNDというハンドルが使われます。C#ではこのようなハンドルをIntPtrで表すことがあります。

C#
IntPtr hWnd;

また、Marshal.AllocHGlobalで確保したメモリのアドレスもIntPtrで受け取ります。

C#
IntPtr memory = Marshal.AllocHGlobal(256);

このように、IntPtrはC#とOS、C#とネイティブコードの境界でよく使われる型です。

1-4. 初心者がIntPtrでつまずきやすいポイント

初心者がIntPtrでつまずきやすいポイントは、主に次の4つです。

まず、IntPtrを普通の整数と同じ感覚で扱ってしまうことです。IntPtrは整数型ではありますが、実際にはポインタやハンドルを表すことが多いため、単純な数値計算とは意味が異なります。

次に、IntPtr.Zeronullを混同することです。IntPtrは構造体なので、通常はnullではなくIntPtr.Zeroと比較します。

3つ目は、32bitと64bitの違いです。64bit環境のIntPtrintに変換すると、値が収まらず例外や不具合の原因になることがあります。

4つ目は、メモリやハンドルの解放忘れです。IntPtrで扱うリソースの多くはGCが自動で解放してくれないため、明示的な解放処理が必要になります。

2. IntPtrが必要になる主な用途

2-1. Windows APIやネイティブDLLを呼び出すP/Invoke

IntPtrがもっともよく登場するのは、P/InvokeでWindows APIやネイティブDLLを呼び出す場面です。

P/Invokeとは、C#などのマネージドコードから、CやC++で作られたネイティブ関数を呼び出す仕組みです。C#ではDllImport属性を使って外部関数を宣言します。

C#
using System;
using System.Runtime.InteropServices;

class NativeMethods
{
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
}

この例では、現在アクティブなウィンドウのハンドルをIntPtrで受け取っています。

Windows APIの戻り値や引数には、HWNDHANDLELPVOIDHINSTANCEなど、ポインタやハンドルを表す型が多くあります。C#では、これらをIntPtrで表現することがあります。

2-2. ウィンドウハンドルやファイルハンドルの受け渡し

Windowsでは、ウィンドウ、ファイル、プロセス、スレッド、アイコンなど、多くのリソースが「ハンドル」という値で管理されます。

たとえば、ウィンドウハンドルは特定のウィンドウを識別する値です。

C#
IntPtr hWnd = NativeMethods.GetForegroundWindow();

このhWndを別のWindows APIに渡すことで、そのウィンドウに対して操作を行えます。

ただし、ハンドルは単なる数値ではなく、OSが管理するリソースを指す識別子です。そのため、使い終わったら適切なAPIで解放しなければならない場合があります。

2-3. unmanagedメモリの確保・解放

C#の通常のオブジェクトはGCによって管理されます。しかし、Marshal.AllocHGlobalなどで確保したアンマネージメモリはGCの管理外です。

C#
IntPtr ptr = Marshal.AllocHGlobal(100);

このように確保したメモリは、不要になったら自分で解放する必要があります。

C#
Marshal.FreeHGlobal(ptr);

IntPtrは、このようなアンマネージメモリのアドレスを保持するために使われます。

2-4. unsafeコードやポインタ操作との関係

C#では、unsafeを使うとC/C++に近いポインタ操作ができます。

C#
unsafe
{
int value = 10;
int* p = &value;
}

IntPtrは、unsafeポインタと相互に変換されることがあります。

C#
unsafe
{
int value = 10;
int* p = &value;
IntPtr ptr = (IntPtr)p;
}

ただし、unsafeコードはメモリ安全性の保証が弱くなるため、初心者が安易に使うべきではありません。多くの場合は、Marshalクラスや適切なラッパーを使う方が安全です。

2-5. 通常のC#アプリでIntPtrを使うべきケース・使わないケース

通常のC#アプリで、IntPtrを自分から積極的に使う場面は多くありません。

たとえば、Webアプリ、業務アプリ、デスクトップアプリの一般的なロジックでは、IntPtrを使わずに済むことがほとんどです。文字列、数値、クラス、コレクション、ストリームなど、通常の.NET型を使えば十分です。

一方で、次のような場合はIntPtrが必要になることがあります。

Windows APIを呼び出す場合、C/C++のDLLと連携する場合、画像処理や音声処理などでネイティブライブラリを使う場合、既存のAPIがIntPtrを要求している場合です。

つまり、IntPtrは「通常のデータ処理のための型」ではなく、「C#の外側にあるネイティブ世界とやり取りするための型」と考えるとよいでしょう。

3. IntPtrの基本的な使い方

3-1. IntPtr型の宣言と初期化

IntPtrは次のように宣言できます。

C#
IntPtr ptr;

初期値としてゼロを設定したい場合は、IntPtr.Zeroを使います。

C#
IntPtr ptr = IntPtr.Zero;

IntPtr.Zeroは、値が0のIntPtrを表します。ポインタやハンドルが無効であることを表すためによく使われます。

3-2. int・longからIntPtrを作成する方法

intlongからIntPtrを作成するには、コンストラクタを使います。

C#
IntPtr ptr1 = new IntPtr(123);
IntPtr ptr2 = new IntPtr(123456789L);

また、キャストで作成することもできます。

C#
IntPtr ptr = (IntPtr)123;

ただし、実際の開発では、単なる数値からIntPtrを作る場面は多くありません。多くの場合、ネイティブAPIの戻り値やMarshalクラスの戻り値としてIntPtrを受け取ります。

3-3. IntPtrから数値へ変換する方法

IntPtrから数値へ変換するには、ToInt32またはToInt64を使います。

C#
IntPtr ptr = new IntPtr(123);

int value32 = ptr.ToInt32();
long value64 = ptr.ToInt64();

ただし、64bit環境ではIntPtrの値がintの範囲を超える可能性があります。そのため、安易にToInt32を使うのは危険です。

基本的には、ログ出力や比較などで数値化したい場合はToInt64を使う方が安全です。

3-4. IntPtr.ToInt32とToInt64の使い分け

ToInt32は、IntPtrの値を32bit整数に変換します。Microsoftのドキュメントでも、ToInt32は現在のインスタンスの値を32bit符号付き整数に変換するメソッドとして説明されています。

32bitプロセスであれば、IntPtrのサイズは4バイトなのでToInt32でも扱えることが多いです。しかし、64bitプロセスではIntPtrが8バイトになるため、ToInt32では値が収まらない可能性があります。

そのため、32bitと64bitの両方で動くコードを書くなら、次のようにToInt64を使う方が安全です。

C#
long address = ptr.ToInt64();

ただし、そもそもポインタやハンドルを数値に変換しなければならない場面は限られます。必要がないなら、IntPtrのまま扱うのが基本です。

3-5. IntPtr.Addを使ったアドレス計算

IntPtr.Addを使うと、IntPtrにオフセットを加算できます。公式ドキュメントでも、Add(IntPtr, Int32)は符号付き整数のオフセットを追加するメソッドとして説明されています。

C#
IntPtr basePtr = Marshal.AllocHGlobal(100);
IntPtr nextPtr = IntPtr.Add(basePtr, 4);

この例では、basePtrから4バイト進んだ位置をnextPtrとして取得しています。

ただし、アドレス計算は非常に慎重に行う必要があります。確保したメモリ範囲を超えてアクセスすると、不正なメモリアクセスやアプリケーションのクラッシュにつながる可能性があります。

3-6. サンプルコードで見る基本操作

次のコードは、IntPtrの基本操作をまとめた例です。

C#
using System;

class Program
{
static void Main()
{
IntPtr ptr1 = IntPtr.Zero;
IntPtr ptr2 = new IntPtr(12345);

Console.WriteLine(ptr1 == IntPtr.Zero); // True
Console.WriteLine(ptr2.ToInt64()); // 12345

IntPtr ptr3 = IntPtr.Add(ptr2, 10);
Console.WriteLine(ptr3.ToInt64()); // 12355

Console.WriteLine(IntPtr.Size); // 32bitなら4、64bitなら8
}
}

このように、IntPtrはゼロ比較、数値変換、オフセット加算、サイズ確認などの基本操作ができます。

4. IntPtr.Zeroとは?nullとの違いを解説

4-1. IntPtr.Zeroの意味

IntPtr.Zeroは、値が0のIntPtrを表す読み取り専用フィールドです。Microsoftの公式ドキュメントでも、IntPtr.Zeroは0に初期化された符号付き整数を表すフィールドとして説明されています。

C#では、ポインタやハンドルが存在しない、または無効であることを表すためにIntPtr.Zeroを使います。

C#
IntPtr ptr = IntPtr.Zero;

これは、C/C++でいうNULLポインタのような意味で使われることがあります。

4-2. IntPtr.Zeroを使うべき場面

IntPtr.Zeroは、主に次のような場面で使います。

初期値として無効なポインタを表したいとき、ネイティブAPIの戻り値が有効かどうか確認するとき、P/Invokeの引数にNULL相当の値を渡したいときです。

たとえば、Windows APIの戻り値がIntPtr.Zeroなら失敗や未取得を表すことがあります。

C#
IntPtr hWnd = GetForegroundWindow();

if (hWnd == IntPtr.Zero)
{
Console.WriteLine("ウィンドウハンドルを取得できませんでした。");
}

4-3. nullではなくIntPtr.Zeroを使う理由

IntPtrはクラスではなく構造体です。そのため、通常のIntPtr変数はnullにはなりません。

C#
IntPtr ptr = IntPtr.Zero;

次のようにnullと比較するのではなく、IntPtr.Zeroと比較します。

C#
if (ptr == IntPtr.Zero)
{
Console.WriteLine("無効なポインタです。");
}

IntPtr.Zeronullと同じではありません。公式ドキュメントでも、IntPtr.Zeroの値はnullと同じではないと説明されています。

4-4. ハンドルやポインタの有効・無効を判定する方法

ハンドルやポインタの有効・無効は、APIごとに判定方法が異なります。

多くのAPIでは、失敗時にIntPtr.Zeroを返します。

C#
if (handle == IntPtr.Zero)
{
// 失敗または無効
}

しかし、Windows APIの中には、失敗時に-1相当の値を返すものもあります。たとえば、ファイルハンドルを扱うAPIでは、無効なハンドルとしてINVALID_HANDLE_VALUEが使われることがあります。

C#
static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

if (handle == INVALID_HANDLE_VALUE)
{
// 無効なハンドル
}

つまり、IntPtr.Zeroだけを見れば常に正しいとは限りません。使用するAPIの仕様を確認することが重要です。

4-5. IntPtr.Zeroの比較サンプルコード

次のコードは、IntPtr.Zeroとの比較例です。

C#
using System;

class Program
{
static void Main()
{
IntPtr ptr = IntPtr.Zero;

if (ptr == IntPtr.Zero)
{
Console.WriteLine("ptrはゼロです。");
}

IntPtr another = new IntPtr(100);

if (another != IntPtr.Zero)
{
Console.WriteLine("anotherは有効な値を持っています。");
}
}
}

IntPtrの有効性をチェックする場合は、nullではなくIntPtr.Zeroと比較するのが基本です。

5. 32bit・64bit環境でのIntPtrの違い

5-1. IntPtrのサイズは実行環境によって変わる

IntPtrの大きな特徴は、サイズが実行環境によって変わることです。

32bitプロセスではIntPtrは4バイト、64bitプロセスでは8バイトになります。これは、ポインタサイズに合わせるためです。公式ドキュメントでも、IntPtrは32bitプロセスでは32bit、64bitプロセスでは64bitになると説明されています。

そのため、IntPtrを固定サイズのintと同じように扱うと、64bit環境で問題が起きる可能性があります。

5-2. IntPtr.Sizeでサイズを確認する方法

現在の実行環境でIntPtrが何バイトかを確認するには、IntPtr.Sizeを使います。

C#
Console.WriteLine(IntPtr.Size);

32bitプロセスなら4、64bitプロセスなら8が表示されます。

C#
if (IntPtr.Size == 4)
{
Console.WriteLine("32bitプロセスです。");
}
else if (IntPtr.Size == 8)
{
Console.WriteLine("64bitプロセスです。");
}

ここで注意したいのは、OSが64bitでも、アプリケーションが32bitプロセスとして動作していればIntPtr.Sizeは4になるという点です。

5-3. 32bitでは4バイト、64bitでは8バイトになる理由

IntPtrはポインタと同じサイズになるように設計されています。

32bitプロセスでは、メモリアドレスを表すためのポインタサイズが32bitです。そのため、IntPtrも4バイトになります。

64bitプロセスでは、ポインタサイズが64bitです。そのため、IntPtrも8バイトになります。

この性質により、IntPtrは32bitと64bitの両方の環境で、ネイティブポインタやハンドルを表すために使えます。

5-4. intに変換すると危険なケース

64bit環境では、IntPtrの値がintに収まらない可能性があります。

次のようなコードは危険です。

C#
int value = ptr.ToInt32();

64bitのアドレスやハンドル値を32bit整数に変換しようとすると、オーバーフローする可能性があります。

安全に数値として扱いたい場合は、次のようにlongへ変換します。

C#
long value = ptr.ToInt64();

ただし、ハンドルやポインタは本来、数値として加工するものではありません。必要がなければ、IntPtrのまま受け渡しするのが安全です。

5-5. Any CPU・x86・x64ビルド時の注意点

C#プロジェクトでは、ビルド設定としてAny CPUx86x64などを選べます。

x86でビルドすると、アプリケーションは32bitプロセスとして動作します。この場合、IntPtr.Sizeは4です。

x64でビルドすると、64bitプロセスとして動作します。この場合、IntPtr.Sizeは8です。

Any CPUの場合は、実行環境や設定によって32bitまたは64bitとして動作します。そのため、Any CPUで動かすアプリでは、IntPtrintに決め打ちするようなコードは避けるべきです。

5-6. 32/64bit対応でバグを防ぐ書き方

32bitと64bitの違いによるバグを防ぐには、次のような書き方を意識します。

まず、IntPtrを不用意にintへ変換しないことです。ログ出力などで数値が必要な場合はToInt64を使います。

C#
Console.WriteLine(ptr.ToInt64());

次に、ハンドルやポインタはできるだけIntPtrのまま扱います。

C#
IntPtr handle = GetHandle();
UseHandle(handle);

また、P/Invokeの型定義では、C/C++側の型に合わせて正しいC#型を指定することが重要です。ポインタやハンドルをintで定義すると、64bit環境で不具合が起きる可能性があります。

6. P/InvokeでIntPtrを使う方法

6-1. DllImportとIntPtrの基本

P/Invokeでは、DllImport属性を使って外部DLLの関数を宣言します。

C#
using System;
using System.Runtime.InteropServices;

class NativeMethods
{
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
}

この例では、GetForegroundWindowというWindows APIを呼び出し、戻り値としてウィンドウハンドルをIntPtrで受け取っています。

呼び出し側は次のようになります。

C#
IntPtr hWnd = NativeMethods.GetForegroundWindow();

if (hWnd != IntPtr.Zero)
{
Console.WriteLine("ウィンドウハンドルを取得しました。");
}

6-2. Windows APIのハンドルをIntPtrで受け取る例

次のコードは、アクティブなウィンドウのハンドルを取得する例です。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();

static void Main()
{
IntPtr hWnd = GetForegroundWindow();

if (hWnd == IntPtr.Zero)
{
Console.WriteLine("取得できませんでした。");
}
else
{
Console.WriteLine($"ハンドル: {hWnd}");
}
}
}

GetForegroundWindowの戻り値はウィンドウハンドルです。C#ではこれをIntPtrとして受け取ります。

6-3. 引数にIntPtrを渡すサンプル

取得したハンドルを別のAPIに渡すこともできます。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("user32.dll")]
private static extern bool IsWindow(IntPtr hWnd);

static void Main()
{
IntPtr hWnd = new IntPtr(123456);

if (IsWindow(hWnd))
{
Console.WriteLine("有効なウィンドウです。");
}
else
{
Console.WriteLine("有効なウィンドウではありません。");
}
}
}

このように、P/InvokeではIntPtrを戻り値として受け取るだけでなく、引数として渡すこともよくあります。

6-4. 戻り値がIntPtrの場合のエラーチェック

戻り値がIntPtrの場合、失敗時にどの値が返るかはAPIによって異なります。

よくあるのは、失敗時にIntPtr.Zeroが返るパターンです。

C#
IntPtr result = SomeNativeFunction();

if (result == IntPtr.Zero)
{
Console.WriteLine("処理に失敗しました。");
}

しかし、すべてのAPIがIntPtr.Zeroを返すわけではありません。失敗時に-1相当の値を返すAPIもあります。

C#
static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

if (result == INVALID_HANDLE_VALUE)
{
Console.WriteLine("無効なハンドルです。");
}

P/Invokeでは、必ず呼び出すAPIの仕様を確認し、正しいエラーチェックを行う必要があります。

6-5. Marshalクラスと組み合わせた使い方

Marshalクラスは、マネージドコードとアンマネージコードの間でデータをやり取りするためによく使われます。

たとえば、アンマネージメモリに文字列や構造体を渡す場合、IntPtrMarshalクラスを組み合わせます。

C#
IntPtr ptr = Marshal.AllocHGlobal(100);

try
{
byte[] data = { 1, 2, 3, 4 };
Marshal.Copy(data, 0, ptr, data.Length);
}
finally
{
Marshal.FreeHGlobal(ptr);
}

この例では、AllocHGlobalで確保したメモリに、Marshal.Copyでバイト配列をコピーしています。

6-6. P/Invokeでよくある型指定ミス

P/Invokeでは、C/C++側の型とC#側の型を正しく対応させる必要があります。

よくあるミスは、ポインタやハンドルをintで定義してしまうことです。

C#
// 64bit環境で危険な可能性がある例
[DllImport("some.dll")]
private static extern int GetHandle();

ハンドルやポインタを返す関数なら、IntPtrで定義する方が適切です。

C#
[DllImport("some.dll")]
private static extern IntPtr GetHandle();

また、文字列、構造体、配列、コールバック関数などは、それぞれ適切なマーシャリング設定が必要です。IntPtrを使えば何でも解決するわけではありません。

7. メモリ操作でIntPtrを使う方法

7-1. Marshal.AllocHGlobalでメモリを確保する

Marshal.AllocHGlobalを使うと、アンマネージメモリを確保できます。

C#
IntPtr ptr = Marshal.AllocHGlobal(256);

この戻り値がIntPtrです。つまり、確保されたメモリ領域の先頭アドレスを表しています。

アンマネージメモリは、C#のGCが自動的に管理する通常のオブジェクトとは異なります。そのため、使い終わったら必ず解放する必要があります。

7-2. Marshal.FreeHGlobalでメモリを解放する

Marshal.AllocHGlobalで確保したメモリは、Marshal.FreeHGlobalで解放します。

C#
Marshal.FreeHGlobal(ptr);

解放を忘れると、メモリリークの原因になります。

また、解放後のIntPtrには以前のアドレス値が残っていることがあります。そのアドレスを再び使うと危険なので、解放後はIntPtr.Zeroを代入しておくとわかりやすくなります。

C#
Marshal.FreeHGlobal(ptr);
ptr = IntPtr.Zero;

7-3. Marshal.Copyで配列とメモリをやり取りする

Marshal.Copyを使うと、マネージド配列とアンマネージメモリの間でデータをコピーできます。

C#
byte[] source = { 10, 20, 30, 40 };

IntPtr ptr = Marshal.AllocHGlobal(source.Length);

try
{
Marshal.Copy(source, 0, ptr, source.Length);

byte[] destination = new byte[source.Length];
Marshal.Copy(ptr, destination, 0, destination.Length);

Console.WriteLine(string.Join(", ", destination));
}
finally
{
Marshal.FreeHGlobal(ptr);
}

このコードでは、バイト配列をアンマネージメモリへコピーし、その後、再び別のバイト配列へ読み戻しています。

7-4. メモリリークを防ぐための注意点

アンマネージメモリを扱うときにもっとも注意すべきなのは、解放漏れです。

通常のC#オブジェクトであれば、不要になったタイミングでGCが回収します。しかし、Marshal.AllocHGlobalで確保したメモリはGCの管理外です。

そのため、次のようなコードは危険です。

C#
void BadExample()
{
IntPtr ptr = Marshal.AllocHGlobal(100);

// 途中で例外が発生するとFreeHGlobalが呼ばれない
DoSomething(ptr);

Marshal.FreeHGlobal(ptr);
}

DoSomethingで例外が発生すると、FreeHGlobalが呼ばれず、メモリが解放されない可能性があります。

7-5. try-finallyで安全に解放するサンプルコード

アンマネージメモリを確実に解放するには、try-finallyを使います。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
static void Main()
{
IntPtr ptr = IntPtr.Zero;

try
{
ptr = Marshal.AllocHGlobal(100);

byte[] data = { 1, 2, 3, 4, 5 };
Marshal.Copy(data, 0, ptr, data.Length);

Console.WriteLine("メモリへコピーしました。");
}
finally
{
if (ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptr);
ptr = IntPtr.Zero;
}
}
}
}

finallyブロックは、例外が発生しても実行されます。そのため、メモリ解放のような重要な処理を置くのに適しています。

8. IntPtrを使うときの注意点とよくあるエラー

8-1. IntPtrはメモリの安全性を保証しない

IntPtrは、ポインタやハンドルを表す値を保持できます。しかし、IntPtrを使ったからといって、メモリ安全性が保証されるわけではありません。

公式ドキュメントでも、IntPtrをポインタやハンドルとして使うことはエラーが起きやすく安全ではないと説明されており、ハンドルにはSafeHandleを使うべきケースがあるとされています。

つまり、IntPtrは便利な型ですが、安全なリソース管理を自動で行ってくれる型ではありません。

8-2. 解放済みメモリを参照してしまう問題

アンマネージメモリを解放した後、そのIntPtrを使い続けると危険です。

C#
IntPtr ptr = Marshal.AllocHGlobal(100);
Marshal.FreeHGlobal(ptr);

// この後にptrを使うのは危険

解放済みメモリへのアクセスは、予測不能な動作やクラッシュにつながる可能性があります。

解放後は、次のようにIntPtr.Zeroを代入しておくと、誤使用に気づきやすくなります。

C#
Marshal.FreeHGlobal(ptr);
ptr = IntPtr.Zero;

8-3. 32bit・64bit変換によるオーバーフロー

64bit環境では、IntPtrの値が32bit整数に収まらない可能性があります。

C#
int value = ptr.ToInt32();

このようなコードは、64bit環境でオーバーフローの原因になることがあります。

32bitと64bitの両方で動くコードでは、なるべくIntPtrのまま扱い、必要な場合はToInt64を使います。

C#
long value = ptr.ToInt64();

8-4. ハンドルの解放漏れ

Windows APIなどから取得したハンドルは、使い終わったあとに解放が必要な場合があります。

たとえば、あるAPIで取得したハンドルを、別のAPIで閉じる必要がある場合があります。この解放処理を忘れると、OSリソースのリークにつながります。

ただし、すべてのハンドルを自分で解放するわけではありません。APIによっては、取得したハンドルを解放してはいけない場合もあります。

そのため、ハンドルを受け取るP/Invokeを書くときは、取得元APIのドキュメントで「誰が解放するのか」「どの関数で解放するのか」を確認する必要があります。

8-5. GC管理外のリソースに注意する

IntPtrで扱うリソースの多くは、GCの管理外です。

通常のC#オブジェクトであれば、参照されなくなったあとGCによって回収されます。しかし、OSハンドルやアンマネージメモリは、GCが中身まで理解して自動解放してくれるとは限りません。

そのため、IntPtrを使うコードでは、次のような設計が重要です。

リソースを確保した場所を明確にすること、解放する責任を明確にすること、例外が発生しても解放されるようにすることです。

8-6. 例外が発生しても確実に解放する設計

IntPtrを使うコードでは、例外発生時の解放漏れを防ぐ設計が重要です。

基本はtry-finallyです。

C#
IntPtr ptr = IntPtr.Zero;

try
{
ptr = Marshal.AllocHGlobal(100);
// メモリを使う処理
}
finally
{
if (ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptr);
}
}

より本格的には、IDisposableを実装したクラスでラップしたり、ハンドルであればSafeHandleを使ったりする方法があります。

9. IntPtrよりSafeHandleを使うべきケース

9-1. SafeHandleとは何か

SafeHandleは、OSハンドルなどのアンマネージリソースを安全に管理するためのクラスです。

IntPtrは単にハンドル値を保持するだけですが、SafeHandleはリソースの解放処理を組み込めます。そのため、例外が発生した場合や、解放忘れが起きやすい場面で安全性を高められます。

MicrosoftのIntPtrドキュメントでも、ハンドルを表す場合にはSafeHandleを使うべきと説明されています。

9-2. IntPtrだけでハンドル管理するリスク

IntPtrだけでハンドルを管理すると、次のようなリスクがあります。

ハンドルを閉じ忘れる、同じハンドルを二重に解放する、例外発生時に解放処理が飛ばされる、無効なハンドルを使い続ける、所有権がわかりにくくなるといった問題です。

たとえば、次のようなコードでは、途中で例外が発生するとCloseHandleが呼ばれない可能性があります。

C#
IntPtr handle = OpenSomething();

DoSomething(handle);

CloseHandle(handle);

このようなコードは、try-finallyで守る必要があります。

9-3. SafeHandleを使うメリット

SafeHandleを使うメリットは、リソース解放の安全性を高められることです。

SafeHandleは、ハンドルが無効かどうかを判定する仕組みや、解放処理をカプセル化する仕組みを提供します。これにより、IntPtrを直接扱うよりも、保守しやすく安全なコードを書きやすくなります。

.NETには、ファイルハンドル向けのSafeFileHandleなど、用途に応じた派生クラスも用意されています。

9-4. IntPtrとSafeHandleの使い分け

IntPtrSafeHandleは、どちらか一方だけを使うというより、目的に応じて使い分けます。

一時的にポインタ値を受け渡すだけなら、IntPtrで十分な場合があります。たとえば、ウィンドウハンドルを取得して、すぐ別のAPIに渡すだけのようなケースです。

一方で、取得したハンドルを自分のコードで保持し、あとで解放する責任がある場合は、SafeHandleを使う方が安全です。

つまり、所有権を持たない一時的な値ならIntPtr、所有権を持って解放責任があるリソースならSafeHandleを検討する、という考え方が実用的です。

9-5. 初心者はどちらを使うべきか

初心者は、可能であればSafeHandleを使う方が安全です。

IntPtrは低レベルな値をそのまま扱うため、解放漏れや誤使用のリスクがあります。特に、OSハンドルを長く保持する設計では、SafeHandleを使った方がミスを減らせます。

ただし、学習のためにはIntPtrの意味を理解しておくことも重要です。P/Invokeやネイティブ連携のサンプルではIntPtrが頻繁に登場するため、IntPtr.Zero、32bitと64bitの違い、解放責任の考え方は押さえておきましょう。

10. IntPtrに関するよくある質問

10-1. IntPtrはポインタですか?

厳密には、IntPtrはポインタそのものではなく、ポインタと同じサイズの整数型です。

ただし、実際の用途としては、ポインタやハンドルを表すためによく使われます。C#のunsafeポインタとは別物ですが、相互に変換されることもあります。

初心者向けには、「ポインタやハンドルの値を入れるための型」と理解するとよいでしょう。

10-2. IntPtrとUIntPtrの違いは何ですか?

IntPtrは符号付きのネイティブサイズ整数です。UIntPtrは符号なしのネイティブサイズ整数です。

IntPtrは負の値も表せますが、UIntPtrは0以上の値を表します。

C#でWindows APIやP/Invokeを扱う場合は、IntPtrの方をよく見かけます。ただし、C/C++側の型が符号なしポインタサイズ整数を要求している場合は、UIntPtrが使われることもあります。

10-3. IntPtr.Zeroは0と同じですか?

値としては0を表しますが、IntPtr.ZeroIntPtr型の0です。

C#
IntPtr ptr = IntPtr.Zero;

単なる整数の0ではなく、ポインタやハンドルが無効であることを表すために使われます。

比較するときは、次のように書くのが一般的です。

C#
if (ptr == IntPtr.Zero)
{
Console.WriteLine("ゼロです。");
}

IntPtr.Zeronullとは異なります。IntPtrは構造体なので、通常はnullではなくIntPtr.Zeroで判定します。

10-4. IntPtrをstringに変換できますか?

IntPtrの数値表現を文字列にすることはできます。ToStringを使えば、現在のIntPtrの数値を文字列表現に変換できます。Microsoftのドキュメントでも、IntPtr.ToStringは現在のIntPtrオブジェクトの数値を等価な文字列表現へ変換するメソッドとして説明されています。

C#
IntPtr ptr = new IntPtr(12345);
string text = ptr.ToString();

Console.WriteLine(text);

ただし、これはあくまでIntPtrの数値を文字列化するだけです。

もしIntPtrがネイティブメモリ上の文字列を指している場合は、Marshal.PtrToStringAnsiMarshal.PtrToStringUniなどを使って文字列を読み取ることがあります。

C#
string? text = Marshal.PtrToStringUni(ptr);

この場合、ポインタが本当に有効な文字列を指している必要があります。

10-5. IntPtrは普段のC#開発で必要ですか?

多くのC#開発では、IntPtrを直接使う必要はありません。

Webアプリ、業務システム、通常のデスクトップアプリ、API開発などでは、IntPtrを意識しなくても開発できます。

一方で、Windows API、ネイティブDLL、ハードウェア制御、画像処理ライブラリ、音声処理ライブラリ、アンマネージメモリ操作などを扱う場合には、IntPtrが必要になることがあります。

つまり、IntPtrは日常的な型というより、ネイティブ連携や低レベル処理で必要になる型です。

10-6. unsafeを使わなくてもIntPtrは使えますか?

はい、unsafeを使わなくてもIntPtrは使えます。

たとえば、次のコードはunsafeなしで書けます。

C#
IntPtr ptr = IntPtr.Zero;
IntPtr memory = Marshal.AllocHGlobal(100);
Marshal.FreeHGlobal(memory);

P/Invokeでハンドルを受け取る場合も、unsafeなしで書けることが多いです。

C#
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();

ただし、IntPtrが指すメモリの中身を直接ポインタとして操作する場合は、unsafeコードが必要になることがあります。初心者は、まずMarshalクラスを使った安全寄りの方法から学ぶのがおすすめです。

まとめ

IntPtrは、C#でポインタやハンドルの値を扱うために使われる特殊な型です。通常のC#アプリでは頻繁に使うものではありませんが、Windows API、P/Invoke、ネイティブDLL、アンマネージメモリ操作では重要な役割を持ちます。

IntPtrを理解するときのポイントは、ポインタと同じサイズになる整数型であること、32bitでは4バイト、64bitでは8バイトになること、無効な値の判定にはnullではなくIntPtr.Zeroを使うことです。

また、IntPtrは安全なメモリ管理を自動で行ってくれる型ではありません。アンマネージメモリを確保した場合は必ず解放し、ハンドルを扱う場合は解放責任を意識する必要があります。

特に、ハンドルを長く保持する場合や解放処理が重要な場合は、IntPtrを直接管理するのではなく、SafeHandleの利用を検討しましょう。

C#初心者にとってIntPtrは少し難しく見える型ですが、「C#とネイティブな世界をつなぐための型」と考えると理解しやすくなります。P/Invokeやメモリ操作に進む前に、IntPtr.ZeroIntPtr.SizeToInt64Marshal.AllocHGlobalMarshal.FreeHGlobalの基本を押さえておくと、安全に扱いやすくなります。