変数の動的確保

 ポインタ編の大詰めです。今回は「変数をいつでも作れ、いつでも削除できる」方法を見ていきます。特にこの機能はポインタを使わないと絶対に実現できないものです。では、ゆっくりと見ていきましょう。

フツーな変数
スタック上の変数  通常の変数は「スタック」と呼ばれる場所に作成されます。作成された変数は「メモリ上にどんな風に置かれているのか」分かりません。確保した変数の隣のメモリ領域が、どこも使ってないのか、それともすぐあとに宣言した変数が使っているのか、それとも他のアプリケーションが使っているのか、それは基本的に調べられません。
 また、作成された変数は、スコープを外れるまで削除できませんし、また外れれば有無を言わさず削除されます。そのため、変数は関数のできるだけ外で作らなければならず、最終的には「グローバル変数」という形で作ることになってしまいます。
 配列の確保にも問題があります。配列の作成には「定数値」を使わなくてはなりません。次のようなコードはコンパイルエラーになります。


	int iArySize = 30;
	char ch[iArySize];	//コンパイルエラーです。
	

 このため、配列のサイズをプログラム中で変更することはできません。

 これらの、普通の変数が持つ「できないこと」が顕著に現れるのが、「文章を配列に格納する」ときです。
 文章をデータとして持つ場合、通常は「文字配列へのポインタ配列」として持ちます。たとえば、次のようにします。


	char *chAry[10];

	char ch1[] = "あいうえお";
	chAry[0] = ch1;

	char ch2[] = "かきくけこ";
	chAry[1] = ch2;

	TRACE( "1: %s, 2: %s\n", chAry[0], chAry[1] );
	

 このコードはあまりにもべたべただなと感じると思います。文字を格納した配列はこれ以上の文字を格納できませんし、文章自体も10行以上にする事ができません。また、この文字配列もスコープから外れれば自動的に削除されてしまいます。まったく使えないものだということが分かると思います。
 と、いうわけで、これらの規制を取り払った、変数を確保する方法を見てみましょう。

newとdeleteを使った変数の確保
 実は「動的に変数を作成する」ことにかなり多くの方法が存在します。その中でもまず一番分かりやすいと思うnewdeleteを使った方法を見てみましょう。


	int *pi = new int;	//変数の確保。

	if( pi )
	{
		*pi = 3;
		TRACE( "%d\n", *pi );

		delete pi;	//変数の解放。
	}
	

動的に作成された変数  動的に変数を作成するときにはnewというキーワードを使います(これはC++のものですので、C言語では使えません)。上のように「new 型名」とすると、「ヒープ」と呼ばれる場所に変数が確保されます。
 「ヒープ」(C++では「フリーストア」とも呼ばれます)はメモリの領域のひとつで、「スタック」とは違う場所にあります。「スタック」では通常の変数を、「ヒープ」ではこのような動的に作成された変数を格納します。ちなみに「スタック」も「ヒープ」も「積み上げたもの」という意味ですが、「ヒープ」の方が「乱雑に積み上げる」とちょっとニュアンスが違います。

 newは、指定した型のポインタを返すので、その型のポインタ変数で受け取ります。
 ただ、メモリの空きがないときなど、変数の確保に失敗することがあります。その場合NULLポインタという値が返ってきます。NULLポインタは「どこも指していないポインタ」という意味で、このように失敗したときなどにちゃんとしたポインタの代わりに返されたりします。ちなみにNULLポインタはアドレスの整数値としてはゼロです。

 さて、newで確保した変数はポインタを通してのみ操作できます。雰囲気としては、相手の見えない電話のようなものでしょうか。メモリ上の変数はただポインタを介してのみ、存在を確かめられるというわけです。
 そのあとの操作は、これまでのポインタと同じものです。別段難しいことはないでしょう。

 さて、最後に後始末をしなければなりません。newで確保した変数は必ずdelete解放しなければなりません。解放することで、メモリは「この領域を他の変数に使ってもいいんだな」と認識します。
 逆に言えば、解放しない限り変数はずっと残っているということです。newで取得したポインタを渡していけば、他の関数でも変数を使用できるというわけです。
メモリリーク  もし解放し忘れると、その領域はほったらかしにされてしまいます。基本的には自動的に削除されたりはしません。この領域は他からは使えないため、メモリ上の領域が無駄になってしまいます。これを「メモリリーク」と呼びます。「リーク」とは「漏れる」という意味です。使えるはずの領域が漏れだしてしまい、どこかへ行ってしまうわけです。

 これまで、変数は自動的に作成され、自動的に削除されていました。が、このように動的に変数を作成して、適切なタイミングで削除することもできるというわけです。もちろんこの方法の方が難しいのですが、でもその分得られるものも大きいのです。

文字配列の作成
 さて、ここから具体的な使用方法を見ていきましょう。newを使って、文字配列を作成してみます。


	char *pch1 = new char[20];

	strcpy( pch1, "あいうえお" );
	TRACE( "%s\n", pch1 );

	delete [] pch1;
	

 まずnewを使って配列を作成します。配列の要素数は型のあとに付けます。また、この要素数は別に定数値じゃなくてもOKです。
 作成したら、前回紹介したstrcpy()というランタイム関数で文字列をコピーします。
 使ったあとはdeleteで変数を解放します。配列として作成した場合には必ずdeleteのあとに[]を付ける必要があります。これを忘れないでください。

 では次に、もう少し具体的な例を見てみましょう。文字配列に文字を足そうとしてみて、入りきらない場合には拡張します。


	// とりあえずconstな文字列を作成しておきます。
	// 通常これらは関数の引数として受け取ります。
	const char chDef1[] = "ABCDE";
	const char chDef2[] = "FGHIJ";
	// さらに文字列の文字数を取得します。
	// strlen()は文字数を取得するランタイム関数です。
	const int chDef1Size = strlen( chDef1 );
	const int chDef2Size = strlen( chDef2 );

	// まずchDef1の文字配列を作成します。
	char *pch = new char[chDef1Size + 1];	//+1!!
	strcpy( pch, chDef1 );
	TRACE( "%s\n", pch );

	// 次にchDef1とchDef2をくっつけた文字配列を作成します。
	char *pchTemp = new char[strlen( pch ) + chDef2Size + 1];
	strcpy( pchTemp, pch );
	strcat( pchTemp, chDef2 );	//くっつけるラインタイム関数です。
	delete [] pch;	//前の文字配列は削除。
	pch = pchTemp;	//文字配列のポインタを入れ替えます。
	TRACE( "%s\n", pch );

	delete [] pch;
	

 まず最初に、入れたりくっつけたりする文字列を配列として取っておきます。普通こういった文字列は関数の引数として受け取ったりします。また同時にstrlen()というランタイム関数を使って文字数を取得します。
 次にnewを使って文字配列を作成します。今回は「文字数とぴったり合った配列」を作成しています。ここで+1していることに注意してください。strlen()では最後のNULL文字を数えないので、その分を足す必要があるからです。

 さて、ここからです。上の例でのpchというポインタ、これを文字配列の中心として操作したいとしましょう。つまり、これ以外のポインタを使いたくない、これ以外のポインタに文字配列を入れたくないというわけです。
 でも、今pchが指している文字配列は、余裕がありません。文字を足そうにも配列のサイズが足らないのです。

動的配列の操作  そこで、まず「足しあわせた文字列が入りきるサイズの配列」をnewで作成して、そのポインタをpchTempに取っておきます(ここでも+1してることに注意してください)。
 作ったら、そこにpchが指す文字配列の中身をコピーします。これで、ふたつの文字配列の中身は同じになります。
 その上に、strcat()というランタイム関数を使って文字列を継ぎ足します。pchTempは十分なサイズがあるので、足しあわせてもちゃんと入ります。
 入ったらpchの指す文字配列を削除してしまいます。要らなくなったメモリの領域は、無駄やメモリリークの可能性ともども削除してしまうのがいいでしょう。くれぐれも、「コピーのあと」ということを忘れずに。
 削除したら、pchpchTempが持つアドレスを渡してしまいます。これはつまり、pchの持つ文字配列が、さらにサイズを大きくし、その上文字列を足した状態になった、ということです。

 というわけで、このような手順を踏むことで配列のサイズを変えることができるというわけです。仮の配列を作って、値をコピー、そしてポインタを入れ替え。こんな感じでできてしまうわけです。
 実際にこれを組んでみるとなると、難しいと感じると思います。サイズの正確な計算方法、コピーに使うランタイム関数の選択、deleteのタイミング、関数の実装方法、他いろいろとやっかいな問題が出てくるでしょう。
 こういった具体的な方法については、本などに詳しく書かれているのでそちらを読んでください(爆)。とりあえず、ここでは雰囲気というかイメージを大事にしてもらえればと思います。あと、「シェルエクステンション」のアイテムIDリストとは?は結構具体的な例になると思います。
 と、これらを読む前に、newdelete以外の方法について見ておきましょう。

malloc()とfree()の場合
 他の方法としておそらく一番使われているのがmalloc()free()の組み合わせでしょう。ほぼnewdeleteのような使い方ですが、細かい部分で違いがあります。


	int *piAry = (int *)malloc( sizeof( int ) * 10 );
//	int *piAry = new int[10];	//newを使った場合。

	piAry[0] = 100;
	piAry[1] = 101;
	TRACE( "%d, %d\n", piAry[0], piAry[1] );

	free( piAry );
	

 malloc()strcpy()などと同じランタイム関数です。newに似た使い方ですが、いくつか違いもあります。
 まず、malloc()の引数にはバイト単位のサイズを指定します。すぐ下の行にあるように、newでは型のサイズを気にする必要はありません。が、malloc()の場合には「型のサイズ×要素数」とする必要があります。
 また、戻り値はvoid *というポインタが返ってきます。これは「特に型の指定されていないポインタ」という意味で、そのため型キャストする必要があります。

 確保したらnewと同じように使用できます。メモリ上のどこかに変数が確保されて、それをポインタを通して操作します。
 最後にdeleteと同じように、ランタイム関数のfree()を使って変数を解放し、また他で使えるようにします。

newとmalloc()の重要な違い
 さて、これだけ見るとnewmalloc()は同じような機能を持つように見えますが、実はとても重要な違いが存在します。


	CString *pcStr = (CString *)malloc( sizeof( CString ) ); 
//	CString *pcStr = new CString;

	*pcStr = "あいうえお";	// 例外の発生。
	TRACE( "%s\n", (LPCTSTR)*pcStr );

	free( pcStr );
//	delete pcStr;
	

 上のコードは例外が発生します。が、コメントアウトする行を入れ替えてnewを使ったコードに変えると、例外は発生せずちゃんと実行されます。この違いはコンストラクタデストラクタにあります。

 まずCStringの実装方法について見てみましょう。CStringはこれまで解説してきたような形で動的に文字配列を作成します。が、その仕組みがちょっと特殊で、文字列以外の情報も格納し、それを利用することで効率的な文字列操作を行うよう組み込まれています。
 この「文字列以外の情報」に初期値をセットするのが、「コンストラクタ」と呼ばれるメンバ関数です。コンストラクタはそのクラス型の変数(非ポインタ)が宣言されたときに自動的に呼び出されます。このコンストラクタが呼ばれて初めて、CStringは使える状態になるというわけです。
 またCStringは動的に文字配列を確保するので、あとで削除する必要があります。そこで出てくるのが「デストラクタ」です。デストラクタはクラス型の変数がスコープから外れて削除されるときに自動的に呼び出されます。この関数が呼び出されることで文字配列は削除され、メモリリークを防げるというわけです。
 実際にどのように呼ばれるのか、次のコードを試してみてください。


class CTest
{
public:
	CTest(){		TRACE0( "コンストラクタ\n" ); }
	~CTest(){	TRACE0( "デストラクタ\n" ); }
};

void TestFunc()
{
	CTest cTest;
	TRACE0( "次はポインタ\n" );
	CTest *pcTest;	//「警告」が出ますが気にしないで。
}
	

 コンストラクタとデストラクタがちゃんと呼ばれていること、そしてポインタの場合には呼ばれないことを確認してください(ちなみにデフォルトの設定だと上のコードは「警告」が出ますが気にしないでください)。

 さて以上をふまえた上で、次のコードを試してみてください。


class CTest
{
public:
	CTest()
	{
		TRACE0( "コンストラクタ\n" );
		m_iTest = 100;
	}
	void Trace(){	TRACE( "%d\n", m_iTest ); }
	~CTest(){	TRACE0( "デストラクタ\n" ); }

	int m_iTest;
};

void TestFunc2()
{
	CTest *pcTest = (CTest *)malloc( sizeof( CTest ) ); 
//	CTest *pcTest = new CTest;

	pcTest->Trace();

	free( pcTest );
//	delete pcTest;
}	

 実行してみれば分かるとおり、上のコードではコンストラクタとデストラクタが呼ばれません。でもnewdeleteの方に書き換えれば呼び出されるはずです。これがnewmalloc()の大きな違いです。

 malloc()が行うのは、引数で受け取ったサイズだけメモリ上の領域を確保して、その先頭ポインタを返すだけです。つまりそこにあるのは「場所を確保するだけ」で、どういう変数がそこに入るか等はまったく考えられていません。宴会をするための部屋は借りたけどお酒や料理をまったく用意しない、まさに場所だけを用意するようなものです。
 同様にfree()も、ポインタが指す領域を開放するだけです。そのメモリ領域にどれだけ重要な情報が入っていようがお構いなしです。宴会が終わったということで片づけも何もナシに部屋を返すようなものです。

 newmalloc()と同じようにメモリ領域を確保します。そのあとコンストラクタを呼び出します。この機能によって、クラスのメンバ変数が適切に初期化されます。宴会場を確保して、そこにお酒や料理を用意するわけです。
 同様にdeleteはデストラクタを呼び出し、そのあとで領域を開放します。お客さんを帰して、後片づけをして、そのあと部屋の持ち主に返すわけです。

 newdeleteにはこのように「コンストラクタとデストラクタを呼ぶ機能」が備わっています。また、配列として宣言したときには、各要素ひとつひとつのコンストラクタとデストラクタがちゃんと呼ばれます。このように、newmalloc()に比べて重要な機能を持っています。

じゃぁnewだけ使えばいいの?
 実はそういうわけにもいきません。たとえば「アイテムIDリスト」のようにシェルエクステンションで使用するオブジェクトは、IMallocインターフェイスCoTaskMemAlloc()で領域を確保しなければならないことになっています。
 また、実はMFCのnewは実際には_malloc_dbg()malloc()のデバッグ版)を使用しています。こういった関係から、malloc()の使い方をちゃんと知っておいた方がいいでしょう。
 あと、特に気を付けて欲しいのが、必ずペアを間違えないということです。malloc()で確保した領域をdeleteで解放したり、逆にnewで確保した領域をfree()で解放しないようにしてください。矛盾する話ですが、こういった問題を避けるためにも、使用する方法はひとつに限定した方がいいでしょう。

とりあえずここまで
 さて、ポインタの使い方についてものすごーく駆け足で見てきましたが、いかがだったでしょうか。
 はっきり言って、ここまで読んだだけでポインタを使いこなせるほど甘くはないです(爆)。「参照とポインタ 」「ポインタと配列」「文字列操作」の3ページでは、ポインタがどういうものなのか、っていうイメージと、それを扱うときの簡単な注意に絞って書いてきました。これ以降の話、つまり「ポインタを具体的にどんな風に使うか」は他の良書を参考にしてください。

 で、ポインタ編はまだ続きます。次回はまとめとして、参照とポインタの関係、そしてVisual C++全体の話をしようと思います。ちょっと脱線した内容かも……。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.