WisdomSoft - for your serial experiences.

マネージ配列

ハンドル型のオブジェクトを格納する配列を作成するには、標準 C++ の配列とは異なるマネージ配列を用います。マネージ配列は配列自身も参照型のオブジェクトであり、メモリ管理が不要なので関数などで容易に受け渡しできます。

参照クラス型の配列

参照クラス型オブジェクトは、共通言語ランタイムによって管理されているマネージヒープ上に配置されるため、参照クラス型の配列を標準 C++ のネイティブ型の配列と同じように扱うことはできません。参照クラス型の配列は、参照クラスのオブジェクトと同様に共通言語ランタイムによって管理されるためです。

標準 C++ ネイティブのアンマネージ配列は、メモリアドレスが連続する記憶領域を物理的に確保するデータ配列でした。静的な配列のサイズはコンパイル時に決定されるため、配列の要素数も型の一部であると考えられます。 配列へのアクセスは、連続したデータ列への先頭へのポインタをオフセットとしたポインタ演算によって実現されています。

しかし、参照クラスのように共通言語ランタイムによって管理されているオブジェクトはプログラムからアドレス単位でアクセスすることはできません。マネージ型配列は論理的な配列であり、メモリ上に連続する物理的な配列ではないということを理解してください。マネージ型配列は配列の先頭へのポインタではなく、任意の数のオブジェクトの列を管理する配列オブジェクトを参照しています。配列は System::Array クラスのオブジェクトが管理しています。

図1 マネージ型の配列
図1 マネージ型の配列

マネージ型配列は実行時にマネージヒープに生成される動的な配列ですが、共通言語ランタイムによって統一管理されているため、参照クラス型のオブジェクトと同様に明示的な解放を必要としません。

共通言語ランタイムのあらゆる配列は Array クラス型を基底クラスとしているため、C++ 言語によらず全ての .NET Framework 対応言語で配列に対して統一した操作を行うことが出来ます。この場では Array クラスについて特別な知識は不要ですが、高度な配列制御が要求される場合は Array クラスを含めたコレクション API と呼ばれる .NET Framework が提供する配列管理用の各種クラスについて学習する必要があります。

マネージ配列の宣言

C++/CLI では、従来の C/C++ 言語の配列構文とは異なる構文を採用しています。新しい構文で配列を作成するには array キーワードを利用します。array キーワードによる宣言は、一見すると C++ のテンプレートを使った配列のようにも見えますが、これは言語仕様で定められている配列生成式であり C++ のテンプレートとは異なります。

array キーワードによる配列変数の宣言は次のように記述します。

マネージ配列の宣言
修飾子 cli::array<修飾子 次元数>^ 変数名

修飾子は array キーワードを使った宣言がクラスのメンバである場合に指定することができる省略可能な修飾子です。cli:: は array キーワードの前に付けることができますが、これも省略可能です。< > 内のカンマ , と次元数は 1 次元配列を宣言する場合は省略可能です。前述したように、マネージ配列の実体は Array クラスのオブジェクトなので、マネージ配列は常にハンドル型です。

より単純にすると、次のような形でマネージ配列を宣言できます。

array<型名>^ 変数名;

array<型名, 次元数>^ 変数名;

次元数を省略した場合は自動的に 1 となります。最大で 32 次元までの配列を指定することができます。配列の添字は標準 C++ と同様に 0 番から数えます。

配列型の変数を宣言しただけでは、その中身は nullptr でありオブジェクトを参照していません。配列をインスタンス化するには gcnew 演算子を使って次のように記述します。

マネージ配列のインスタンス化
gcnew cli::array<次元数>(要素数1, 要素数2...)

この場合も cli:: は省略可能です。また、1 次元配列のインスタンスを作成する場合は次元数を省略できます。その後 ( ) で括って配列の要素数を指定します。多次元配列であれば、カンマ , で区切って各次元の要素数を指定することができます。

array キーワードで作成する配列に指定できる型はマネージ型のみであり、アンマネージ型を指定することはできません。よって、ref キーワードを指定していない標準 C++ のクラス型の配列を array で作成することはできません。

int の配列型変数 ary を宣言すると同時に、変数初期化子で配列オブジェクトのインスタンスを生成して変数を初期化する一般的な書き方は次のようなものになるでしょう。

array<int>^ ary = gcnew array<int>(5);

この宣言では、int 型の要素を 5 つまで保持できる配列を作成しています。次元数は省略しているので既定の 1 次元配列であると解釈されます。構文は従来のアンマネージ配列とは異なりますが、配列の考え方、次元やサイズなどの概念についてはそのままです。

配列の要素に対するアクセスは [ ] 演算子を使いますが、アンマネージ配列のようなポインタの演算処理は行われません。標準 C++ の [ ] 演算子は内部の数値と配列へのポインタを加算するポインタ演算の代理表記でしたが、マネージ型配列の場合はオブジェクトの列を管理している配列オブジェクトに対して、アクセスする要素番号を指定する引数のようなものとして添字が機能します。

コード1
int main()
{
	array<int>^ ary = gcnew array<int>(5);
	for(int i = 0 ; i < 5 ; i++)
	{
		ary[i] = i;
		System::Console::WriteLine(ary[i]);
	}

	return 0;
}
実行結果
コード1 実行結果

コード1は、int 型の配列 ary 変数に、サイズ 5 の配列インスタンスを生成して代入しています。その後、for 文で全ての要素に値を代入し、その値を標準出力に表示させています。型の宣言方法が大きく異なりますが、それ以降は従来の配列と同じように扱うことができます。

コード2
int main()
{
	array<int, 2>^ ary = gcnew array<int, 2>(3, 5);
	for(int i = 0 ; i < 3 ; i++)
	{
		for(int j = 0 ; j < 5 ; j++) 
		{
			ary[i, j] = ((i + 1) * 10) + j;
			System::Console::Write(ary[i, j] + ", ");
		}
		System::Console::WriteLine();
	}

	return 0;
}
実行結果
コード2 実行結果

コード2は 2 次元配列の ary 変数を宣言し 3 × 5 の 2 次元配列オブジェクトで初期化しています。array キーワードの次元数を明示的に 2 としているため、この配列は 2 次元であると解釈されます。配列のサイズには、それぞれの次元のサイズを指定する必要があるためカンマ , で区切ってサイズを指定しています。

参照クラス型の配列を作成するには、ハンドル型として配列を作成する必要があります。参照クラス T の配列は次のように宣言してください。

array<T^> var = gcnew array<T^>(size);

これで、参照クラス T 型のハンドル型の配列を作成することができます。

cli:: は何か

array キーワードには接頭辞のような形で cli:: という省略可能な文言がありました。これは、構文からも想像できるように cli 名前空間の array キーワードということを表しています。cli 名前空間は C++/CLI の言語仕様で予約済みとされています。この cli は何のためにあるのでしょうか。

cli 名前空間は、array キーワードを使って配列を宣言しようとしているスコープ上に、すでに array という名前の有効な識別子が存在する場合に使うことができます。array という名前は標準 C/C++ ではキーワードではないため、識別子に使うことができます。そのため、C++/CLI でも array はキーワードであるにもかかわらず array という識別子を許可しています。

int array;
array<int>^ list;	//識別子と衝突

array という識別子がスコープに存在している場合、array キーワードを使おうとすると識別子と衝突します。この問題を回避するために cli::array と記述するのです。cli 名前空間を明記することで、既存の識別子との衝突を回避することができます。

int array;
cli::array<int>^ list; //OK

cli は名前空間なので using namespace で可視化することもできます。そうすれば、例えば ::array という形でも識別子の array と区別することができます。

配列の初期化リスト

配列をインスタンス化すると同時に各要素を初期化することができます。配列の初期化方法は標準 C++ と同じで波カッコ { } 内に各要素の初期値をカンマ , で区切って指定します。例えば、整数型の配列を初期化するには次のように記述します。

array<int>^ ary = { 0, 1, 2, 3, 4 };

この宣言の場合、先頭の要素から順番に 0、1、2、3、4 という値を持つサイズが 5 の配列が作られます。配列のサイズは波カッコ内の要素に割り当てる初期値の数から決定することができます。多次元配列の初期を行う場合もアンマネージ配列の初期化と同じです。次元数に対して { } を入れ子にして記述します。

ただし、どちらの場合でもアンマネージ配列とは異なり、初期化を行う変数のサイズは決定されていません。よって、配列の初期化子で空の波カッコ { } を指定した場合は自動的にサイズが 0 の配列となります。また、多次元配列であればサイズを変数型から想定することができないため、波カッコの入れ子を省略することはできません。

array<int>^ ary1 = { }; //OK 空の配列
int ary2[3][3] = { 10, 0, 0, 20, 21, 0, 30, 0, 0 }; //OK
array<int, 2>^ ary3 = { 10, 0, 0, 20, 21, 0, 30, 0, 0 }; //エラー
array<int, 2>^ ary4 = { { 10, 11, 12, }, { 20, 21 }, { } }; //OK

配列の初期化子は、gcnew による配列インスタンス生成式の後に明示的に指定することもできます。

array<int>^ ary = gcnew array<int>(3) { 1, 2, 3 };

これは、gcnew 演算子でインスタンスの生成を記述し、その後に明示的な初期化リストを記述しています。gcnew による配列インスタンスの生成で、この配列のサイズが決定されているため、リストに指定できる要素の数は固定されています。リストに指定されている要素数が生成した配列オブジェクトのサイズ以下であれば、残りの要素は初期値で初期化されます。しかし、リスト要素数が配列のサイズを上回っている場合はコンパイルエラーとなります。

gcnew array<int>(3) { 1, 2 }; //OK { 1, 2, 0 } と同義
gcnew array<int>(3) { 1, 2, 3 }; //OK
gcnew array<int>(3) { 1, 2, 3, 4 }; //エラー

これは、配列インスタンス生成式と初期化リストの組み合わせの例です。最初のリストは 2 番要素までしか値を指定していませんが、配列のサイズは 3 です。この場合、初期化されない要素は整数型の初期値 0 で初期化されます。次のリストは、配列のサイズとリストの要素数が同じなので何も問題はないでしょう。しかし、最後のリストは配列のサイズよりも多く初期化する要素を指定しているため、コンパイルエラーとなります。

コード3
int main()
{
	array<int>^ ary1 = { 0, 1, 2, 3, 4 };
	array<int, 2>^ ary2 = gcnew array<int, 2>(3, 3)
	{
		{ 10, 11, 12 }, { 20, 21 }, { 30 }
	};

	System::Console::WriteLine("1次元配列 ary1 の値");
	for(int i = 0 ; i < 5 ; i++) 
	{
		System::Console::Write(ary1[i]);
		System::Console::Write(", ");
	}
	System::Console::WriteLine();
	System::Console::WriteLine("2次元配列 ary2 の値");

	for(int i = 0 ; i < 3 ; i++) 
	{
		System::Console::Write("{ ");
		for(int j = 0 ; j < 3 ; j++)
		{
			System::Console::Write(ary2[i, j]);
			System::Console::Write(", ");
		}
		System::Console::Write("}, ");
	}
	System::Console::WriteLine();

	return 0;
}
実行結果
コード3 実行結果

コード3は、int 型の配列 ary1 変数と ary2 変数に対し、宣言と同時に明示的な初期化リストで各要素を初期化しています。実行結果を見れば、それぞれの変数の各要素が、適切な値で初期化されていることを確認できます。ary2 変数は 2 次元配列で、波カッコ { } の入れ子にしているリストの数が最初の次元数、リスト内の最も多い要素数が次の次元の要素数となるため、この配列は 3 × 3 の 2 次元配列となります。

Array クラス

マネージ配列は物理的なメモリ配列ではなく、オブジェクトの列を管理する System::Array クラスのオブジェクトであるという話をしました。そのため、Array オブジェクトは配列に関する情報を提供しています。C 言語では文字配列の末尾は 0 とするという手法を使っていましたが、マネージ配列では Array オブジェクトから配列のサイズや次元数を取得することができます。

System::Array クラス
[SerializableAttribute]
[ComVisibleAttribute(true)]
public ref class Array abstract : ICloneable, 
	IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable

Array クラスは追求すれば非常に高度な機能を持っている配列管理クラスですが、基本的な機能は要素に対する操作です。ここでは、配列操作で最も重要となる配列オブジェクトの長さの取得方法について解説します。これまで、for 文で配列の全ての要素を初期化したり、出力をするという処理に、繰返し回数を定数で記述しました。しかし、これでは当然、プログラムの変更によって配列のサイズが変わってしまった場合に問題が発生します。

通常、こうした配列の各要素にアクセスする繰返し文では配列のサイズを実行時に取得します。Array クラスのオブジェクトから配列のサイズを取得するには Length プロパティを使います。

Array クラス Length プロパティ
public:
property int Length {
	int get ();
}

Length プロパティは配列全体のサイズを返します。多次元配列の場合は各次元数を乗算した配列全体のサイズが返ります。オブジェクトの次元数を取得するには Rank プロパティを使ってください。

Array クラス Rank プロパティ
public:
property int Rank {
	int get ();
}

Rank プロパティはオブジェクトの次元数を返します。1 次元配列ならば 1、2 次元配列ならば 2 となります。

コード4
int main(int argc, char ** args)
{
	array<int>^ ary1 = gcnew array<int>(5);
	array<int, 2>^ ary2 = gcnew array<int, 2>(3, 5);

	System::Console::WriteLine("ary1 Rank=" + ary1->Rank + ", Length=" + ary1->Length);
	System::Console::WriteLine("ary2 Rank=" + ary2->Rank + ", Length=" + ary2->Length);

return 0;
}
実行結果
コード4 実行結果

コード4は、生成した配列オブジェクトの次元数とサイズを実行時に取得して標準出力に表示するプログラムです。この方法を使えば、配列の全ての要素を操作する目的の for 文で、繰返し回数の限界値を実行時に決定することができます。

2 次元配列で、各次元ごとのサイズを取得したい場合は Length に代わって GetLength() メソッドを使います。GetLength() メソッドは、引数にサイズを調べる次元を指定します。

Array クラス GetLength() メソッド
public:
int GetLength (int dimension)

dimension に、要素数を調べたい次元を指定します。このメソッドは、dimension で指定された次元にある要素の数を返します。