スタックとヒープで学んだ事をアウトプットしようと思っていたのですが、ポインター操作が必要になりましたので、その前にポインターについて学んだ事をアウトプットしていきたいと思います。
その中でも簡単で基本的と思われる部分に関してアウトプットします。
ここで扱っているプログラムは.NET9を使って確認しています。
確認前の設定
今回、記事ではポインターを操作するため、unsafe
を使用します。
unsafe
を使用するためには設定ファイルを変更する必要があります。
ビルド→全般→アンセーフコードにチェックを入れます。
アドレス取得演算子
&
演算子を使用することで、アドレスを取得することが可能です。
unsafe
{
int x;
Console.WriteLine($"x = {(long)&x:X}");
}
// x = F31D57ED18
ポインター型の変数宣言
型*
を使用することでポインター変数を宣言できます。
unsafe
{
int x;
int* p = &x;
Console.WriteLine($"p = {(long)p:X}");
}
// p = 5E0437E9AC
ポインター間接参照演算子
ポインター変数名の前に*
を付けることでポインターが指すデータにアクセスすることが可能です。
unsafe
{
int x = 100;
int* p = &x;
Console.WriteLine($"p = {(long)p:X}");
Console.WriteLine($"*p = {*p}");
}
// p = 62BDF7EA3C
// *p = 100
また、このアクセスは参照だけでなく、変更も可能です。
unsafe
{
int x = 100;
int* p = &x;
Console.WriteLine($"p = {(long)p:X}");
Console.WriteLine($"*p = {*p}");
*p = 200;
Console.WriteLine($"*p = {*p}");
Console.WriteLine($"x = {x}");
}
// p = AAC157EAFC
// *p = 100
// *p = 200
// x = 200
構造体のメンバーへのアクセス
->
演算子を使用することで、構造体のメンバーへアクセスすることが可能です。
class Program
{
struct S
{
public int X;
public int Y;
public int Z;
}
public static void Main()
{
unsafe
{
S s = new() { X = 100, Y = 200, Z = 300 };
S* p = &s;
Console.WriteLine($"p->X = {p->X}");
Console.WriteLine($"p->Y = {p->Y}");
Console.WriteLine($"p->Y = {p->Z}");
}
}
}
// p->X = 100
// p->Y = 200
// p->Y = 300
また、この演算子を使用した場合でも、値の変更は可能です。
class Program
{
struct S
{
public int X;
public int Y;
public int Z;
}
public static void Main()
{
unsafe
{
S s = new() { X = 100, Y = 200, Z = 300 };
S* p = &s;
Console.WriteLine($"p->X(1) = {p->X}");
p->X = 500;
Console.WriteLine($"p->X(2) = {p->X}");
}
}
}
// p->X(1) = 100
// p->X(2) = 500
この演算子を使わず、(*構造体).メンバー
という形でも表現できます。
class Program
{
struct S
{
public int X;
public int Y;
public int Z;
}
public static void Main()
{
unsafe
{
S s = new() { X = 100, Y = 200, Z = 300 };
S* p = &s;
Console.WriteLine($"(*p).X = {(*p).X}");
Console.WriteLine($"(*p).Y = {(*p).Y}");
Console.WriteLine($"(*p).Z = {(*p).Z}");
}
}
}
// (*p).X = 100
// (*p).Y = 200
// (*p).Z = 300
ポインター変数の加算・減算
ポインター変数を加算・減算することにより、指しているアドレスの位置を移動させることが可能です。
移動する距離は変数のバイト数分です。
unsafe
{
byte b1;
byte* p1 = &b1;
byte* p2 = p1 + 1;
byte* p3 = p1 + 2;
Console.WriteLine($"p1 = {(long)p1}");
Console.WriteLine($"p2 = {(long)p2}");
Console.WriteLine($"p3 = {(long)p3}");
Console.WriteLine();
int i1;
int* p4 = &i1;
int* p5 = &i1 + 1;
int* p6 = &i1 + 2;
Console.WriteLine($"i1 = {(long)p4}");
Console.WriteLine($"i2 = {(long)p5}");
Console.WriteLine($"i3 = {(long)p6}");
}
// p1 = 974501964588
// p2 = 974501964589
// p3 = 974501964590
// i1 = 974501964556
// i2 = 974501964560
// i3 = 974501964564
アドレスがbyte*
のデータはアドレスが1バイトずつ、int*
のデータはアドレスが4バイトずつ移動していることがわかると思います。
Console.WriteLine
メソッド内で加算・減算した場合、1バイトずつ移動します。
これはキャストするとint*
ならば4バイトずつ移動するため、恐らく何もしないとvoid*
になっているのだと思います。
unsafe
{
int x;
int*p = &x;
Console.WriteLine($"p(0) = {(long)p}");
Console.WriteLine($"p(1) = {(long)p + 1}");
Console.WriteLine($"p(2) = {(long)p + 2}");
Console.WriteLine();
Console.WriteLine($"p(1) = {(long)(int*)(p + 1)}");
Console.WriteLine($"p(2) = {(long)(int*)(p + 2)}");
}
// p(0) = 200782900636
// p(1) = 200782900637
// p(2) = 200782900638
// p(1) = 200782900640
// p(2) = 200782900644
ポインターの比較演算子
ポインターも比較演算子を使用することで比較が可能です。
以下は宣言した変数とそれの位置をプラス方向、マイナス方向に移動したポインターのアドレスと比較をしています。
unsafe
{
long x;
long y;
long* p1 = &x - 1;
long* p2 = &y + 1;
Console.WriteLine($"(y == p1) = {&y == p1}");
Console.WriteLine($"(x == p2) = {&x == p2}");
}
// (y == p1) = True
// (x == p2) = True
するとアドレスが同じ位置を指すため、一致しました。
ポインター要素アクセス演算子
[]
演算子を使っても特定の位置のアドレスのデータを参照することが可能です。
添え字で指定した位置は各型のバイト数分進めた位置です。
unsafe
{
int x = 0xF1;
int y = 0xF2;
int z = 0xF3;
int* p = &z;
Console.WriteLine($"p[0] = {p[0]:X}, {(long)&p[0]}");
Console.WriteLine($"p[1] = {p[1]:X}, {(long)&p[1]}");
Console.WriteLine($"p[2] = {p[2]:X}, {(long)&p[2]}");
}
// p[0] = F3, 106293946740
// p[1] = F2, 106293946744
// p[2] = F1, 106293946748
おわりに
アンセーフなコードやポインター周りの機能はまだまだありますが、この記事ではここまでにしたいと思います。
固定サイズバッファー、関数ポインターなどカロリー高めな感じがしていますので、学べる事ができたらそれぞれ記事にしてアウトプットできたらと思います。