参照とポインタ

この文章はプログラマでもプログラミング言語の専門家でもないただのプログラミング好きの私が適当に書きつづったものです。よってこの文章には正しい箇所もあるでしょうが、間違っている箇所もかなりあるはずです。ご指摘いただけたらと思います。また、この文章を読んでも内容を鵜呑みにしないでください。鵜呑みにした結果何か起きても知りません。

"諸悪の根元"−ポインタ

Cの特徴の1つとして、また初心者が必ずつまずく(*)とされる難所としてポインタがあげられるわけだが、ポインタはどうも最新流行のプログラミング手法の中では諸悪の根元として忌み嫌われているようだ。

(*) 自慢するわけではないのだが、私は別にポインタでつまずいた記憶はない。もちろん、効果的な使い方などはその後いろいろなコードを読んでいく中で身に付いていったわけだが。

ポインタは確かに諸刃の剣である。非常に便利な反面、危険でもある。例えば次のようなコードを考えてみる。

int *ptr;
ptr = NULL;
(*ptr)++;

この場合、NULLポインタを操作している事が明白であるので、もしかしたらコンパイラが何か言ってくれるかも知れないが、大抵の場合、どこかで設定すべきポインタの初期化を忘れたなどの理由で、上記と同じ様な操作をしてしまう可能性がある。NULLポインタの操作は、Windows9x系の場合、システムを巻き込んでの大虐殺を繰り広げる可能性もある。

また、NULLポインタに限らず、一般に宣言された直後のポインタには、予測不能な値が入っており、初期化を忘れて使うと一般保護違反を引き起こしたり、システムによっては他のプログラムやシステムそのものを殺戮する可能性もある。

参照

そこでC++に導入されたのが"参照(reference)"である。参照は次のように宣言する。

int &ref;

最も一般的な使い方は以下のように、関数の引数として参照を用いる方法である。

int some_func(int &tekito)
{
    tekito = 256;
    ...
}

参照の本質はポインタそのものであるから、上記の例で言えば、関数some_funcが引数tekitoに値を代入すれば、呼び出し側の引数の値も変更される。だが、参照は構文上は普通の変数そのもののように扱うことができ、ポインタのように明示的に*をつけて値を参照することはない。

またポインタと違って、プログラマは参照が指しているアドレスに関与できない。(int *ptr = &tekito; とすればアドレスを得ることはできる。) よって、関数間でポインタをやりとりするときによく見られる"NULLポインタを渡す"というような操作は行えない。

参照は宣言時に指す変数を指定する。例えば次のコードは、refをbarへの参照として宣言する。

int &ref = bar;

これはポインタで書くと以下の操作と同じである。

int *pref = &bar;

ポインタと異なる点は、「一度初期化したあとはその指すアドレスを変更できない」という点である。よって、ポインタがどこを指しているのか曖昧になることもないし、NULLポインタのような特殊な状態を持つこともない。この点で参照はポインタよりも安全だと言われる。

ポインタと参照の使い分け

私は新し物好きで、Windows2000 RC2は真っ先に入れたし、新しいツールが出ると試してみたくてたまらなくなるし(大抵はクラッシュだらけでがっかりするだけだが)、アップデートモジュールはためらいもなくインストールする。そういうわけで、一時は参照に惚れ込んで参照ばかり使っていたこともあった。

もちろん、参照だけで生きていけるほど世の中甘くはないし、ポインタを完全に廃してしまえるわけではない。そこで、私がポインタと参照を使い分ける時の基準について書いてみようと思う。

高速なオブジェクトの引き渡しには参照

関数に構造体やオブジェクト(実際にはどちらも同じ物であるが)を渡す場面が数多くある。そのような場合に通常の値渡しを行うと、クラスのメンバを全てコピーして新しいオブジェクトを生成するため、巨大なオブジェクトではオーバーヘッドがものすごいものとなる。そこで、オブジェクトを渡すときには参照を使う。なお、オブジェクトが変更されないことを明示し保証するためにconstをつける。

class SomeLargeClass;

int foo(const SomeLargeClass &large)
{
    ....
}

int main()
{
    SomeLargeClass large;

    foo(large); // 参照で渡す
    ...
}

値を変更する場合にはポインタ

関数に変数(オブジェクトも含む)を渡して、関数内部で状態を変更させて返したい場合がある。値を変更する場合には、参照を使っても全く同じ事が可能であるが、呼び出し側で「値が変更される」ことをわかりやすくするためにポインタを使う。(参照による呼び出しでは、一見すると値渡しのようにも見えるため、C言語出身の私は値が変更されないようなイメージを受ける。)

class SomeLargeClass;

int foo(SomeLargeClass *plarge)
{
    ....
}

int main()
{
    SomeLargeClass large;

    foo(&large); // ポインタで渡す。
    ...
}

"任意で戻り値を得る"あるいは"オブジェクト引数を省略する"場合にもポインタ

これはポインタでないとできない処理である。関数が複数の値を返したい場合、Cからの流れで言うと複数の変数へのポインタを受け取り、そこに値を入れて返すのが普通である。例えば次のような関数SplitTimeだ。これは、第1引数に受け取ったミリ秒を時、分、秒に分けて返す。

C++的にはどうするのが綺麗なのだろうか。値を2つ返す場合にはSTLのpairが活用できそうだし、実際そうしている例も見かけたのだが。

void SplitTime(int msec, int *phour, int *pminute, int *psecond);

int msec, hour, minute, second;
SplitTime(msec, &hour, &minute, &second);

だが、常に全ての引数を利用したいわけではない。時と分だけ得られれば秒は無視して構わない場合もあるだろう。その時に次のようにできると便利である。

int msec, hour, minute;
SplitTime(msec, &hour, &minute, NULL);

もちろんこの時、関数内部では渡されたアドレスがNULLでないかをチェックする必要がある。

void SplitTime(int msec, int *phour, int *pminute, int *psecond)
{
   // ここで処理を行う
   ...
   if(phour) *phour = ...// NULLでなければ値を返す
   if(pminute) *pminute = ...
   if(psecond) *psecond = ...

   return;
}

ポインタを使わないと実現できない構造

例えばリストなどを作る場合は、「次のノードへのポインタ」をメンバとして設け、それがNULLだったらリストの終わりである、と判定するようにするとデータ構造がシンプルで見通しのよいものになる。また、リストの挿入はポインタの指す先を繋ぎ変えるだけの処理で済む。

参照にはNULLポインタにあたる概念がなく、またポインタのように指す場所を変えることもできないため、このようなデータ構造を表現することはできない。

まあ、C++なんだからリストくらいSTLにやらせろ、というのが正論か。

ちょっとインチキか?

上記のように、オブジェクトを変更するのでポインタで渡したとする。だが関数内で操作する際には、"->"でメンバを呼び出すよりも"."で呼んだ方がコードが書きやすいし見やすい。そこで、私は次のような手法を使うことがある。

int foo(SomeLargeClass *plarge)
{
    SomeLargeClass &refparam = *plarge; // ポインタを参照に変えてアクセス

    refparam.TheMethod(...); // plarge->TheMethod(...);と同じ
}

だが、どのオブジェクトに対する操作なのかがわかりにくくなるので、あまり奨められない手法であろう。


戻る

Copyright (C) 2001-2007 Seal Software <sealsoft AT sealsoft.jp>