ここでは C 言語と C++ 以外の言語ではあまり見かけないポインタについて解説します。
C 言語にはなく、C++ にはある、マニアックなポインタといえば、クラスメンバへのポインタです。クラスメンバへのポインタを知らない人も多いのではないでしょうか。これについては、このページの後半で解説します。前半は、通常のポインタについて解説します。
ポインタは難しいといわれることがありますが、そんなことはありません。C 言語と C++ がアセンブリ言語のように軽快であるのは、ポインタのおかげといっても良いでしょう。ここで、明快に理解してください。
ポインタはプログラムにバグを生じさせやすいものであるという議論があります。これは、注意を怠ると本当にそのとおりで、ポインタは、メモリ上のどこでも無節操に指せるので、悪用すれば、Windows マシンをブルーバックにすることぐらい、朝 メシ前です。そのような意味で、例えば main 関数の中で、裸のポインタが乱舞するようなプログラミングはもってのほかです。
では、ポインタは要らないのかというと、C++ では、ヒープにメモリを取ろうとすると、ポインタはなくてはならない存在です。いわば、諸刃の剣のようなもので、使い方によって、システムを破壊することも出来るし、有効にも使えます。
床に画鋲をばらまいたり、ドアのノブにカミソリの刃を取り付けたりする人はいませんね。main 関数の中で、裸のポインタを使ったりするのは、これに良く似た行為であるといえます。
反対に、電気カミソリというものがありますね。これには、刃がなくてはならない存在です。しかし、刃は管理された位置にあって、髭は剃るけれども、皮膚は傷つけないようになっています。良いポインタの使用方法とは、これに良く似ていて、C++ では、殆どの場合、ポインタはクラスの中にうまく組み込まれ、外からはあまり陽には見えないような形で使う事になります。よく管理されたポインタは、動作が完全であるがゆえに、プログラムにバグを生じさせやすい、などという低レベルの話とは別の次元で動作することになるわけです。
C++ マニアック的には、このような、洗練されたポインタの使用が前提であるし、そのように使う限り、ポインタは有用で、当然なくてはならない存在です。
ナオ : 店主、熱く語るって感じですか。
店主 : いやぁ、ポインタって目の敵みたいにバッシングされているところがあるけれど、使い方次第なんだよネ。
ナオ : よっ。ポインタ守護者。
店主 : そういうことじゃなくて...
まず、C++ の組み込み型の変数とそのポインタの関係を見てみましょう。
組み込み型は char、int、float などの型で、変数は次のように定義されます。
char cValue; // 1 バイト int iValue; // 4 バイト float fValue = 1.3f; // 4 バイト
上記の例では、float 型の変数 fValue だけを 1.3f で初期化しています。他の cValue と iValue の値は不定となります。各変数がコンピュータの主記憶メモリの中で占めるバイト数は、CPU が Pentium 系の場合には、コメントに書いたようにそれぞれ 1、4、4 バイトになります。
上記の例では、変数型が一目で分かるように c、i、f で始めていますが、このような記法をハンガリアン記法といいます。ポインタを理解するには、この記法が大いに役に立つと思われるので、ご存じない方は ハンガリアン記法 のページを一度ご覧ください。
さて、上記のように変数を連続して定義すると、コンピュータの主記憶メモリ上に変数領域が取られます。これを模式図に書いてみると、次のようになります。メモリには 1 バイト毎に番地がふられているので、cValue が例えば 10 番地であったとしましょう。
cValue iValue fValue 10番地 11番地 15番地 [1バイト、不定][4 バイト、不定][4バイト、1.3f]....
例えば iValue について考えてみると、int 型の変数 iValue は 11 番地から連続した 4 バイトの領域が割り当てられており、その内容は不定、ということになります。
これを少し言い換えると、iValue のアドレスは 11 番地である、ということになります。
iValue という名前から、11 番地という数字を取り出すには、iValue の前に & を付けて、次のように書きます。いまは、とりあえず、右辺だけ見てください。
int* piValue = &iValue; // iValue のアドレスを取り出して、変数 piValue に代入する。
&iValue は、この場合 11 というアドレスを表す整数値になります。つまり 11 と書いたのと同じです。そのように書き換えてみましょう。
int* piValue = 11; // iValue のアドレスを取り出して、変数 piValue に代入する。
さて、ここでポインタが出てきました。piValue がポインタです。piValue の pi は int 型への pointer を意図してハンガリアン記法で付けた名前です。コンパイラにとっては、pi に意味があるわけではなく、これは、人間が読みやすいようにつけているだけなので、変数 piValue がポインタであるということは、型の指定 int* が決定しています。このように、基本となる型(この場合は int)の後ろに * を付けて宣言した変数は、その基本形の変数のアドレスを表すようになります。このようにアドレスを入れた変数をポインタといいます。
ポインタも、変数なので、主記憶メモリ上に置かれます。上で描いた模式図に追加してみると、次のようになります。ここでは piValue が 19 番地にあるとします。アドレスは 4 バイトで表現されるとしておきます。
cValue iValue fValue piValue 10番地 11番地 15番地 19番地 [1バイト、不定][4 バイト、不定][4バイト、1.3f][4 バイト、11番地]....
ポインタ変数 piValue の内容は 11 であり、これは iValue のアドレスです。
さて、ポインタ piValue の使い道ですが、これを使って iValue のメモリに値を書き込んだり、逆に読んだりする事ができます。そのためには piValue の前に * をつけて、次のように書きます。
*piValue = 3; // iValue に 3 を書き込む。 int iValue2 = *piValue; // iValue から値を読み取り iValue2 に代入する。
これは、 *piValue と書くと、iValue と書いたのと同じ事が起こるということになります。つまり、* と p が打ち消しあって iValue と書かれているのと同じことになると考えても良いでしょう。ハンガリアン記法を使うと、このように解釈することもできるわけです。
上記を実行すると、模式図は次のようになります。
cValue iValue fValue piValue iValue2 10番地 11番地 15番地 19番地 23番地 [1バイト、不定][4 バイト、3 ][4バイト、1.3f][4 バイト、11番地][4 バイト、3 ]....
ここまでは、簡単ですね。
初心者がつまづくとすれば、配列の先頭アドレスとポインタとの関係についてでしょう。このような簡単なところでつまずいては、大いなる損失なので、ここで、完全に理解してください。
配列は、例えば、次のように定義します。
int aiTable[100]; // int 型の長さ 100 の配列
この配列の先頭アドレスは aiTable と書くことで得られます。aiTable と書くと、これは、アドレス、つまり数値、であって、ポインタではありません。アドレスとは、200 番地というような数値です。
では、アドレスとポインタの違いは何かというと、次のようになります。
値を代入することが出来るもの、つまり変数、を左辺値といいます。左辺値と言う名前は、A = B のような式で、= の左辺にくることから由来しています。この言葉を使えば、アドレスとポインタの違いは、
ということになります。コード的には、次のようになります。
aiTable = 200; // これは、aiTable が、左辺値でないので、不可。 int* piTable = 200; // これは、piTable が、左辺値なので、可。 piTalbe = aiTable; // これは、piTable が、左辺値なので、可。
アドレスとポインタの差は、上記のように数値を書き込めるかどうかだけで、読み取る場合にはまったく同様に使うことが出来ます。例えば、次の表現はどちらも同じ動作をします。
int iValue0 = aiTable[2]; int iValue1 = piTable[2];
たった、これだけのことなので、もうポインタでつまずくことはありませんね。
前項までは、変数とメモリ領域の先頭を指すポインタについて解説しました。ここでは関数へのポインタについて解説します。
次のような関数を考えます。
int Function(int iValue) { return 2 * iValue; }
この関数を指すポインタは、次のように定義できます。
int (*pFunction)(int) = Function;
Function という記述は、関数のアドレスを示すのですが、前に & をつけても同じ値となります。つまり上記は次のように書いても同じ事になります。
int (*pFunction)(int) = &Function;
関数へのポインタを定義するだけで、初期化しない次の書き方も、もちろん可能です。
int (*pFunction)(int); // 関数へのポインタの定義
この書き方と、元の関数のプロトタイプ宣言を比較してみましょう。
int Function(int); // 関数のプロトタイプ宣言
関数へのポインタを定義するには、上記のように、元の関数のプロトタイプ宣言の、関数名 Function の部分を (*pFunction) に置き換えればよいわけです。
pFunction が Function で初期化されていれば、pFunction を使って間接的に Function を次のように呼び出すことが出来ます。ポインタなので前に * をつけて、
int iValue = (*pFunction)(3);
と書くか、または、簡単に、
int iValue = pFunction(3);
と書いても OK です。このあたりの書き方には、冗長性(redundancy)があるというわけです。
最初の関数へのポインタの定義の仕方 int (*pFunction)(int) という書き方が、少し見にくいという場合には、次のようにこのポインタ型を typedef しておけば、
typedef int (*PFUNCTION)(int);
次のように、通常の変数のような形で、関数へのポインタを定義することが出来ます。
PFUNCTION pfunction = Fucntion;
関数へのポインタのサンプル Pointer2Function1.cpp
ポインタに整数 iN を加算すると、初めに指していたアドレスの iN 要素先のアドレスを指すようになります。例えば、整数の配列内の要素を指すポインタ piElement を考えてみます。
int aiTable[5] = { 111, 222, 333, 444, 555 }; int* piElement = aiTable; // aiTable の最初の要素 111 を指している。 piElement += 1; // aiTable の次の要素 222 を指すようになる。
上記の例では、piElement は int 型を指すポインタなので、1 を加算すると int 型のメモリ領域の長さ sizeof(int) 分だけアドレスが加算されることになります。int が 4 バイトであれば、アドレスは 4 だけ加算されるわけです。
加減算には、次のような書き方のバラエティがあります。意味を確認しておいてください。
piElement += 1; // 1 要素進む。333 を指すようになる。 int piOld = *piElement++; // piOld に元の値 333 をコピーした後で、piElement自体は 1 要素進み、444 を指すようになる。 int piNew = *++piElement; // piNew も piElement も 1 要素進むので、piNew は 555 となり、piElement は 555 を指すようになる。 piElement += iN; // iN 要素進む。
piElement++ を後置きインクリメント、++piElement を前置きインクリメントと呼びます。上記で + を - に置き換えた表現も存在し、この場合、進むを戻るに、インクリメントをデクリメントに読み替えることになります。このあたりは当たり前なので、これ以上解説する必要はないでしょう。
上記のようにポインタに整数を加減算するときの整数のことをディスプレースメント(要素数)と呼びます。
少し興味があるのは、では、関数を指すポインタに整数を加減算したらどうなるかということです。つまり、次の例で、++ した後の pfunc はいくつになるかということです。
int (*pfunc)(void) = 0; // ここまでは、問題ない。 ++pfunc; // これは、不可
正解は、関数を指すポインタに整数を加減算することは出来ない、ということで、上記は、コンパイルエラーになります。
関数へのポインタのインクリメントサンプル(コンパイルできません) Pointer2Function_Displacement.cpp
ポインタ同士を引き算すると、前項で解説した、ディスプレースメント(要素数)が得られます。
int aiTable[3]; int* piElement0 = aiTable; int* piElement1 = aiTable + 2; // 右辺は、アドレスと整数の加算 int iDisplacement = piElement1 - piElement0; // 要素数 2 が得られる。
3 行目は、アドレスと整数の加算ですが、この場合にも、要素のサイズが考慮されて、2 要素分アドレスが進みます。4 行目のポインタ同士の引き算では、要素数の差 2 が得られます。
異なる型を指すポインタ同士の引き算は、上記のメカニズム上、当然、コンパイルエラーになります。
int* piElement; char* pcElement; int iDisplacement = piElement - pcElement; // コンパイルエラーとなる。
さて、いよいよ、クラスメンバへのポインタについて解説します。このポインタは、知名度が低い(?)せいか、一般には、あまり使われていないようです。詳しい解説もあまり見かけないと感じるのは私だけでしょうか? 知名度が低いゆえに、このポインタを使用すること自体がかなりマニアックであるということもいえますし、これでなくては出来ない使い道もあります。では、早速見ていきましょう。
まず、簡単に、クラスメンバへのポインタとはどんなものかというと、これは、次のようになります。
具体例をあげるのが良いでしょう。まず、メンバ変数とメンバ関数を持つ次のようなクラスを考えます。
class CManiac { public: int m_iValue; // メンバ変数 void Hello() {} // メンバ関数 };
上記クラスのメンバ変数へのポインタとメンバ関数へのポインタは、次のように定義されます。
int CManiac::* piValue = &CManiac::m_iValue; // メンバ変数へのポインタ void (CManiac::* pfuncHello)() = &CManiac::Hello; // メンバ関数へのポインタ
まず、形式的に、次の、通常のポインタの定義と比較してみてください。
int * piValue = & iValue; // 普通の変数 iValue へのポインタ void ( * pfuncHello)() = & Hello; // 普通の関数 Hello へのポインタ
このように、クラスメンバへのポインタの定義では、左辺では、* を CManiac:: で修飾したような形になり、右辺では、変数名や関数名を同様に CManiac:: で修飾したような形になります。
前項では、クラスメンバへのポインタの定義の仕方について解説しました。この項では、クラスメンバへのポインタの使い方について解説します。
クラスメンバへのポインタを使う場面は、クラスオブジェクトからクラスメンバを解決する普通の . 演算子や、クラスオブジェクトのポインタからクラスメンバを解決する普通の -> 演算子を使う場面と同じです。
ただ、異なるのは、演算子 . や -> の右に来るメンバ変数名やメンバ関数名の部分が、クラスメンバへのポインタに替わるということです。そのため、演算子 . や -> の右側に更に * をつけて、その右にクラスメンバへのポインタが来ることになります。形式的見ると演算子が .* や ->* になったように見えます。そして、実際、C++ では . や -> に * を追加したというのではなく、.* や ->* という演算子が文法的に存在することになっています。C++ を使う側の理解の仕方としては、どちらでも良いでしょう。
具体的なコーディングの例としては、次の関数の中身を見てください。私自身このページを見に来る必要性を感じる場合があるぐらいなので、かなり参考になるはずです。
class CManiac { public: int m_iValue; // メンバ変数 void Hello() { // メンバ関数 printf("Hello maniac!\n"); } }; int main() { void (CManiac::*pManiacHello)() = &CManiac::Hello; // メンバ関数 Hello へのポインタ int CManiac::*piValue = &CManiac::m_iValue; // メンバ変数 m_iValue へのポインタ CManiac maniac; // CManiac オブジェクトを構築する。 (maniac.*pManiacHello)(); // .* 演算子を使って、メンバ関数を呼び出す。左辺は、クラスオブジェクト maniac maniac.*piValue; // .* 演算子を使って、メンバ変数を選択する。左辺は、クラスオブジェクト maniac ((&maniac)->*pManiacHello)(); // ->* 演算子を使って、メンバ関数を呼び出す。左辺は、maniac のアドレス (&maniac)->*piValue; // ->* 演算子を使って、メンバ変数を選択する。左辺は、maniac のアドレス return 0; }
.*、->* クラスオブジェクトへのポインタからメンバを選択のサンプル ArrowAst2.cpp
main() の 1 行目と 2 行目で、メンバ関数へのポインタとメンバ変数へのポインタを定義しています。これらクラスメンバへのポインタは、通常のポインタと異なり、全ての CManiac オブジェクトに共通なポインタになります。もう少し具体的にいうと、コンパイラの実装にもよりますが、クラスのメンバ変数へのポインタは、クラスオブジェクトの先頭アドレスからの相対的な変数の位置を示していると思えばわかりやすいと思います。例えば、上記の例の piValue は、CManiac で定義されている最初の変数 m_iValue の相対アドレスなので 0 になります。メンバ関数については、ディスプレースメントという概念が無いのでこのようにはいえませんが、クラスに共通なメンバ関数アドレスがあるということになります。
これらの演算子は、STL の mem_fun などの実装に使われています。
上記サンプルの実行結果は、次のようになります。
Hello maniac! Hello maniac!
ナオ : 意外とあっさりした解説ネ。
店主 : 実用性を考えると、このぐらいコンパクトな解説が一番役に立つと思うんだよね。
ナオ : そうかもね。