C#のIntPtrとは?ポインタ・ハンドル・IntPtr.Zeroの使い方を初心者向けに解説

はじめに

C#でWindows APIやC/C++ライブラリを呼び出していると、IntPtrという型がよく登場します。たとえば、ウィンドウハンドルを取得する、アンマネージドメモリを確保する、ネイティブDLLにポインタを渡す、といった場面です。

初心者にとっては、IntPtr、ポインタ、ハンドル、IntPtr.Zerounsafeなどの用語が一気に出てくるため、少し難しく感じるかもしれません。

結論から言うと、IntPtrは「OSや実行環境に合わせたサイズの整数として、ポインタやハンドルの値を扱うための型」です。通常のC#アプリ開発では頻繁に使うものではありませんが、Windows API、P/Invoke、アンマネージドメモリ、ネイティブライブラリ連携では重要な役割を持ちます。

この記事では、C#のIntPtrとは何か、ポインタやハンドルとの違い、IntPtr.Zeroの使い方、基本的なコード例、注意点まで初心者向けにわかりやすく解説します。

1. C#のIntPtrとは?初心者向けに役割をわかりやすく解説

IntPtrは、C#で「ポインタの値」や「OSが返すハンドル値」を安全に受け渡しするためによく使われる型です。

C#は基本的にメモリ管理を.NETランタイムに任せる「安全な言語」です。そのため、C/C++のようにメモリアドレスを直接操作する場面は多くありません。しかし、Windows APIやネイティブDLLと連携するときには、C#の外側にあるメモリやOSリソースを表す値を扱う必要があります。

その橋渡しとして使われるのがIntPtrです。

1-1. IntPtrは「ポインタサイズの整数」を扱うための型

IntPtrは、名前のとおり「Integer Pointer」を意味する型です。Microsoftの公式ドキュメントでも、IntPtrにはInt32Int64void*からインスタンスを作るコンストラクターがあり、ZeroSizeAddSubtractToInt32ToInt64ToPointerなどのメンバーが用意されています。

イメージとしては、次のような値を入れるための箱です。

C#
IntPtr pointerValue;
IntPtr windowHandle;
IntPtr nativeMemory;

ここで大切なのは、IntPtr自体が「メモリの中身」ではなく、「メモリ位置やハンドルを表す数値」を保持する型だということです。

たとえば、住所が書かれたメモを持っているとしても、そのメモ自体が家ではありません。同じように、IntPtrはメモリ上のアドレス値を持つことはできますが、IntPtrそのものが参照先のデータを管理しているわけではありません。

1-2. 32bit環境と64bit環境でサイズが変わる理由

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

32bitプロセスでは、ポインタサイズは通常4バイトです。そのため、IntPtr.Size4になります。

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

C#
Console.WriteLine(IntPtr.Size);

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

ポインタやハンドルは、OSやプロセスのビット数に合わせたサイズで扱う必要があります。もし64bit環境で本来8バイト必要な値をintの4バイトに入れてしまうと、値が途中で切り捨てられ、不具合やクラッシュの原因になります。

IntPtrを使えば、32bitでは32bitサイズ、64bitでは64bitサイズとして扱えるため、環境差を吸収しやすくなります。

1-3. intやlongではなくIntPtrを使うべき場面

「64bit環境も考えるならlongでよいのでは?」と思うかもしれません。しかし、ポインタやハンドルを表す値にはIntPtrを使うのが基本です。

intは常に32bitです。64bitプロセスではサイズが足りない可能性があります。

longは常に64bitです。32bit環境でも使えますが、「これは普通の数値なのか、ポインタサイズの値なのか」がコード上でわかりにくくなります。

一方、IntPtrは「ポインタサイズの値である」ことを型として表現できます。つまり、単なる数値ではなく、ネイティブAPIとの境界で扱うアドレスやハンドルであることが読み手に伝わります。

たとえば、Windows APIのハンドルを受け取る場合は、次のようにintではなくIntPtrを使います。

C#
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName);

このように書くことで、32bit環境でも64bit環境でも適切なサイズでハンドル値を受け取れます。

1-4. C#でIntPtrがよく登場する代表的なケース

IntPtrは、主に次のような場面で登場します。

Windows APIをDllImportで呼び出す場面では、ウィンドウハンドル、ファイルハンドル、デバイスコンテキスト、モジュールハンドルなどをIntPtrで受け取ることがあります。

C/C++で作られたネイティブライブラリと連携する場面では、関数の引数や戻り値にポインタが含まれることがあり、その受け渡しにIntPtrを使います。

Marshal.AllocHGlobalなどでアンマネージドメモリを確保する場面でも、確保されたメモリ位置を表す値としてIntPtrが返されます。Marshal.AllocHGlobalはプロセスのアンマネージドメモリからメモリを割り当て、その戻り値は新しく確保されたメモリへのポインタであり、FreeHGlobalで解放する必要があります。

また、GCHandleSafeHandle、COM連携、画像処理ライブラリ、デバイス制御ライブラリなどでもIntPtrが使われることがあります。

2. IntPtrが必要になる理由:C#とアンマネージドコードの関係

IntPtrを理解するには、C#の「マネージドコード」と、C/C++やOS APIなどの「アンマネージドコード」の違いを知っておく必要があります。

2-1. マネージドコードとアンマネージドコードの違い

マネージドコードとは、.NETランタイムの管理下で実行されるコードです。C#で通常書くコードの多くはマネージドコードです。

マネージドコードでは、オブジェクトのメモリ確保や解放はガベージコレクションによって管理されます。たとえば、次のようにnewで作ったオブジェクトは、不要になったタイミングで.NETが回収します。

C#
var list = new List<string>();

一方、アンマネージドコードとは、.NETランタイムの管理外で実行されるコードです。代表例は、C/C++で作られたDLL、Windows API、OSリソース、ネイティブメモリなどです。

アンマネージドコードでは、メモリやハンドルの解放を開発者が明示的に行う必要がある場合があります。C#側からそれらを扱うときに、アドレス値やハンドル値を表現する型としてIntPtrが必要になります。

2-2. Windows APIやC/C++ライブラリとの連携で使われる

Windows APIには、C言語向けに設計された関数が多くあります。そのため、引数や戻り値にポインタ、ハンドル、構造体へのポインタ、文字列バッファへのポインタなどが登場します。

C#からWindows APIを呼び出す場合、C#の型とC/C++側の型を対応させる必要があります。

たとえば、C/C++側のHWNDHANDLEHINSTANCEなどは、C#では多くの場合IntPtrとして表現されます。

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

この例では、hWndはウィンドウハンドルを表します。C#側ではIntPtrとして受け取り、Windows APIに渡しています。

2-3. DllImportでIntPtrが使われる理由

DllImportは、C#からアンマネージドDLL内の関数を呼び出すために使う属性です。Microsoftの公式ドキュメントでは、DllImportAttributeはアンマネージドDLLからエクスポートされた関数を静的エントリポイントとして公開するための属性と説明されています。

たとえば、次のコードはuser32.dllMessageBox関数をC#から呼び出す例です。

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

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);

static void Main()
{
MessageBox(IntPtr.Zero, "こんにちは", "C#からWindows API", 0);
}
}

MessageBoxの第1引数hWndは、親ウィンドウのハンドルを表します。親ウィンドウを指定しない場合は、IntPtr.Zeroを渡します。

このように、DLLの関数定義にポインタやハンドルが含まれる場合、C#側のシグネチャではIntPtrを使うことがよくあります。

2-4. 安全なC#コードだけでは扱えない領域を橋渡しする型

C#の通常のコードでは、メモリアドレスを直接扱う必要はほとんどありません。これは安全性の面では大きなメリットです。

しかし、低レベルなOS機能、既存のC/C++ライブラリ、ハードウェア制御、画像処理、音声処理、ゲームエンジン、古いDLL資産などを利用する場合は、どうしてもマネージドコードの外側にある値を扱う必要があります。

IntPtrは、その境界を橋渡しするための型です。

ただし、IntPtrを使ったからといって、自動的に安全になるわけではありません。IntPtrはあくまでアドレス値やハンドル値を保持するだけです。指している先が有効か、解放が必要か、どのタイミングで使えるかは、開発者が理解して管理する必要があります。

3. IntPtrとポインタの違い

IntPtrを学ぶときに混同しやすいのが、C#のポインタとの違いです。

IntPtrはポインタに関連する型ですが、C#のポインタそのものとは違います。

3-1. C#のポインタとは何か

C#にも、C/C++のようなポインタを扱う機能があります。ただし、通常のC#コードでは使えず、unsafeコンテキストが必要です。

C#
unsafe
{
int value = 123;
int* p = &value;

Console.WriteLine(*p);
}

このコードでは、int*がポインタ型です。&valueで変数のアドレスを取得し、*pでポインタの指す値にアクセスしています。

C#のunsafeコードでは、ポインタ型、ポインタ演算、固定バッファなどを扱えます。公式ドキュメントでも、unsafeコードではポインタ型を使い、ポインタが指すデータを操作できることが説明されています。

3-2. IntPtrはポインタそのものではなくアドレス値を表す型

IntPtrは、ポインタと同じような値を保持できますが、C#のポインタ型ではありません。

C#
IntPtr ptr = new IntPtr(123456);

このコードは、123456という数値をIntPtrに入れているだけです。これだけでは、その値が本当に有効なメモリアドレスなのか、ハンドルなのか、ただの数値なのかはわかりません。

一方、ポインタ型は次のように型情報を持ちます。

C#
int* p;
byte* buffer;

int*は「intを指すポインタ」、byte*は「byteを指すポインタ」です。

IntPtrは型付きポインタではなく、ポインタサイズの整数です。そのため、IntPtrから直接*ptrのように参照先の値を読むことはできません。

3-3. unsafeコードとIntPtrの違い

IntPtrを使うだけなら、通常はunsafeは不要です。

C#
IntPtr handle = IntPtr.Zero;

このコードは安全なC#コードとしてコンパイルできます。

一方、C#のポインタ型を使うにはunsafeが必要です。

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

つまり、IntPtrは「ポインタ値を安全なC#コード内で保持するための型」と考えるとわかりやすいです。

ただし、IntPtrを使ってアンマネージドメモリを読み書きしたり、ポインタに変換したりする場合は、危険な操作になります。IntPtr自体は安全なコードで使えても、その先の扱いは慎重に行う必要があります。

3-4. IntPtrをポインタに変換する場合の注意点

IntPtrToPointerメソッドでポインタに変換できます。ただし、この操作にはunsafeが必要です。

C#
unsafe
{
IntPtr ptr = GetSomePointer();

void* rawPointer = ptr.ToPointer();

// 必要に応じて型付きポインタに変換する
byte* bytePointer = (byte*)rawPointer;
}

ここで重要なのは、ptrが本当に有効なメモリアドレスを表している必要があることです。

すでに解放されたメモリ、間違ったアドレス、別の型として解釈してはいけないアドレスをポインタに変換して参照すると、例外、アクセス違反、データ破損、プロセス終了などにつながる可能性があります。

3-5. 初心者が混同しやすいポイント

初心者が混同しやすいポイントは、次の3つです。

1つ目は、IntPtrはポインタそのものではなく、ポインタやハンドルの値を入れるための型だという点です。

2つ目は、IntPtrを使うだけならunsafeは不要ですが、ポインタとして参照する場合はunsafeが必要になることです。

3つ目は、IntPtrは参照先のメモリやリソースの寿命を管理してくれないことです。IntPtrに値が入っていても、その先が有効とは限りません。

IntPtrは便利ですが、あくまで「値を入れる入れ物」です。メモリの確保、解放、有効性、所有権は別に考える必要があります。

4. IntPtrとハンドルの関係

IntPtrは、ポインタだけでなく「ハンドル」を表すためにもよく使われます。

Windows APIを扱う場合、IntPtrはむしろハンドルとして登場することが多いです。

4-1. ハンドルとは何か

ハンドルとは、OSが管理しているリソースを識別するための値です。

たとえば、Windowsでは次のようなものにハンドルがあります。

ウィンドウを識別するウィンドウハンドル、ファイルを識別するファイルハンドル、プロセスを識別するプロセスハンドル、スレッドを識別するスレッドハンドル、アイコンやブラシなどのGDIオブジェクトを識別するハンドルなどです。

ハンドルは、C#側から見ると「何らかの数値」のように見えます。しかし、その数値の意味はOSが管理しています。開発者はハンドルを使ってAPIを呼び出し、対象のリソースを操作します。

4-2. ウィンドウハンドルやファイルハンドルでIntPtrが使われる理由

Windows APIのハンドルは、実行環境によってサイズが変わる可能性があります。そのため、C#側でintとして受け取るのは危険です。

たとえば、ウィンドウハンドルを取得するFindWindowは、C#では次のように定義できます。

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

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr FindWindow(
string? lpClassName,
string? lpWindowName);

static void Main()
{
IntPtr hWnd = FindWindow(null, "無題 - メモ帳");

if (hWnd == IntPtr.Zero)
{
Console.WriteLine("ウィンドウが見つかりませんでした");
}
else
{
Console.WriteLine($"ウィンドウハンドル: {hWnd}");
}
}
}

この例では、戻り値のIntPtrがウィンドウハンドルです。IntPtr.Zeroであれば、対象のウィンドウが見つからなかったと判断しています。

4-3. HWND・HANDLE・HINSTANCEとIntPtrの関係

Windows APIには、HWNDHANDLEHINSTANCEHMODULEHICONなど、さまざまなハンドル型があります。

C/C++側では型名が分かれていますが、C#のP/Invokeでは多くの場合IntPtrとして表現します。

たとえば、次のような対応になります。

HWNDはウィンドウハンドルで、C#ではIntPtrとして扱うことが多いです。

HANDLEは汎用ハンドルで、ファイル、プロセス、スレッドなどを表すことがあります。

HINSTANCEHMODULEは、アプリケーションインスタンスやモジュールを表すハンドルです。

ただし、すべてを機械的にIntPtrにすればよいわけではありません。ファイルハンドルやプロセスハンドルのように解放が必要なものは、可能であればSafeHandleやその派生型を使う方が安全です。

4-4. ハンドルをintで受け取ってはいけない理由

ハンドルをintで受け取ってはいけない最大の理由は、64bit環境で値が入りきらない可能性があるからです。

悪い例は次のようなコードです。

C#
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int FindWindow(string? lpClassName, string? lpWindowName);

このコードは、戻り値をintで受け取っています。32bit環境では動作する場合があっても、64bit環境ではハンドル値が切り詰められる可能性があります。

正しくは次のようにします。

C#
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName);

C#でWindows APIを定義するときは、元のAPI定義を確認し、ポインタやハンドルに相当するものはIntPtrまたは適切なSafeHandleで表現することが重要です。

4-5. ハンドルの解放漏れに注意する

ハンドルはOSリソースです。取得したハンドルの種類によっては、使い終わったあとに明示的な解放が必要です。

たとえば、CreateFileで取得したファイルハンドル、OpenProcessで取得したプロセスハンドル、GDIオブジェクトのハンドルなどは、対応する解放処理が必要になります。

IntPtrでハンドルを保持しているだけでは、自動的にハンドルが閉じられるわけではありません。解放を忘れると、ハンドルリークが発生します。

そのため、解放が必要なハンドルにはSafeHandleを使うことが推奨されます。SafeHandleはOSハンドルをラップするためのクラスで、リソースの安全な解放に役立ちます。Microsoftのドキュメントでも、SafeHandleはハンドルリソースのクリティカルファイナライズを提供し、OSリソースの安全な扱いに役立つと説明されています。

5. IntPtr.Zeroとは?nullとの違いと使い方

IntPtrを使うときに必ず出てくるのがIntPtr.Zeroです。

IntPtr.Zeroは、簡単に言えば「値が0のIntPtr」です。ポインタやハンドルが存在しないことを表すためによく使われます。

5-1. IntPtr.Zeroは「ゼロ値のポインタ」を表す

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

次の2つは、値としては同じ意味になります。

C#
IntPtr p1 = IntPtr.Zero;
IntPtr p2 = new IntPtr(0);

ただし、通常はnew IntPtr(0)よりもIntPtr.Zeroを使う方が読みやすく、意図も明確です。

5-2. nullではなくIntPtr.Zeroを使う場面

IntPtrは値型です。そのため、通常のIntPtr変数にはnullを代入できません。

C#
IntPtr ptr = null; // コンパイルエラー

代わりに、ゼロ値を表すIntPtr.Zeroを使います。

C#
IntPtr ptr = IntPtr.Zero;

参照型で「何も参照していない」状態を表すのがnullなら、IntPtrで「ポインタやハンドルがない」状態を表すのがIntPtr.Zeroです。

ただし、IntPtr.Zeronullは同じものではありません。公式ドキュメントでも、IntPtr.ZeroはWindows APIのポインタやnull相当の値として使われることはありますが、C#のnullそのものと同等ではないと説明されています。

5-3. Windows API呼び出しでIntPtr.Zeroを渡す例

MessageBoxの親ウィンドウを指定しない場合、IntPtr.Zeroを渡せます。

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

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);

static void Main()
{
MessageBox(
IntPtr.Zero,
"親ウィンドウなしで表示します",
"IntPtr.Zeroの例",
0);
}
}

この例では、hWndIntPtr.Zeroを渡すことで、親ウィンドウを指定しないことを表しています。

5-4. 戻り値がIntPtr.Zeroかどうかを判定する方法

Windows APIやネイティブ関数の戻り値がIntPtrの場合、失敗時にIntPtr.Zeroが返ることがあります。

C#
IntPtr hWnd = FindWindow(null, "存在しないウィンドウタイトル");

if (hWnd == IntPtr.Zero)
{
Console.WriteLine("取得に失敗しました");
}
else
{
Console.WriteLine("取得に成功しました");
}

IntPtr.Zeroとの比較は、==演算子で書けます。

C#
if (ptr == IntPtr.Zero)
{
// 無効または未設定
}

また、逆に有効な値が入っているかを確認する場合は次のようにします。

C#
if (ptr != IntPtr.Zero)
{
// 何らかの値が入っている
}

5-5. IntPtr.Zeroとnullを同じものとして扱わない理由

IntPtr.Zeronullを混同してはいけない理由は、型の意味が違うからです。

nullは、参照型の変数がオブジェクトを参照していないことを表します。

IntPtr.Zeroは、IntPtrという値型の値が0であることを表します。

たとえば、次の比較は常に直感どおりとは限りません。

C#
IntPtr ptr = IntPtr.Zero;

Console.WriteLine(ptr.Equals(null)); // false

IntPtr.Zeroは「ゼロ値」であり、C#のnullではありません。P/InvokeやWindows APIの文脈ではnullポインタ相当として扱われる場合がありますが、C#の言語仕様上のnullとは区別して考えましょう。

6. IntPtrの基本的な使い方

ここからは、IntPtrの基本的な使い方をコードで確認していきます。

6-1. IntPtr型の変数を宣言する

IntPtr型の変数は、通常の変数と同じように宣言できます。

C#
IntPtr ptr;

初期値を指定する場合は、次のように書きます。

C#
IntPtr ptr = new IntPtr(1234);

ただし、実際の開発では、適当な数値をIntPtrに入れることはほとんどありません。多くの場合、Windows APIやMarshalクラスのメソッドから返ってきた値を受け取ります。

C#
IntPtr hWnd = FindWindow(null, "無題 - メモ帳");

6-2. IntPtr.Zeroで初期化する

未設定の状態を表したい場合は、IntPtr.Zeroで初期化します。

C#
IntPtr handle = IntPtr.Zero;

あとでAPIの戻り値を代入する場合にも、最初はIntPtr.Zeroにしておくと意図がわかりやすくなります。

C#
IntPtr nativeBuffer = IntPtr.Zero;

try
{
nativeBuffer = Marshal.AllocHGlobal(256);

// nativeBufferを使う処理
}
finally
{
if (nativeBuffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(nativeBuffer);
}
}

このように、finallyIntPtr.Zeroチェックをしてから解放すると、確保に失敗した場合でも安全に処理しやすくなります。

6-3. ToInt32・ToInt64で数値に変換する

IntPtrの値は、ToInt32ToInt64で数値に変換できます。

C#
IntPtr ptr = new IntPtr(12345);

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

Console.WriteLine(value32);
Console.WriteLine(value64);

ただし、64bit環境でToInt32を使うと、値が32bitに収まらない場合に問題が起きる可能性があります。アドレス値やハンドル値をログ出力したいだけなら、ToStringToInt64を使う方が安全です。

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

特に、ハンドルやポインタをintにキャストして保存するのは避けましょう。

6-4. AddやSubtractでアドレス計算を行う

IntPtr.Addを使うと、IntPtrにオフセットを加算できます。

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

try
{
IntPtr offsetPtr = IntPtr.Add(basePtr, 10);

Console.WriteLine(basePtr);
Console.WriteLine(offsetPtr);
}
finally
{
Marshal.FreeHGlobal(basePtr);
}

IntPtr.Subtractを使うと、オフセットを減算できます。

C#
IntPtr previousPtr = IntPtr.Subtract(offsetPtr, 10);

ただし、アドレス計算は非常に慎重に行う必要があります。確保したメモリ範囲を超えた位置にアクセスすると、アクセス違反やデータ破壊の原因になります。

6-5. IntPtr.Sizeでポインタサイズを確認する

IntPtr.Sizeを使うと、現在のプロセスにおけるポインタサイズを確認できます。

C#
Console.WriteLine($"IntPtr.Size = {IntPtr.Size}");

if (IntPtr.Size == 4)
{
Console.WriteLine("32bit環境です");
}
else if (IntPtr.Size == 8)
{
Console.WriteLine("64bit環境です");
}

IntPtr.Sizeは、32bit/64bitの違いによる不具合を調査するときにも役立ちます。

たとえば、開発環境では動くのに本番環境で動かない場合、本番が64bitプロセスとして動作しており、intにキャストしていたハンドル値が壊れている、というケースがあります。

7. IntPtrを使った実践コード例

ここでは、実際にIntPtrが登場するコード例を紹介します。

7-1. Windows APIをDllImportで呼び出す例

まずは、MessageBoxを表示する簡単な例です。

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

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);

static void Main()
{
MessageBox(
IntPtr.Zero,
"C#からWindows APIを呼び出しています",
"DllImportの例",
0);
}
}

hWndには親ウィンドウのハンドルを指定します。今回は親ウィンドウを指定しないため、IntPtr.Zeroを渡しています。

7-2. FindWindowでウィンドウハンドルを取得する例

次に、FindWindowを使ってウィンドウハンドルを取得する例です。

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

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr FindWindow(
string? lpClassName,
string? lpWindowName);

static void Main()
{
IntPtr hWnd = FindWindow(null, "無題 - メモ帳");

if (hWnd == IntPtr.Zero)
{
Console.WriteLine("ウィンドウが見つかりませんでした");
}
else
{
Console.WriteLine($"ウィンドウハンドルを取得しました: {hWnd}");
}
}
}

FindWindowの戻り値は、見つかったウィンドウのハンドルです。見つからなかった場合はIntPtr.Zeroが返るため、必ずチェックします。

7-3. 取得したハンドルがIntPtr.Zeroか判定する例

ハンドルを使う前には、IntPtr.Zeroかどうかを確認する習慣をつけましょう。

C#
IntPtr hWnd = FindWindow(null, "無題 - メモ帳");

if (hWnd == IntPtr.Zero)
{
Console.WriteLine("ハンドルが無効です");
return;
}

// ここから先でhWndを使う
Console.WriteLine("有効なハンドルです");

IntPtr.ZeroのままAPIに渡すと、APIによっては失敗したり、意図しない動作をしたりします。

ただし、すべてのAPIでIntPtr.Zeroが必ずエラーを意味するわけではありません。APIによっては「親なし」「既定値」「すべて対象」などの意味を持つこともあります。必ず呼び出すAPIの仕様を確認しましょう。

7-4. MarshalクラスとIntPtrを組み合わせる例

Marshalクラスを使うと、アンマネージドメモリを確保し、C#のデータをコピーできます。

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

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

try
{
int size = 16;
buffer = Marshal.AllocHGlobal(size);

byte[] data = { 1, 2, 3, 4, 5 };

Marshal.Copy(data, 0, buffer, data.Length);

Console.WriteLine("アンマネージドメモリにデータをコピーしました");
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(buffer);
}
}
}
}

この例では、Marshal.AllocHGlobalでアンマネージドメモリを確保し、そのアドレスをIntPtrで受け取っています。使い終わったらMarshal.FreeHGlobalで必ず解放します。

IntPtrはメモリの場所を表すだけで、解放までは行いません。AllocHGlobalで確保したらFreeHGlobalで解放する、という対応関係を守ることが重要です。

7-5. 構造体や文字列をアンマネージドメモリに渡す例

構造体をアンマネージドメモリにコピーする例です。

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

[StructLayout(LayoutKind.Sequential)]
struct SampleData
{
public int Id;
public double Value;
}

class Program
{
static void Main()
{
SampleData data = new SampleData
{
Id = 1,
Value = 123.45
};

int size = Marshal.SizeOf<SampleData>();
IntPtr ptr = IntPtr.Zero;

try
{
ptr = Marshal.AllocHGlobal(size);

Marshal.StructureToPtr(data, ptr, false);

Console.WriteLine("構造体をアンマネージドメモリにコピーしました");
}
finally
{
if (ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptr);
}
}
}
}

文字列をアンマネージドメモリに変換する例もあります。

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

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

try
{
strPtr = Marshal.StringToHGlobalUni("こんにちは");

Console.WriteLine("文字列をアンマネージドメモリに配置しました");
Console.WriteLine(strPtr);
}
finally
{
if (strPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(strPtr);
}
}
}
}

このようなコードでは、例外が発生しても解放処理が実行されるようにtry-finallyを使うことが大切です。

8. IntPtr使用時の注意点とよくあるエラー

IntPtrは便利ですが、扱いを間違えると危険です。特に、メモリやOSリソースに関わるため、通常のC#コードよりも慎重に実装する必要があります。

8-1. 32bit・64bit環境の違いによる不具合

もっとも多いトラブルの1つが、32bit環境では動くのに64bit環境で動かないケースです。

原因としてよくあるのは、ポインタやハンドルをintで扱っていることです。

C#
int handle = GetSomeHandle(); // 悪い例

64bit環境では、ハンドル値が32bitに収まらない可能性があります。正しくはIntPtrを使います。

C#
IntPtr handle = GetSomeHandle(); // 良い例

P/Invokeのシグネチャを定義するときは、元のC/C++の型がポインタやハンドルなのか、単なる整数なのかを確認しましょう。

8-2. intへのキャストで値が欠落する問題

IntPtrintにキャストすると、64bit環境で値が欠落する可能性があります。

C#
IntPtr ptr = GetPointer();

int value = ptr.ToInt32(); // 64bit環境では危険な場合がある

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

C#
long value = ptr.ToInt64();

ただし、そもそもポインタやハンドルを数値として加工する必要がある場面は多くありません。必要がなければ、IntPtrのまま扱いましょう。

8-3. メモリ解放忘れによるメモリリーク

Marshal.AllocHGlobalMarshal.StringToHGlobalUni、ネイティブAPIによるメモリ確保などを使った場合、解放を忘れるとメモリリークが発生します。

悪い例です。

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

// 処理

// FreeHGlobalを呼んでいない

良い例です。

C#
IntPtr ptr = IntPtr.Zero;

try
{
ptr = Marshal.AllocHGlobal(1024);

// 処理
}
finally
{
if (ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptr);
}
}

アンマネージドメモリは、C#のガベージコレクションが自動で解放してくれるものではありません。確保したら必ず解放する、という原則を守りましょう。

8-4. 無効なアドレス参照による例外やクラッシュ

IntPtrに入っている値が、常に有効なアドレスとは限りません。

次のようなケースでは危険です。

すでに解放されたメモリのIntPtrを使う。

別のAPIから返されたハンドルを、関係ないAPIに渡す。

IntPtr.Zeroのままメモリアクセスしようとする。

ランダムな整数値をIntPtrに変換してポインタとして使う。

特に、Marshal.PtrToStructureMarshal.Copyunsafeでのポインタ参照などは、渡されたIntPtrが正しいことを前提に動きます。間違ったアドレスを渡すと、例外だけでなくプロセス全体が不安定になる可能性があります。

8-5. IntPtr.Zeroチェックを忘れたときのトラブル

API呼び出しの戻り値がIntPtr.Zeroなのに、そのまま使ってしまうとエラーになります。

C#
IntPtr hWnd = FindWindow(null, "存在しないウィンドウ");

// チェックせずに使うのは危険
SetForegroundWindow(hWnd);

次のようにチェックしてから使いましょう。

C#
IntPtr hWnd = FindWindow(null, "存在しないウィンドウ");

if (hWnd == IntPtr.Zero)
{
Console.WriteLine("ウィンドウが見つかりません");
return;
}

SetForegroundWindow(hWnd);

IntPtr.Zeroチェックは、IntPtrを安全に使うための基本です。

9. IntPtrと関連する型の違い

IntPtrの周辺には、似たような型がいくつかあります。ここでは、UIntPtrnintSafeHandleGCHandleとの違いを整理します。

9-1. IntPtrとUIntPtrの違い

IntPtrは符号付きのポインタサイズ整数です。

UIntPtrは符号なしのポインタサイズ整数です。

簡単に言うと、IntPtrはマイナス値を表現でき、UIntPtrはマイナス値を表現しません。

Windows APIやネイティブAPIでは、SIZE_Tのような符号なしのサイズ値にUIntPtrを使うことがあります。一方、ハンドルや一般的なポインタ値にはIntPtrが使われることが多いです。

ただし、どちらを使うべきかは元のAPI定義によります。C/C++側の型がvoid*HANDLEHWNDならIntPtrSIZE_Tや符号なしポインタサイズ整数ならUIntPtrが候補になります。

9-2. IntPtrとnintの違い

nintは、C#で使えるネイティブサイズの整数型です。

nintは、整数計算としてポインタサイズの値を扱いたいときに便利です。C# 9以降ではnintを使ってネイティブサイズ整数を定義でき、C# 11かつ.NET 7以降ではnintIntPtrのエイリアスとして扱われることが公式ドキュメントで説明されています。

C#
nint value = 100;
Console.WriteLine(value);

ただし、P/Invokeのシグネチャやハンドル表現では、依然としてIntPtrの方がよく使われます。

初心者のうちは、Windows APIやハンドルにはIntPtr、ネイティブサイズの整数計算にはnintと考えるとわかりやすいです。

9-3. IntPtrとSafeHandleの違い

IntPtrは、ハンドル値そのものを保持する型です。

SafeHandleは、ハンドルの解放まで含めて安全に扱うためのラッパーです。

たとえば、ファイルハンドルやプロセスハンドルのように、使い終わったら閉じる必要があるリソースでは、IntPtrだけで持つよりもSafeHandleを使う方が安全です。

IntPtrだけを使う場合、解放処理を自分で忘れずに書く必要があります。

C#
IntPtr handle = OpenSomething();

// 使い終わったらCloseHandleなどを呼ぶ必要がある

SafeHandleを使う場合、usingDisposeと組み合わせて、解放漏れを防ぎやすくなります。

C#
using SafeHandle handle = OpenSomethingSafe();

// スコープを抜けるとDisposeされる

可能であれば、リソース所有権を持つハンドルにはSafeHandleを優先しましょう。

9-4. IntPtrとGCHandleの違い

GCHandleは、マネージドオブジェクトをガベージコレクションから一時的に固定したり、アンマネージドコードに渡したりするために使う型です。

IntPtrはアドレス値やハンドル値を表します。

GCHandleは、マネージドオブジェクトへのハンドルを表します。

たとえば、マネージドオブジェクトをネイティブコード側に渡す必要がある場合、GCHandle.Allocでハンドルを作り、GCHandle.ToIntPtrIntPtrに変換することがあります。

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

class Program
{
static void Main()
{
object obj = "管理対象オブジェクト";

GCHandle handle = GCHandle.Alloc(obj);

try
{
IntPtr ptr = GCHandle.ToIntPtr(handle);

GCHandle restored = GCHandle.FromIntPtr(ptr);
object target = restored.Target!;

Console.WriteLine(target);
}
finally
{
handle.Free();
}
}
}

この場合、IntPtrGCHandleを表す値として使われています。ただし、GCHandleも使い終わったらFreeする必要があります。

9-5. 目的別にどの型を選ぶべきか

目的別に整理すると、次のようになります。

Windows APIのHWNDや一般的なポインタ値を受け取るなら、IntPtrを使います。

符号なしのポインタサイズ整数やサイズ値を扱うなら、UIntPtrを検討します。

C#コード内でネイティブサイズの整数計算をしたいなら、nintを使います。

解放が必要なOSハンドルを所有するなら、SafeHandleを優先します。

マネージドオブジェクトを固定したり、アンマネージドコードに参照として渡したりするなら、GCHandleを使います。

すべてをIntPtrで済ませようとすると、解放漏れや型の意味の混乱につながります。目的に合った型を選ぶことが重要です。

10. IntPtrを安全に使うためのベストプラクティス

IntPtrを使うときは、通常のC#コードよりも一段慎重に設計する必要があります。

ここでは、実務で意識したいベストプラクティスを紹介します。

10-1. 可能ならSafeHandleを優先する

解放が必要なハンドルを扱う場合は、可能であればIntPtrではなくSafeHandleを使いましょう。

IntPtrは値を持つだけなので、スコープを抜けても自動でハンドルを閉じません。

一方、SafeHandleIDisposableと組み合わせて使えるため、usingでリソース管理しやすくなります。

C#
using SafeHandle handle = GetHandle();

// handleを使う

すべてのAPIで簡単にSafeHandleが使えるわけではありませんが、ファイル、プロセス、スレッド、レジストリなど、.NETに既存のSafeHandle派生型がある場合は積極的に利用しましょう。

10-2. ハンドルやメモリは必ず解放する

アンマネージドメモリやOSハンドルは、使い終わったら必ず解放します。

メモリの場合は、確保方法と解放方法を対応させることが重要です。

Marshal.AllocHGlobalで確保したらMarshal.FreeHGlobalで解放します。

Marshal.AllocCoTaskMemで確保したらMarshal.FreeCoTaskMemで解放します。

Windows APIで取得したハンドルは、API仕様に従ってCloseHandleDestroyWindowDeleteObjectなど適切な関数で解放します。

確保と解放の対応を間違えると、メモリリークやクラッシュの原因になります。

10-3. 64bit対応を前提に実装する

現代の環境では、64bit対応を前提に実装するべきです。

ポインタやハンドルをintに保存しない。

ToInt32を安易に使わない。

P/Invokeのシグネチャを32bit前提で書かない。

IntPtr.Sizeを使って環境差を確認する。

Any CPUx86x64のビルド設定によって動作が変わることもあります。特に古いサンプルコードではintでハンドルを受け取っているものもあるため、そのままコピーせず、現在の64bit環境でも正しいか確認しましょう。

10-4. IntPtr.Zeroによるエラーチェックを徹底する

IntPtrを返すAPIでは、戻り値がIntPtr.Zeroかどうかを必ず確認しましょう。

C#
IntPtr ptr = NativeMethod();

if (ptr == IntPtr.Zero)
{
throw new InvalidOperationException("ネイティブAPIの呼び出しに失敗しました");
}

ただし、IntPtr.Zeroが何を意味するかはAPIによって異なります。

失敗を意味する場合もあれば、親ウィンドウなし、既定値、対象なしなどを意味する場合もあります。API仕様を確認したうえで、適切に判定しましょう。

10-5. 不要なポインタ操作を避ける

IntPtrを使う必要がない場面では、無理に使わないことも大切です。

C#の通常の配列、文字列、Span<T>Memory<T>、ストリーム、SafeHandle、既存の.NET APIで済むなら、それらを使う方が安全で保守しやすいです。

IntPtrが必要になるのは、主に次のような場面です。

P/InvokeでネイティブAPIを呼び出す。

アンマネージドメモリを扱う。

OSハンドルを扱う。

C/C++ライブラリと連携する。

これらに該当しない場合は、IntPtrを使わない設計を検討しましょう。

11. C#のIntPtrに関するよくある質問

最後に、IntPtrについて初心者が疑問に思いやすい点をQ&A形式で整理します。

11-1. IntPtrは初心者でも使う必要がある?

通常のC#アプリ開発では、IntPtrを直接使う機会は多くありません。

Webアプリ、業務アプリ、コンソールアプリ、一般的なデスクトップアプリでは、ほとんどの場合.NETの標準APIだけで実装できます。

ただし、Windows API、ネイティブDLL、デバイス制御、画像処理ライブラリ、ゲーム開発、既存C/C++資産との連携を行う場合は、IntPtrが必要になることがあります。

初心者はまず、「普通のC#コードではあまり使わないが、C#の外側と連携するときに必要になる型」と理解しておけば十分です。

11-2. IntPtr.Zeroと0は同じ意味?

値としては、IntPtr.Zeroは0を表します。

C#
IntPtr p1 = IntPtr.Zero;
IntPtr p2 = new IntPtr(0);

Console.WriteLine(p1 == p2); // true

しかし、コードの意味としてはIntPtr.Zeroを使う方が適切です。

0new IntPtr(0)よりも、IntPtr.Zeroの方が「ポインタやハンドルがない状態」を表していることが明確になります。

特に、APIの戻り値チェックでは次のように書くのが一般的です。

C#
if (handle == IntPtr.Zero)
{
// 失敗または未設定
}

11-3. IntPtrをstringやbyte配列に変換できる?

できます。ただし、IntPtrが指している先に何があるかを正しく知っている必要があります。

文字列ポインタであれば、Marshal.PtrToStringUniMarshal.PtrToStringAnsiなどを使えます。

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

バイト配列にコピーする場合は、Marshal.Copyを使えます。

C#
byte[] buffer = new byte[100];
Marshal.Copy(ptr, buffer, 0, buffer.Length);

ただし、ptrが有効なメモリを指していること、読み取るサイズが正しいこと、文字列のエンコーディングが正しいことを確認する必要があります。

不明なIntPtrを無理に文字列や配列に変換するのは危険です。

11-4. IntPtrはガベージコレクションの対象になる?

IntPtr自体は値型なので、通常のC#の値として扱われます。

しかし、IntPtrが指しているアンマネージドメモリやOSハンドルは、ガベージコレクションの対象ではありません。

たとえば、次のコードで確保したメモリは、GCが自動で解放してくれるわけではありません。

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

必ず自分で解放する必要があります。

C#
Marshal.FreeHGlobal(ptr);

つまり、IntPtr変数そのものと、IntPtrが指している外部リソースは別物です。ガベージコレクションが管理するのはマネージドオブジェクトであり、アンマネージドリソースは開発者が管理しなければなりません。

11-5. IntPtrを使うときにunsafeは必要?

IntPtrを宣言したり、APIの引数や戻り値として使ったりするだけなら、unsafeは必要ありません。

C#
IntPtr ptr = IntPtr.Zero;
C#
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);

これらは通常の安全なC#コードです。

ただし、IntPtrをC#のポインタに変換して直接参照する場合はunsafeが必要です。

C#
unsafe
{
void* p = ptr.ToPointer();
}

初心者のうちは、unsafeを使って直接メモリを読むよりも、Marshal.CopyMarshal.PtrToStructureSafeHandleなどの仕組みを使う方が安全です。

まとめ

IntPtrは、C#でポインタサイズの整数を扱うための型です。主に、Windows API、C/C++ライブラリ、アンマネージドメモリ、OSハンドルなど、マネージドコードの外側にある値を扱うときに使われます。

IntPtrは、32bit環境では4バイト、64bit環境では8バイトのように、実行環境に合わせたサイズで値を保持できます。そのため、ポインタやハンドルをintで受け取るよりも安全です。

IntPtr.Zeroは、値が0のIntPtrを表します。ポインタやハンドルがない状態、API呼び出しの失敗、親ウィンドウなしなどを表すためによく使われます。ただし、C#のnullそのものではありません。

また、IntPtrは参照先のメモリやハンドルを自動で管理しません。アンマネージドメモリを確保した場合は必ず解放し、OSハンドルを取得した場合は適切な方法で閉じる必要があります。

実務では、次のポイントを意識しましょう。

ポインタやハンドルにはintではなくIntPtrを使う。

未設定や失敗判定にはIntPtr.Zeroを使う。

64bit環境を前提に実装する。

アンマネージドメモリやハンドルは必ず解放する。

解放が必要なハンドルには可能ならSafeHandleを使う。

不要なポインタ操作やunsafeコードは避ける。

IntPtrは初心者にとって少し難しい型ですが、「C#と外部のネイティブ世界をつなぐための型」と考えると理解しやすくなります。通常のC#開発では頻繁に使いませんが、Windows APIやネイティブ連携を扱うなら、必ず押さえておきたい重要な型です。