【C++】参照とポインタ

C++

こんにちは。良昌です。

最近cocos2d-xを使う機会があり、初めてC++に触れました。C++について様々な記事を読ませて頂いた中で、私自身が参照、ポインタについての整理を出来ていないという事に気付いたので、これを機に纏めてみたいと思います。

参照とポインタ

私は「結局、参照とポインタってどう使い分けるの?」という疑問を持っていました。ですので、同じ疑問を持っている方に対し、この記事が少しでも参考になればと思っています。私は分かり難い点を少しずつ整理することで、この問題を解決することができましたので、順を追って説明していきたいと思います。


■参照

参照とポインタを使い分けるにあたって、先に参照を理解しておいたほうが、ポインタを理解し易いと思いますので、まずは参照について説明していきたいと思います。

C++における参照の概念は、他言語と同様、「参照先のオブジェクトを指し示すオブジェクト」となります。

宣言は以下2行目のように、「参照先の型& 変数名 = 参照先変数」の形式で宣言します。

int foo = 10;
int& bar = foo;

実体は参照先オブジェクトのアドレスを指しており、大きく以下5つの性質を備えています。

性質1:実体の変更が、参照オブジェクトにも反映される(逆も然り)

これは説明するまでも無いと思いますので、割愛します。

性質2:通常のオブジェクトと同様に扱うことができる

これについてはポイントが3つあります。

★1:値へのアクセスは変数をそのまま使う

特に何も気にせず値にアクセスすることができます。

int foo = 10;
int& bar = foo;
CCLOG("%i", bar); //10が出力される

★2:参照渡しを利用する場合は、呼出し先引数で明示的に宣言する

参照渡しをする場合、呼出し元の引数は特に加工する必要はありません。呼出し先の仮引数が参照になっていれば、自動的に参照渡しとなります。

void func1() {
    int foo = 10;
    func2(foo); //変数をそのまま渡す(※特に加工する必要は無し)
}

void func2(int& bar) { //参照を使うことを明示的に宣言
    ++bar;
}

★3:メンバへのアクセスはドット演算子(.)を使う

void func1(Hoge& hoge) {
    hoge.setVal("hogehoge");
}

性質3:参照が指しているアドレスを変更することはできない

一度宣言した参照の参照先は変更できません。以下は通常の代入と同じ処理となります。

int foo = 10;
int& bar = foo;
CCLOG("%i", foo); //10が出力される
CCLOG("%i", bar); //10が出力される

int baz = 20;
bar = baz; //通常の代入と同処理
CCLOG("%i", foo); //20が出力される
CCLOG("%i", bar); //20が出力される

性質4:参照のみの宣言はできない

参照のみの宣言はコンパイルエラーとなります。

int& foo; //コンパイルエラー

性質5:定数値への参照はできない

定数値への参照はコンパイルエラーとなります。

int& foo = 10; //コンパイルエラー

■ポインタ

続いて、ポインタについての説明となります。

ポインタは「オブジェクトのアドレス」を指し示します。実は参照の実態もポインタであり、構文上ルールを制限されたポインタであると理解をしています。

宣言は以下2行目のように、「ポインタする変数の型* 変数名 = &ポインタする変数」の形式で宣言します。

int foo = 10;
int* bar = &foo;

※ここでのアンパサンド(&)は、オブジェクトのアドレスを取得するための演算子であり、参照で利用するアンパサンド(&)とは、全くの別物となりますので、注意してください。

また、new演算子を利用した場合、返却されるインスタンスはポインタとなりますので、以下の様に宣言します。

Hoge* hoge = new Hoge();

ポインタは多数の性質を備えていますが、上記参照の性質と比較するため、同様の性質について書いていきます。

性質1:実体の変更が、ポインタオブジェクトにも反映される(逆も然り)

参照と同様の性質となります。

性質2:通常のオブジェクトと同様に扱うことができない

こちらについては、参照とは異なる内容となりますので注意してください。

★1:値へのアクセスはアスタリスク(*)を利用する。

参照と異なり、値へアクセスするには変数の前にアスタリスク(*)を付与する必要があります。

int foo = 10;
int* bar = &foo;
CCLOG("%p", bar); //fooのアドレスが出力される
CCLOG("%i", *bar); //10が出力される

★2:ポインタを渡す場合は、呼出し元ではアドレスを渡し、呼出し先引数で明示的に宣言する

ポインタを渡す場合、参照とは異なり、呼出し元の引数はアドレスを設定する必要があります。また、呼出し先の仮引数を明示的にポインタとします。

void func1() {
    int foo = 10;
    func2(&foo); //アドレスを渡す
}

void func2(int* bar) { //ポインタを使うことを明示的に宣言
    ++(*bar);
}

★3:メンバへのアクセスはアロー演算子(->)を使う

void func1(Hoge* hoge) {
    hoge->setVal("hogehoge");
}

性質3:ポインタが指し示しているアドレスを変更することができる

参照とは異なり、ポインタが指し示しているアドレスは変更可能です。

int foo = 10;
int* bar = &foo;
CCLOG("%i", foo); //10が出力される
CCLOG("%i", *bar); //10が出力される

int baz = 20;
bar = &baz; //参照先の変更
CCLOG("%i", foo); //10が出力される
CCLOG("%i", *bar); //20が出力される

性質4:ポインタのみの宣言ができる

ポインタのみの宣言は可能ですが、使い方を誤ると実行時エラーが発生するので注意が必要です。

int* foo;
CCLOG("%p", foo); //保障されていないアドレスが入っている。
int bar = 10;
foo = &bar; //利用したい場合は、ポインタで初期化する。

性質5:定数値への参照はできない

参照と同様コンパイルエラーとなります。

int* foo = 10; //コンパイルエラー

■まとめ

如何でしたでしょうか。ポインタについては配列を利用するケースなどもっと深い話がありますが、今回の観点は「参照とポインタで何が違うのか?」ということでしたので、上記の様に纏めてみました。

肝心の「参照とポインタ」の使い分けについてですが、結論としては、ケースバイケースです。これに関しては、色々と記事を読みましたが、著者により内容が違っており、一概にこれが良いという形はありませんでした。

一般的には、アドレスに関与できない参照の方が安全とされていますが、cocos2d-xのようにポインタばかりを利用しているFWを利用する場合は、参照に変換する手間を考えると、そのままポインタを利用した方が効率が良いように思えます。

肝心なことは、両者の性質をよく理解し、使い分けることだと思いますので、この記事を、読者の方々に合っている使い分け方を探すための一つの指標として頂ければ幸いです。

コメントを残す

メールアドレスが公開されることはありません。