ポインタ編の大詰めです。今回は「変数をいつでも作れ、いつでも削除できる」方法を見ていきます。特にこの機能はポインタを使わないと絶対に実現できないものです。では、ゆっくりと見ていきましょう。
|
フツーな変数
通常の変数は「スタック」と呼ばれる場所に作成されます。作成された変数は「メモリ上にどんな風に置かれているのか」分かりません。確保した変数の隣のメモリ領域が、どこも使ってないのか、それともすぐあとに宣言した変数が使っているのか、それとも他のアプリケーションが使っているのか、それは基本的に調べられません。 また、作成された変数は、スコープを外れるまで削除できませんし、また外れれば有無を言わさず削除されます。そのため、変数は関数のできるだけ外で作らなければならず、最終的には「グローバル変数」という形で作ることになってしまいます。 配列の確保にも問題があります。配列の作成には「定数値」を使わなくてはなりません。次のようなコードはコンパイルエラーになります。 |
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を使った変数の確保
実は「動的に変数を作成する」ことにかなり多くの方法が存在します。その中でもまず一番分かりやすいと思うnewとdeleteを使った方法を見てみましょう。 |
int *pi = new int; //変数の確保。
if( pi )
{
*pi = 3;
TRACE( "%d\n", *pi );
delete pi; //変数の解放。
}
動的に変数を作成するときにはnewというキーワードを使います(これはC++のものですので、C言語では使えません)。上のように「new 型名」とすると、「ヒープ」と呼ばれる場所に変数が確保されます。
newは、指定した型のポインタを返すので、その型のポインタ変数で受け取ります。
さて、newで確保した変数はポインタを通してのみ操作できます。雰囲気としては、相手の見えない電話のようなものでしょうか。メモリ上の変数はただポインタを介してのみ、存在を確かめられるというわけです。
さて、最後に後始末をしなければなりません。newで確保した変数は必ずdeleteで解放しなければなりません。解放することで、メモリは「この領域を他の変数に使ってもいいんだな」と認識します。
これまで、変数は自動的に作成され、自動的に削除されていました。が、このように動的に変数を作成して、適切なタイミングで削除することもできるというわけです。もちろんこの方法の方が難しいのですが、でもその分得られるものも大きいのです。 |
文字配列の作成
さて、ここから具体的な使用方法を見ていきましょう。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というポインタ、これを文字配列の中心として操作したいとしましょう。つまり、これ以外のポインタを使いたくない、これ以外のポインタに文字配列を入れたくないというわけです。
そこで、まず「足しあわせた文字列が入りきるサイズの配列」をnewで作成して、そのポインタをpchTempに取っておきます(ここでも+1してることに注意してください)。
というわけで、このような手順を踏むことで配列のサイズを変えることができるというわけです。仮の配列を作って、値をコピー、そしてポインタを入れ替え。こんな感じでできてしまうわけです。 |
malloc()とfree()の場合
他の方法としておそらく一番使われているのがmalloc()とfree()の組み合わせでしょう。ほぼnewとdeleteのような使い方ですが、細かい部分で違いがあります。 |
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と同じように使用できます。メモリ上のどこかに変数が確保されて、それをポインタを通して操作します。 |
newとmalloc()の重要な違い
さて、これだけ見るとnewとmalloc()は同じような機能を持つように見えますが、実はとても重要な違いが存在します。 |
CString *pcStr = (CString *)malloc( sizeof( CString ) );
// CString *pcStr = new CString;
*pcStr = "あいうえお"; // 例外の発生。
TRACE( "%s\n", (LPCTSTR)*pcStr );
free( pcStr );
// delete pcStr;
上のコードは例外が発生します。が、コメントアウトする行を入れ替えてnewを使ったコードに変えると、例外は発生せずちゃんと実行されます。この違いはコンストラクタとデストラクタにあります。
まず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;
}
実行してみれば分かるとおり、上のコードではコンストラクタとデストラクタが呼ばれません。でもnewとdeleteの方に書き換えれば呼び出されるはずです。これがnewとmalloc()の大きな違いです。
malloc()が行うのは、引数で受け取ったサイズだけメモリ上の領域を確保して、その先頭ポインタを返すだけです。つまりそこにあるのは「場所を確保するだけ」で、どういう変数がそこに入るか等はまったく考えられていません。宴会をするための部屋は借りたけどお酒や料理をまったく用意しない、まさに場所だけを用意するようなものです。
newはmalloc()と同じようにメモリ領域を確保します。そのあとコンストラクタを呼び出します。この機能によって、クラスのメンバ変数が適切に初期化されます。宴会場を確保して、そこにお酒や料理を用意するわけです。
newとdeleteにはこのように「コンストラクタとデストラクタを呼ぶ機能」が備わっています。また、配列として宣言したときには、各要素ひとつひとつのコンストラクタとデストラクタがちゃんと呼ばれます。このように、newはmalloc()に比べて重要な機能を持っています。 |
じゃぁnewだけ使えばいいの?
実はそういうわけにもいきません。たとえば「アイテムIDリスト」のようにシェルエクステンションで使用するオブジェクトは、IMallocインターフェイスやCoTaskMemAlloc()で領域を確保しなければならないことになっています。 また、実はMFCのnewは実際には_malloc_dbg()(malloc()のデバッグ版)を使用しています。こういった関係から、malloc()の使い方をちゃんと知っておいた方がいいでしょう。 あと、特に気を付けて欲しいのが、必ずペアを間違えないということです。malloc()で確保した領域をdeleteで解放したり、逆にnewで確保した領域をfree()で解放しないようにしてください。矛盾する話ですが、こういった問題を避けるためにも、使用する方法はひとつに限定した方がいいでしょう。 |
とりあえずここまで
さて、ポインタの使い方についてものすごーく駆け足で見てきましたが、いかがだったでしょうか。 はっきり言って、ここまで読んだだけでポインタを使いこなせるほど甘くはないです(爆)。「参照とポインタ 」「ポインタと配列」「文字列操作」の3ページでは、ポインタがどういうものなのか、っていうイメージと、それを扱うときの簡単な注意に絞って書いてきました。これ以降の話、つまり「ポインタを具体的にどんな風に使うか」は他の良書を参考にしてください。
で、ポインタ編はまだ続きます。次回はまとめとして、参照とポインタの関係、そしてVisual C++全体の話をしようと思います。ちょっと脱線した内容かも……。 |
(C)KAB-studio 1998 ALL RIGHTS RESERVED. |