C# や Java などのプログラミング言語では、 コンピュータのメモリ上の任意の場所に自由にアクセスするための手段、 すなわち、ポインタの利用が禁止もしくは制限されています。
ポインタは、その自由さから、非常に有用であると同時に、 危険なものでもあり、バグの原因になりやすいという問題がありました。 そのため、C# や Java などの言語では、 ポインタの代替となる物を用意し、 必要最小限の機能のみを提供する事によって、 簡単でかつ堅牢なプログラミング環境を提供しています。
しかし、C# では、 C言語などの既存のプログラミング言語との相互運用性や、 プログラムの実行効率向上のために、 ポインタを完全に廃止するのではなく、 unsafe コンテキストと呼ばれる特別な場面でのみポインタを利用できるようにしています。
( 「コンピュータの基礎知識」 に、 もう少し詳しいポインタの説明を書いたので、 そちらも参照してみてください → 「メモリ」 。 )
プログラム中で使用する変数の値はメモリに記憶されています。 図1に示すように、メモリ上には値を格納するための領域が一直線に並んで、 それぞれにアドレス(変数の所在地を示す番号)が付いています。 そして、アドレスを格納するために用いるのがポインタ(アドレスを指し示す変数という意味)です。
ポインタを説明するために、C# の前身であるC++によるポインタの利用例を示します。
C++では、変数の宣言するとき、
型名の後に * を付けるとポインタ変数になります。
また、変数の前に & を付けることで、
その変数のアドレスを取り出すことができます。
逆に、ポインタの参照先の値を読み書きするには、
ポインタ変数の前に * を付けます。
// 注: C++ です。 int* p; // ポインタの宣言 int n = 30; p = &n; // ポインタ p に n のアドレスを代入 cout << *p; // p の参照先 (n) の値 (30) を読み出す *p = 20; // p の参照先 (n) の値を 20 に書き換える cout << n; // n は 20 に書き換わっている
このように、ポインタを使うことで他の変数を参照することが出来ます。
さらに、ポインタにはあくまでメモリ上のアドレスがそのまま数値として格納されていて、
+, --, ++, -- などの演算子を使って値を自由に変更できます。
この自由さのため、ポインタは正しく使用すれば非常に強力な道具になりますが、
ほんのちょっとした不注意からプログラマの意図しない動作を起こすことがあり、
扱いの難しいものとなっていました。
このような問題を解決するため、 C# や Java などのプログラミング言語では、 ポインタの代替となる機能を提供し、 ポインタの使用を禁止もしくは制限しています。
ここでは、ポインタの詳細についてはこれ以上触れませんが、 従来のプログラミング言語においてポインタがどのような場面で使用されいたのかと、 C# においてどのような機能で代替出来るのかだけ、 以下に簡単にまとめます。
| ポインタを必要とする場面 | 代替機能 |
|---|---|
| 動的確保 | 参照型の変数は常に動的に確保される。 |
| 動的配列 | C# の配列は元々動的。 |
| 引数の参照渡し | 参照変数を使うもしくは ref、out キーワードを使う。 |
| 配列に対する反復処理の効率化 | foreach 文を使う。 |
従来のプログラミング言語でポインタを必要としていた場面のほとんどは、 他の機能で代替することが出来るため、 C# や Java 言語にとってポインタは必要なものではありません。 そのため、Java 言語ではポインタを完全に廃止しています。 しかし、C# ではプログラムの効率化と従来のプログラミング言語との相互運用を目的として、 制限付きながらポインタの使用可能にしてあります。
まず、ポインタ使用における制限ですが、
C# では unsafe キーワードを用いて宣言されたメソッドもしくはブロック内(このようなコードを unsafe コードと呼びます)でしかポインタを使用できません。
メソッドに unsafe 修飾子を付けることでそのメソッド内部は unsafe コードとなり、
そのメソッド内でポインタを使用できるようになります。
また、unsafe{} と言うように、ブロックの手前に unsafe キーワードを付けることで、そのブロック内部でのポインタ使用が可能になります。
unsafe void UnsafeMethod() { // unsafe メソッド。 // ポインタが使用可能。 } void SafeMethod() { // ポインタ使用不可。 unsafe { // unsafe ブロック。 // ブロック内でのみポインタ使用可能。 } }
さらに、プログラム内で unsafe キーワードを使用するためには、
コンパイル時に /unsafe オプションを付ける必要があります
。
前節で述べたように、ポインタの使用は危険を伴うため、
C# ではこのような強い制限を設けています。
unsafe コード内では以下の機能が利用可能となります。
C# では、C++ 言語と似た文法でポインタを使用できます。
すなわち、& 演算子を用いてアドレスの取り出し、
* 演算子を用いてポインタの指している先を参照、
+, --, ++, -- などの演算子を使ってアドレスの値を計算できます。
using System; class UnsafeTest { static void Main() { unsafe { int n; int* pn = &n; // n のアドレスをポインタ pn に代入。 byte* p = (byte*)pn; // 違う型のポインタに無理やり代入可能。 *p = 0x78; // n の最初の1バイト目に 0x78 を代入 ++p; *p = 0x56; // n の2バイト目に 0x56 を代入 ++p; *p = 0x34; // n の3バイト目に 0x34 を代入 ++p; *p = 0x12; // n の4バイト目に 0x12 を代入 Console.Write("{0:x}\n", n); // n の値を16進数で表示。 } } }
12345678
C# で通常使用している配列は動的にメモリを確保しています。 しかしながら、動的配列は静的配列と比べ、少しですが効率が悪くなります。 そのため、C# では unsafe コード内限定で、配列を静的に確保するための構文を用意しています。
配列の静的確保は以下に示すように、 stackalloc キーワードを用いて行います。
型名* 変数名 = stackalloc 型名[配列長];
変数の型が 型名[] から 型名* に、
インスタンスの作成方法が new 型名[配列長] から stackalloc 型名[配列長] に代わっていますが、通常の配列と似たような構文で使用できます。
ただし、stackalloc を用いた場合、配列長は定数でなければなりません。
using System; class UnsafeTest { static void Main() { unsafe { const int N = 10; const int MAX = 99; int* x = stackalloc int[N]; // 配列を静的確保 Random rand = new Random(); // 配列 x に乱数を代入 for(int i=0; i<N; ++i) { x[i] = rand.Next(MAX); Console.Write("{0}, ", x[i]); } Console.Write('\n'); // 配列 x を整列 for(int i=0; i<N; ++i) for(int j=i+1; j<N; ++j) if(x[i] > x[j]) { int tmp = x[i]; x[i] = x[j]; x[j] = tmp; } // 整列結果を出力 for(int i=0; i<N; ++i) { Console.Write("{0}, ", x[i]); } Console.Write('\n'); } } }
56, 67, 82, 23, 86, 78, 27, 92, 39, 13, 13, 23, 27, 39, 56, 67, 78, 82, 86, 92,
.NET Framework 向けに作成されたコードは、 メモリの確保・解放は全て .NET Framework によって管理されているため、 managed (管理された)コードと呼ばれています。 即ち、.NET Framework は生成したインスタンスがまだどこかで使われているかどうかを自動的に判別して、 不要になったメモリ領域を自動的に解放します。
unsafe コードも managed コードの一部分なので、 unsafe コード中のオブジェクトも .NET Framework の管理下に置かれます。 ところが、このようなメモリの自動管理とポインタはあまり相性の良いものではありません。 .NET Framework によりインスタンスの格納されているメモリ領域が削除されてしまったり、 移動されてしまう可能性があり、 ポインタの参照先が気が付いたら消えていたということが有り得るからです。
したがって、ポインタを使う場合、しばらくの間メモリ領域の削除や移動を停止してもらう(アドレスを固定する)必要があります。 そのための構文として、C# には fixed 文というものがあります。 fixed 文は以下のような形で書かれます。
fixed(型名* 変数名 = アドレス取得式) 実行したい文
fixed 文中でアドレスを取得したインスタンスは格納先のメモリ領域が移動・削除されなくなり、 アドレスが変化しないことが保障されます。 例えば、参照型のメンバのアドレスをポインタに代入する場合、以下のようにします。
// Complex クラスは re, im というdouble 型のメンバを持っているものとする。 Complex c = new Complex(1, 0); fixed(double* p = &c.re) { *p = 10; } Conosle.Write("({0}, {1})\n", c.re, c.im); // (10, 0) と表示される
また、C# では配列を暗黙的にポインタに変換し、 ポインタを介して配列の内容を変更することが出来ます。 この場合にも、fixed 文を使用してアドレスを固定しなければなりません。
int[] x = new int[10]; fixed(int* px = x) { // ポインタ px を介して配列 x の内容を更新できる。 }
using System; class UnsafeTest { static void Main() { unsafe { const int N = 5; int[] x = new int[N]; for(int i=0; i<N; ++i) x[i] = i; // 配列 x をポインタ px に代入する。 fixed(int* px = x) { // ポインタを介して配列 x の内容を変更。 for(int* p = px; p != px + 10; ++p) *p = (*p) * (*p); } // 結果出力。 for(int i=0; i<N; ++i) Console.Write("{0}, ", x[i]); // 0, 1, 4, 9, 16, と表示される。 } } }
Ver. 2.0
C# 2.0 で、unsafe な構造体のメンバとして、 C 言語の配列風の固定長バッファを定義できるようになりました。
固定長バッファは、以下のように、fixed キーワードを用いて定義します。 通常の C# の配列と異なり、型名[] ではなく、 変数名[要素数] と書きます。
[System.Runtime.InteropServices.StructLayout( System.Runtime.InteropServices.LayoutKind.Sequential, Pack=1)] unsafe struct Header { public Int16 Source; public Int16 Destination; public Byte Type; fixed byte reserved[3]; public Int32 Data; }
固定長配列は、主に unmanaged コードとの相互運用時に用いられます。 その名前の通り、バッファ長は固定で、実行時にサイズを決めたり、 サイズを変えたりすることはできません。
C# の unsafe の目的は、実行効率の向上と既存言語との相互運用性ですが、 この目的のためなら、C# で unsafe を使う以外に、 C++/CLI を使うという選択肢もあります。
C++/CLI は、C++ を .NET Framework 向けに修正したもので、 Microsoft の提供する .NET 言語の中で唯一、 native コードと managed コードを混在させてのプログラミングができる言語です。 C++/CLI を使うことで、既存の(native の) C++ 資源を .NET Framework から利用することも割と容易です。
native なコードは、通常の C++ と同じ構文で記述でき、 managed な部分に関しては、 ref、delegate、property などのいくつかの拡張キーワードや、 TypeName^ や TypeName^% などの追加の記号を使って記述します。
C# と比べるとお世辞にも書きやすいとは言えない言語ですが、 native / managed 混在プログラムを書きたい場合には最良の選択肢となるでしょう。