最後に、ポインタを使う上で良く出てくる質問とその回答を載せておきます。
|
・APIやMFCでLPCTSTRを引数に求めているのならLPCTSTR型の変数を渡すのが道理、と素直に感じます。この型に対してTCHAR型の配列を渡すのは少し違和感を覚えるのですが。
普通の変数とポインタ変数を、まったくの別物と考えるのが違和感を消す近道だと思います。「ポインタを単独で作ることはない」とまず考えて、「LPCTSTR型の変数も作るはずがない」と考えていくべきでしょう。
また、同時に参照を使い、「参照の引数に渡すのもポインタの引数に渡すのも意味は同じ」ととらえれば、引数にポインタを使う理由を理解しやすくなると思います。 最後に、自分で作る関数にポインタを使って、ポインタをなぜ引数に使うのか、ということを考えれば、違和感はなくなっていることでしょう。 |
・LPRECT型のようなものが引数に出てきた場合、RECTなら&を付けて渡せばいいのは分かるのですが、CRectを使う場合にはどう渡せばいいのかいまいちよく分かりません。どう判断するのがいいのでしょうか。
こういう場合、まずクラスのメンバ関数を調べてみてください。クラスは「型変換演算子関数」というメンバ関数を備えることができます。これは「特定の型を要求された時に呼び出す関数」です。
CRectには、LPRECTへと代入されようとしたときに呼び出されるCRect::operator LPRECT()という関数が備わっています。この関数は適切なRECTのポインタを返してくれます。この関数のおかげで、LPRECTを要求されている時にはそのままCRectを渡せることになっています。同様の関数はCStringにも備わっています。
ところが、この関数が備わっていないクラスもあります。CPointやCSizeがそうです。これらの場合には「継承元」をチェックしてください。これらのクラスは、それぞれPOINTやSIZEと同じ構造体から継承されています。そこで、構造体と同じように&を付けて渡せばOKです。 |
・LPRECT型のように、ポインタをなぜわざわざ別の型として定義するのでしょうか。紛らわしいだけだと思います。
これにはふたつの理由があります。
ひとつは*の付け忘れを防ぐためです。 |
int* pi1, i2;
このようにしたとき、i2はただのint、つまりポインタになりません。これを防ぐためにこのポインタ講座では変数の前に*を付けていましたが、実際には忘れることもあるでしょう。
特定の型のポインタ型をtypedefすることで、これを防ぐことができます。 |
LPRECT pstRect1, pstRect2;
こうすればどちらもポインタになります。このように、コーディングミスをカバーするための意味がまずひとつ。
もうひとつは様々なサイズのポインタに対応するためです。たとえば、ウィンドウズ95の場合にはポインタは32ビットサイズですが、ウィンドウズ3.1ではFARやNEARといったキーワードを使ってポインタのサイズを変更していました。 WINDEF.Hは、使用しているOSによってこれらのキーワードを付けたり消したりしています。このようにポインタのサイズを自動的に調整するためにも、ポインタ型を定義しています。 |
・APIのSHBrowseForFolder()で使うBROWSEINFOの各メンバで、値を渡すのか受け取るのかよく分かりません。なぜこのように分かりづらいのでしょうか。また、どのように区別すればいいのでしょうか。
これは僕も分かりにくいと思います。構造体丸ごとポインタとして渡すため、どのメンバからデータを取得してどのメンバに値が返されるのか分かりにくくなっています。目安としては、specifiesがあったら渡すメンバ、receivesがあったら受け取るメンバだととらえてほぼ間違いないでしょう。
自分で関数を組む場合には、できるだけこのような仕様は避けるべきでしょう。たとえば構造体を使わずすべて関数の引数に渡す形にするとか、構造体をふたつに分け、渡す側はconstにしてしまう、といった方法を採るのがバグを減らすコツといえるでしょう。 |
・free()やdeleteはなぜメモリの領域を開放できるのでしょうか。sizeof()では確保された領域のサイズが分からないのですから、どれだけの領域を開放すればいいのか分からないはずだと思います。また、何らかの方法でサイズを取得できるのだとしたら、そういう関数はなぜ存在しないのでしょう。あれば、文字配列を渡す時にサイズも渡さずに済むと思うのですが。
基本的にこの関係の実装は関数やコンパイラ、OSなどによって変わってくるので一概には言えません。ただ、malloc()とfree()のペアは、確保したメモリブロックとそのポインタをリストとして保存し、解放時にポインタを元にリストから検索して解放すべきサイズを取得しています。また、Microsoftの独自拡張としてこのサイズを取得する関数も存在します。
重要なのはそれぞれまったく違う方法でメモリブロックの情報を保存しているかもしれないということです。確保するものでもnewやmalloc()の他に、HeapAlloc()やGlobalAlloc()、CoTaskMemAlloc()といったAPIも存在し、それぞれが別々の実装方法を持っています。
問題なのは、このどれを使用しても、結局はポインタが返ってくるということです。つまり一見しただけではどの関数を用いて確保されたのか分かりません(もちろんスタック領域に作成されたものかもしれないのです)。そのため、現実的には「ポインタが指す領域がどうやって作成されたのか」に依存するようなコードは書けないということになります。 |
・void *やLPVOIDはどのようなポインタなのでしょうか。普通voidは「戻り値のない関数」に使用するので、型になるというイメージが沸きません。
voidとvoid *は別物と考えるべきでしょう。voidポインタは、「特に型の指定されていないポインタ」という意味です。次のコードを試してみてください。
|
int i1 = 3;
int *pi1 = &i1;
void *pv1 = pi1; //これができます。
pi1 = (int *)pv1; //型キャストしないとコンパイルエラー。
このように、voidポインタはどんなポインタも格納できます。ただしそれを他の型のポインタに渡す場合には型キャストを行う必要があります(コンパイラによってはエラーが発生しません)。
voidポインタは特に型を指定せずに単にポインタとして処理したい場合に使用します。たとえばランタイムライブラリのfree()はvoidポインタを引数に取ります。 ただし、voidポインタを駆使してプログラムを組むとバグが発生する可能性が高くなります。C++には「テンプレート」という便利なものがあるので試してみてください。 |
・ポインタをLPARAMやWPARAMに型変換して関数に渡したり、逆にこれらを構造体のポインタへと型変換するコードをよく見かけます。なぜこのようなことをしているのでしょうか。プログラムとしてこれは正しいのでしょうか。
ウィンドウズ95ではポインタは32ビット整数値として扱われます。WPARAMはunsigned int、LPARAMはintと同じなのでこれらも同様に32ビット整数です。そのため、型変換すればそのまま渡せます。
これはvoidポインタ以上にフレキシブルな操作ができることを意味しています。形にとらわれずありとあらゆるものを渡せることになるため、これもバグの可能性を生みます。 ただ、ウィンドウズのメッセージシステムはこの方法をフル活用しているため、使用せざるを得ないのが実状です。MFCのメッセージハンドラはこの変換をメンバ関数内にパッケージングしているので、同様にうまく外部から干渉されないようにし、できるだけ使わないようにするのがいいでしょう。 |
・次のコードは関数の戻り値としてポインタを使っていますが、かといってポインタが指し示す文字列はスタック上に作成されているので、関数が終了したら削除されると思います。このコードはあっているのでしょうか。
|
CString Test1()
{
char chStr[] = "あいうえお";
return chStr;
}
void Test0()
{
CString cStr = (LPCTSTR)Test1();
TRACE( "cStr: %s\n", (LPCTSTR)cStr );
}
結論から言えば正しいコードです。デフォルトの設定では、関数の戻り値は呼び出した側にオブジェクトとして作成されます。つまり、
|
CString cStr = (LPCTSTR)cTempStr;
のように、関数の部分に戻り値となる変数が作成されます。これは新規に作成されるため、持っている文字列ポインタも新しく作成されたものです。
Test1()が終了する直前、chStrが持つ文字列は戻り値となるcTempStrへとコピーされます。その後、スタック内に存在するchStrは削除されますが、文字列はcTempStrへと格納されています。 この文字列はさらにcStrへとコピーされます。そして次の行に移るとき、cTempStrは自動的に削除されます。
このシステムの鍵は、戻り値をcStrで受けている、ということです。たとえば、 |
LPCTSTR pchStr = (LPCTSTR)Test1();
CString cStr = pchStr;
と2段階に分けて渡した場合、次の行に移ったときに戻り値が持つポインタが削除されてしまうためpchStrが持つポインタは無効になってしまいます。
こういったバグの可能性が出てくることや、コピーを繰り返すとそれだけ処理に時間が掛かることを考えて、文字列を受け取るためのポインタを引数に渡す、といった形を取るのが一番の方法でしょう。 (なぜわざわざLPCTSTRへと型キャストしているかというと、CStringはCStringを渡されると特別な処理をしてしまうからです) |
・同じく、CDC::GetWindow()もCWnd *を返します。deleteしなくていいのでしょうか。どこかでされるのだとしたら、どのようなタイミングで行われるのでしょうか。
deleteする必要はありません。これらのポインタを返すメンバ変数を使用した場合、このポインタはマップに格納されます。マップに格納されたポインタは、メッセージキューが空になった状態、いわゆる「アイドル状態」になったときに自動的に削除されます。このシステムにより、基本的にCDC::GetWindow()を呼び出した関数内ではポインタは正当性が失われませんが、関数から抜けた後はいつ削除されても不思議ではない、ということになります。
このような、プロセス内でグローバルな変数が多く存在する実装方法はある意味「MFCらしい」と言えますが、この方法によりマルチスレッドプログラミングが難しくなっています。自分で実装する場合には真似しない方がいいでしょう。 |
・さらに同じく、CDC::GetWindow()はなぜわざわざポインタを返すのでしょうか。CWndそのものや、HWNDを返しても構わないと思いますが、何か意図があるのでしょうか。
これにはオブジェクト指向的な理由があります。まず、MFCの「ハンドルをメンバ変数に持つクラス」は、「ハンドルのラッパークラス」としてではなく「ウィンドウズオブジェクトそのもの」として捉えられています。
たとえばCWnd型はデストラクタが自動的にウィンドウを破棄します。「変数がなくなったとき、オブジェクトもなくなる」という考え方です。そのため、すでに存在するウィンドウのハンドルをCWnd::Attach()で渡したあと、CWnd::Detach()を呼び出さないと、スコープから出た変数がウィンドウを削除してしまいます。
CDC::GetWindow()は、こういった考え方に合わせてCWndのポインタを返します。もしCWndそのものを返したら、それは「すでにあるウィンドウの複製を作成してしまう」ということになります。
もともと、この考え方はHWNDのようなハンドル的なものです。ハンドルは一種のポインタで、ウィンドウズオブジェクトを操作するための「コントローラー」の役目を持っています。MFCでのハンドルの代わりが、オブジェクトクラスへのポインタということになるでしょう。 |
・COMインターフェイスのIShellFolder::EnumObjects()は第3引数にLPENUMIDLIST *、つまり「ポインタのポインタ」を渡すよう指示しています。なぜこのようなことをしているのでしょうか。また、「ポインタのポインタのポインタ」や「ポインタのポインタのポインタのポインタ」は存在するのでしょうか。
これは前述の「オブジェクト指向的な考え方」の影響です。COMインターフェイスは「どこかに実体があり、それをポインタを使って操作する」という考え方を持っています。そのため、COMインターフェイスを操作するためには、オブジェクトそのものではなく、オブジェクトのポインタが必要になります。
そのポインタを取得するための方法が「ポインタのポインタ」です。「ポインタを受け取る変数のアドレスを渡す」と考えると分かりやすいでしょう。また、ポインタをハンドルに置き換えて「ハンドルを受け取る変数のアドレスを渡す」と考えてもいいでしょう。
この方法としては、「受け取る変数へのポインタ」を渡せばいいので、「ポインタのポインタのポインタ」などは使いません。ただし、これらは多次元配列を作成するときにしばしば使用します。具体的な使い方はアルゴリズムの本などをご覧ください。 |
・次のようなコードを見かけました。スタック領域に作成できるのに、なぜわざわざ動的に確保しているのか理解できません。メモリリークを発生させてバグが生まれる可能性が高まるだけだと思うのですが。
|
void CTestDlg::OnButton()
{
CTestDlg2 *pcDlg = new CTestDlg2;
pcDlg->DoModal();
delete pcDlg;
}
これもオブジェクト指向的な考え方です。ここでのCTestDlg2はダイアログクラスですが、このダイアログを実際に作成するのはウィンドウズです。CTestDlg2型の変数を作ると、オブジェクトが複数存在してしまう、そういう考えを持ったのでしょう。
上のコードのようにポインタを使用すれば、ポインタはウィンドウを操作するためのコントローラー、という考え方になります。これはよりハンドルに近い使い方でしょう。またメモリリークの危険性は、MFC版のnewとdeleteを使用しているのなら検出できるので、それほど心配する必要もないかと思います。
また、このコードを書いた方はJAVAを使用したことがあるのかもしれません。JAVAではクラスは参照型として作成されます。JAVAの参照はCのポインタに似た機能を持っていて、newを使って実体を割り当てます。それに似せたのかもしれません。 |
(C)KAB-studio 1998 ALL RIGHTS RESERVED. |