ポインタと参照渡し、ポインタ渡しについて
競プロを始めたのと同時にC++を始めた私ですが、ポインタについてはなんとな〜くの理解で放置してきました。ポインタについて調べてたら、参照渡し、ポインタ渡しについて知ることになり、その違いや使い方、そもそもポインタについても理解が怪しかったので一回まとめてみました。
基本 - ポインタとアドレスについて
&について
私たちが何気なく
int a = 1;
のように変数を宣言すると、その値はコンピュータのメモリに記録されます。そして、そんな値が入っているメモリの位置をアドレスと言います。よく住所で例えられるやつですね。
普段アドレスを見ることはありませんが、変数の前に&をつけることで値が記録されているアドレスを見ることができます。
int a = 1;
cout << a << endl; // 1
cout << &a << endl; // 0x50527c
上の場合、「0x50527c」というのが変数aの値である1という値が入っているアドレスを指します。
ちなみに、私がC++を初めて学んだ際に見た工学院大学のサイトに、「&(アンド)はアドレス」といった覚え方が紹介されてました。私はこれで今でも覚えています。
*について
*(アスタリスク)についてはC++では3通りの使い方があります。1つは「掛け算」を表しますが、今回のトピックとは関係ないので今回は省略します。
「ポインタ変数の宣言」としての*
先ほど紹介したアドレスを持っている変数を”ポインタ変数”と言います。
*はポインタ変数の宣言として用いることができます。
int a = 1;
int *b = &a;
cout << b << endl; // 0x50527c
ここでは、2行目でポインタ変数bを宣言し、1という値を格納してある変数aのアドレスを代入しています。
先ほど紹介した通り、ポインタ変数はアドレスを保持している変数なので、仮に&を書き忘れて
int a = 1;
int *b = a;
と書いてしまうと、「aというint型をポインタ変数bに代入する」という意味になるので、型が合わずコンパイルエラーになります。
「ポインタ変数の実体」としての*
さらにアスタリスクはポインタ変数が指すアドレスに記録された値、つまりポインタ変数の実体という意味で*を使うこともできます。
ポインタ変数にアスタリスクがつけられた場合に、「ポインタ変数の実体」を表すようになります。
例えば、
int a = 1;
int *pointer = &a;
cout << pointer << endl; // 0x50527c
cout << *pointer << endl; // 1
上のようにポインタ変数pointerを宣言し、変数aのアドレスを代入します。
その後、アスタリスクをつけずにpointerとだけ書けば、アドレスを指すポインタ変数として働きますが、最後の行のようにアスタリスクをつけることで、ポインタ変数pointerが指すアドレスの実体(今回は1)を意味するようになります。
本題 - 値渡し、参照渡し、ポインタ渡しについて
さて、ここからが本番です。まずは値渡しから。
値渡し
一番最初に覚えるやり方が値渡しだと思います。関数に値をコピーして渡すようなイメージで、呼び出し元の変数に変更は加わりません。
void addOne(int x) {
x += 1;
}
int main() {
int a = 1;
addOne(a);
cout << a << endl; // 1
}
引数に対して、1を加えるaddOne関数を定義してみました。main()関数から値1を格納した変数aを引数としてaddOne()に渡していますが、あくまでaddOne()に渡されるのは1というaの値"だけ"なので、呼び出し元の変数aの内容は変わっていません。
参照渡し
続いて参照渡しについて。まず、参照とはある変数に別の名前をつけることを言います。
参照渡しでは呼び出し元の変数に別名をつけて扱うことを可能にします。値渡しと違うのは、呼び出し元の変数にもアクセスできるようになり、値を変更した際にはその変更が呼び出し元の変数にも影響する点です。
void addOne(int &x) {
x += 1;
}
int main() {
int a = 1;
addOne(a);
cout << a << endl; // 2
}
上のコードは値渡しだったaddOne()を参照渡しに変えたコードです。違いはaddOne()の仮引数xに&がついている点です。このようにすることで参照を示すことができます。
「&はアドレス」と先ほど紹介がありましたが、実はC++では、&にはアドレスの意味である”アドレス演算子”としての&と、今回のように参照を意味する”参照演算子”としての&があるようです。ここが紛らわしく、かなり混乱したので注意してください。
参照渡しでは呼び出し元の変数(今回はa)に変更が加わり、出力が2になります。
ポインタ渡し
最後にポインタ渡しについて。呼び出し元の変数のアドレスを引数として関数に渡します。関数は引数として渡されたアドレスが格納されたポインタ変数を使って呼び出し元の変数にアクセスでき(間接参照)、参照渡しと同様に値を変更した際にはその変更が呼び出し元の変数にも影響します。
void addOne(int* x) {
*x += 1;
}
int main() {
int a = 1;
addOne(&a);
cout << a << endl; // 2
}
上のコードはaddOne()をポインタ渡し仕様に書き換えたものです。説明した通り、関数に渡される引数はアドレスなので、addOne()の引数xがポインタ変数としてint* xとアスタリスクをつけています。
そして、*x += 1;とすることで、ポインタ変数であるxの実体に対して1を足すという意味になります。
参照渡し同様、addOne()での変更は呼び出し元の変数に影響を及ぼしています。
参照渡しとポインタ渡しってどう使い分けるの?
結果だけ見ると、参照渡しもポインタ渡しも呼び出し元の変数に影響を与えるという点で同じです。使い分けについては諸説あるそうですが、参照渡しの方がポインタ渡しより安全で、制約が多いという違いがあるそうです。
あとは、呼び出し元でも呼び出された関数側でも変数の前に&とか*とかをいちいちポインタ渡しの時のようにつけなくて良くなるので、見やすさといった点でも参照渡しに軍配が上がりそうです。参照渡しはCには無く、C++にのみある機能なようなので、せっかくC++を使うなら参照渡しを積極的に使うのがいいかもしれません。
この辺は詳しくないし、この記事で紹介したい内容をちょっと超えてしまうため、本記事の参考文献や「C++ 参照渡し ポインタ渡し 使い分け」とかでググってみてください(丸投げ)。
参考文献
Discussion