このシリーズは全 4 記事で構成されています。
- ① なぜポインターが必要か / unsafe の基本 /
*と&演算子 - ② 本記事 —
->演算子 /stackalloc/fixed/sizeof - ③ 実践例:ビットマップ画像処理
- ④ 実践例:C++ DLL との相互運用
① では、unsafe キーワードの有効化、* と & 演算子の基本、ポインター演算を説明しました。
この記事では次を説明します。
->演算子によるフィールドアクセスstackallocキーワードfixedキーワードによるピン止めsizeofキーワード
-> 演算子によるフィールドアクセス
構造体のポインターがあるとき、フィールドにアクセスするには C++ と同様に -> 演算子が使えます。ptr->Field は (*ptr).Field と等価です。
まず unsafe ブロック内で使える構造体を定義します(参照型である class はポインターで直接扱えません。struct を使います)。
struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public override string ToString() => $"({X}, {Y})";
}
unsafe static void UseArrowOperator()
{
Point pt = new Point(10, 20);
Point* p = &pt;
// -> でフィールドにアクセス
Console.WriteLine($"X = {p->X}"); // X = 10
Console.WriteLine($"Y = {p->Y}"); // Y = 20
// -> で値を書き換える
p->X = 99;
Console.WriteLine(pt); // (99, 20)
// (*p).X と書いても同じ
(*p).Y = 77;
Console.WriteLine(pt); // (99, 77)
}
構造体のネスト
ネストした構造体も連続して -> でたどれます。
struct Rectangle
{
public Point TopLeft;
public Point BottomRight;
public override string ToString()
=> $"[{TopLeft} - {BottomRight}]";
}
unsafe static void NestedArrow()
{
Rectangle rect = new Rectangle
{
TopLeft = new Point(0, 0),
BottomRight = new Point(100, 50)
};
Rectangle* rp = ▭
Console.WriteLine(rp->TopLeft.X); // 0
Console.WriteLine(rp->BottomRight.Y); // 50
// ネストした構造体へのポインター
Point* pp = &rp->TopLeft;
pp->X = 10;
Console.WriteLine(rect); // [(10, 0) - (100, 50)]
}
stackalloc キーワード
stackalloc を使うと、ヒープではなくスタック上にメモリを確保できます。スタック確保はヒープよりも高速で、GC の対象にならないため fixed も不要です。確保されたメモリはメソッドのスタックフレームが解放されると同時に消えます。
unsafe static void UseStackalloc()
{
// スタックに int 5 要素分のメモリを確保
int* buffer = stackalloc int[5];
for (int i = 0; i < 5; i++)
buffer[i] = i * 10;
for (int i = 0; i < 5; i++)
Console.Write($"{buffer[i]} "); // 0 10 20 30 40
Console.WriteLine();
}
Span<T> との組み合わせ(C# 7.2 以降)
C# 7.2 以降では Span<T> と組み合わせると、unsafe なしでも stackalloc が使えます。
// unsafe 不要(Span<T> で受け取る場合)
static void UseStackallocWithSpan()
{
Span<int> buffer = stackalloc int[5];
for (int i = 0; i < 5; i++)
buffer[i] = (i + 1) * 100;
foreach (var v in buffer)
Console.Write($"{v} "); // 100 200 300 400 500
Console.WriteLine();
}
Span<T> で受け取る場合は unsafe ブロックが不要になり、インデックスの範囲チェックも入るため安全です。スタック確保のパフォーマンスが欲しい場合は、この形が推奨されます。
ただし、スタックは容量が限られているため、大きなバッファには使わないことが重要です。目安として数百バイト〜数キロバイト程度が実用的な範囲です。
fixed キーワードによるピン止め
C# の GC はメモリの断片化を防ぐためにオブジェクトをヒープ上で移動することがあります。管理対象のオブジェクト(managed object)へのポインターを取得すると、GC がオブジェクトを移動した瞬間にポインターが無効になります。
fixed ステートメントは GC によるオブジェクトの移動を一時的に禁止し、安全にポインターを固定(ピン止め)します。
unsafe static void UseFixed()
{
int[] numbers = { 10, 20, 30, 40, 50 };
// numbers 配列を GC が移動しないようにピン止めしてポインターを取得
fixed (int* p = numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"p[{i}] = {p[i]}");
}
// ポインター演算でもアクセスできる
int* end = p + numbers.Length;
for (int* ptr = p; ptr < end; ptr++)
Console.Write($"{*ptr} "); // 10 20 30 40 50
Console.WriteLine();
}
// fixed ブロックを抜けるとピン止めが解除される
}
文字列のピン止め
文字列(string)も fixed でピン止めして char* ポインターを取得できます。
unsafe static void FixedString()
{
string text = "Hello, C#!";
fixed (char* p = text)
{
char* ptr = p;
while (*ptr != '\0')
{
Console.Write(*ptr);
ptr++;
}
Console.WriteLine();
}
// Hello, C#!
}
構造体フィールドのピン止め
クラスのフィールドに対しても fixed が使えます。ただし、フィールドが固定サイズバッファ(fixed フィールド)である必要があります。
unsafe struct Pixel
{
public fixed byte Channels[4]; // R, G, B, A の 4 バイト固定バッファ
}
unsafe static void UseFixedBuffer()
{
Pixel pixel;
pixel.Channels[0] = 255; // R
pixel.Channels[1] = 128; // G
pixel.Channels[2] = 0; // B
pixel.Channels[3] = 255; // A
byte* p = pixel.Channels;
Console.WriteLine($"R={p[0]}, G={p[1]}, B={p[2]}, A={p[3]}");
// R=255, G=128, B=0, A=255
}
fixed フィールドを持つ構造体は、インラインで固定サイズの配列を持てるため、ネイティブの相互運用(P/Invoke など)でよく使われます。
fixed を使うべき場面
| 場面 | 理由 |
|---|---|
| マネージド配列を C API に渡す | GC が移動するとアドレスが無効になるため |
| 文字列の文字を高速に走査する | char* で直接アクセスするほうが速い場合がある |
| P/Invoke でバッファを渡す | ネイティブコードはアドレスが変わると困る |
sizeof キーワード
sizeof は型のバイトサイズをコンパイル時に取得します。組み込み型(int、double など)は unsafe なしでも使えます。
// unsafe 不要(組み込み型)
Console.WriteLine(sizeof(int)); // 4
Console.WriteLine(sizeof(long)); // 8
Console.WriteLine(sizeof(double)); // 8
Console.WriteLine(sizeof(char)); // 2
Console.WriteLine(sizeof(bool)); // 1
Console.WriteLine(sizeof(byte)); // 1
ユーザー定義の構造体のサイズを取得するには unsafe が必要です。
struct Point3D
{
public float X, Y, Z;
}
unsafe static void ShowStructSize()
{
Console.WriteLine(sizeof(Point3D)); // 12(float 4 バイト × 3)
}
Marshal.SizeOf との違い
sizeof と似た用途で Marshal.SizeOf がありますが、目的が異なります。
sizeof |
Marshal.SizeOf |
|
|---|---|---|
| 計測対象 | .NET 上の型サイズ | アンマネージドメモリ上のサイズ |
| unsafe 要否 | 組み込み型は不要、構造体は必要 | 不要 |
| パディング | .NET のアラインメントに従う | アンマネージドの規則に従う |
| 主な用途 | ポインター演算、メモリ計算 | P/Invoke、マーシャリング |
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct PackedPoint
{
public byte X; // 1 バイト
public int Y; // 4 バイト
}
unsafe static void CompareSizes()
{
// .NET のアラインメント: X(1) + パディング(3) + Y(4) = 8
Console.WriteLine(sizeof(PackedPoint)); // 8
// Pack=1 指定のアンマネージドサイズ: 1 + 4 = 5
Console.WriteLine(Marshal.SizeOf<PackedPoint>()); // 5
}
まとめ
| トピック | ポイント |
|---|---|
-> 演算子 |
構造体ポインター経由のフィールドアクセス。(*p).Field と等価 |
stackalloc |
スタックにメモリ確保。GC 対象外で高速。Span<T> と組み合わせると unsafe 不要 |
fixed |
GC によるオブジェクト移動を一時停止してポインターを安全に使う |
sizeof |
組み込み型はどこでも使用可。ユーザー定義型は unsafe が必要 |
C++ 経験者にとって概念自体はなじみ深いはずですが、C# では GC との共存が常に意識すべき点です。fixed は GC のスループットを一時的に下げる可能性があるため、使用範囲はできる限り狭く保つのが望ましいです。多くの場合、Span<T> や ref 構造体を使えば unsafe なしに同等のパフォーマンスが得られます。
次は実践編です。③ ビットマップ画像処理 ではポインターで画像処理を高速化する例、④ C++ DLL との相互運用 では P/Invoke でポインターを渡す例を紹介します。