📘

【C#】ポインターを扱う

2020/12/25に公開

プロジェクトの中で、SwiftとC#のブリッジを行っており、そこで文字列の受け渡しで一瞬ポインターをC#側で扱う必要が出ました。
結局Swift側でchar*を指定して、C#でstringで受ける書き方ができたので、それで実装しましたが、人生でまともにポインター扱うのがはじめてだったので、そのことを書いとこうと思います。

ポインターは難しい?

C言語学びはじめた学生の最初の躓きポイントがポインターらしいんですが、わかる気はします。
変数や関数というプログラミング特有の概念を身につけたあとで、ポインターが出てくると、何が混乱するのでしょう?
たとえばこんなInt型の変数。

int temp = 1;

よくある説明だと変数という「箱」に値を入れておく、みたいな説明をされると思います。
ポインターが出てくるとこの箱という比喩が一気に破綻するので、マジメに話を聞いていた初学者ほどポインターの登場で混乱するんだと思います。

箱というかメモリ

ポインターをちゃんと理解したければ、CPUとメモリの関係を把握しておく必要があるかと思います。
ただプログラミングやりたい初学者にそんな説明してもほとんどの人は理解不可能だと思うので、箱という比喩が便利なのかと。

変数というのは正確にはメモリ上の値であり、それぞれアドレスを持っています。
ポインターはそのアドレス情報を持った一つの変数、という説明でいいと思うんですが、どう説明しても初学者には厳しい気がしますね。

ポインターなんて使わない?

これはまあそうだと思います。
てか使えたとしても使うべきじゃないと思っています。

低レイヤーの何かならともかく、現代のアプリケーション開発でメモリにアクセスしたいユースケースってほぼないと思いますし、
あるとすればパフォーマンス目的ですけど、コンパイラの最適化を頑張った方が筋がいい気がします。

C#のポインター

C#は他のC言語(そんなに知りませんけど)に比べると多少安全性が高いので、「ポインターなんて使うなよ」感が出ている言語設計になっています。
ポインターを使いたければunsafe節で囲まないとコンパイル通りません。

unsafe
{
  // using pointer code
}

また、コンパイルオプションでunsafeを許可しないとプログラムの実行ができません。
Unity環境だとPlayerSettings > Other SettingsのAllow 'unsafe' Codeのチェックを入れましょう。
(たぶんデフォルトでチェック入ってる気がしますが)

fixed

ポインターはメモリ上を参照しますが、参照先がガベージコレクションで再配置されていることがあるらしいです。
再配置されても、ポインターのアドレスはそのままなので、想定している値じゃないものを処理することになってしまいます。
それを防ぐため、fixedステートメントというのが用意されています。

unsafe
{
    fixed (int* p = &a[0])
    {
        // code
    }
}

char*を扱う例

いよいよサンプルコードです。
下記のサンプルコードでは、char型のポインタを元にStringを返します。

private unsafe void getString(char* p_str)
{
    return new string(p_str);
}

もし本気で何かの制約でchar*を受けることになったら、こんな感じで早めにstringに変換することをお勧めします。

C#のstringはコンストラクタにString(Char*)を持っています。

僕は最初このコード見たときに、「アドレスだけだとstringの長さがわからなくないか?」と疑問を持ちました。

興味があればバイナリで出力してみても面白いかと思いますが、C#のstring型は先頭4Byte分文字列長が記録されたエリアがあります。
ここを見ればstringの長さがわかるので、char*を上手いこと処理できるわけです。
逆にメモリ上の情報がstringの想定と合わなければ、例外を投げるか、奇跡的に上手くいったとしても盛大に文字化けするかのどちらかになると思います。

ポインターの加算・減算

ポインターで面白いなと思ったのは、ポインター自体に加算・減算ができることです。
何を言っているかというと、

int[] a = new int[5] { 10, 20, 30, 40, 50 };
unsafe
{
    fixed (int* p = &a[0])
    {
        Console.WriteLine(*p); // 10
        p += 1;
        Console.WriteLine(*p); // 20
        p += 1;
        Console.WriteLine(*p); // 30
    }
}

このコードを実行すると、10, 20, 30が出力します。
int*に加算することで、配列のインデックスを一つ進めるのと同じ挙動になります。

アドレス自体は32or64 bit(CPUによる)の2進数 [1]なので、
そのアドレス番地にプラス1すると言うまでもなく訳わからないところに飛びますが、intやcharならば一応型情報を持っているので、
その型に合わせたByte数分飛んでくれるようです。

そして、なんか説明の順序がおかしいですが、&a[0]で配列の1番目の要素のアドレス(int*)を取得できます。
ポインターの参照先の値にアクセスしたい場合は、*pという記述をします。

参考

ポインター型 (C# プログラミング ガイド)
char*→string型へ

脚注
  1. 32bitの例なら、たとえば0xFFFFFFFFのような。表記は2進数だと長いので16進数 ↩︎

Discussion