背景
C++の学習を始めて、参照とかポインタとか、引数の値渡しとか参照渡しとかいまいちよくわからなかったので、特に参照について整理してみました。
前提
参照と、その元になる変数などを想定され得るパターンで初期化、再代入などを行ってみて、それぞれがどのようなふるまいになるかを確認してみました。
一部正式な用語ではない、本記事のみで使用する用語があります。
-
int a = 10における10を本記事では総称して値リテラルと呼んでいます。 -
int a;のような変数の宣言、や宣言のあとに利用されるaを総称して値型と呼んでいます。 -
int& a;のような変数の宣言、や宣言のあとに利用されるaを総称して参照型と呼んでいます。 -
パターン表で
左辺、右辺という言い方を使ってますが、右辺値参照とかでの意味合いではなく単にA=Bという変数の初期化や代入式のAを左辺、Bを右辺と呼んでいるだけです。
わかったことをざっくり整理
厳密な仕様はさておき、いったん私の頭で整理してみた内容をさっくりまとめると以下のようになります。
そもそも変数とその参照とはなにか
- 変数の宣言とは、メモリ上のアドレスの確保である。変数はアドレスに対するラベルのようなもの。
- ひとつのアドレスに対して、複数の変数を指定することができる。
- 参照とは、元の変数に対して複数指定できる変数のことである。つまり、同一アドレスの別名である。
参照の振る舞い
- 参照などもったいぶった名前がついてはいるが、参照が元の変数の別名(同一アドレスの別名)でしか無いなら、元の変数と振る舞いは何ら変わらない(変数定義の仕方は&がついたりで異なるが、その後の振る舞いは元の変数と同じ)
- ただし、同じアドレスに対して複数の別名が存在するということは、ある一つの別名に対する操作がその他すべての別名に影響がある、ということ。引数の参照渡しとはこの原理を利用する。
変数の再代入
- 変数を再代入しても、それが通常の変数であろうが参照であろうが、初期化時のアドレスから更新されることはない。値のみ置き換えられる。
引数の値渡しとは
- 関数の引数の初期化を下記パターン表の①か③で行うこと。すなわち、関数内の引数が関数に渡された変数とは異なるアドレスに値がコピーされる。
引数の参照渡しとは
- 関数の引数の初期化を下記パターン表の⑨か⑪で行うこと。すなわち、関数内の引数が関数に渡された変数と同じアドレスの値を参照する。
確認のパターン表
| 右辺 | ||||||
|---|---|---|---|---|---|---|
| 値リテラル | アドレス演算子 | 値型 | 参照型 | |||
| 左辺 | 値型 | 初期化 | ① 新規アドレス確保、値代入 | ② コンパイルエラー | ③ 新規アドレス確保、値同じ←すなわちコピー | ④ 新規アドレス確保、値同じ←すなわちコピー |
| 再代入 | ⑤アドレス同じ、値更新 | ⑥コンパイルエラー | ⑦アドレス同じ、値更新 | ⑧アドレス同じ、値更新 | ||
| 参照型 | 初期化 | ⑨ 新規アドレス確保、値代入 | ⑩ コンパイルエラー | ⑪ 値型のアドレスで初期化、値型の値 | ⑫ 参照型 のアドレスで初期化、参照型の値 | |
| 再代入 | ⑬ アドレス同じ、値更新 | ⑭ コンパイルエラー | ⑮ アドレス同じ、値更新 | ⑯ アドレス同じ、値更新 | ||
挙動の確認
① 値型を値リテラルで初期化
int a = 10; // 値型を値リテラルで初期化
std::cout << "a = " << a << " &a = " << &a << std::endl;
a = 10 &a = 0x7ffe7909889c
② 値型を値リテラルで再代入
int a = 10;
std::cout << "a = " << a << " &a = " << &a << std::endl;
a = 20; // 値型を値リテラルで再代入
std::cout << "a = " << a << " &a = " << &a << std::endl;
a = 10 &a = 0x7ffcc922655c
a = 20 &a = 0x7ffcc922655c
初期化時のaのアドレスは同じまま、値が更新される。
③ 値型をアドレス演算子で初期化
int a = 10;
int b = &a; // 値型をアドレス演算子で初期化→コンパイルエラー
アドレス演算子はポインタ型(int*)なので値型(int)に代入しようとしてエラーとなる。
④ 値型をアドレス演算子で再代入
int a = 10;
int b = 20;
b = &a; // 値型をアドレス演算子で再代入→コンパイルエラー
③と同じ
⑤ 値型を値型で初期化
int a = 10;
std::cout << "a = " << a << " &a = " << &a << std::endl;
int b = a; // 値型を値型で初期化
std::cout << "b = " << b << " &b = " << &b << std::endl;
a = 10 &a = 0x7ffebf12bb2c
b = 10 &b = 0x7ffebf12bb28
値型変数bに新しいアドレスを割り当てて、aと同じ値を設定する。
⑥ 値型を値型で再代入
int a = 10;
std::cout << "a = " << a << " &a = " << &a << std::endl;
int b = 20;
std::cout << "b = " << b << " &b = " << &b << std::endl;
a = b; // 値型を値型で再代入
std::cout << "a = " << a << " &a = " << &a << std::endl;
a = 10 &a = 0x7ffc4c8ac5ac
b = 20 &b = 0x7ffc4c8ac5a8
a = 20 &a = 0x7ffc4c8ac5ac
値型変数aのアドレスはそのまま、値型変数bの値で更新される。
⑦ 値型を参照型で初期化
int a = 10;
std::cout << "a = " << a << " &a = " << &a << std::endl;
int& b = a;
std::cout << "b = " << b << " &b = " << &b << std::endl;
int c = b; // 値型を参照型で初期化
std::cout << "c = " << c << " &c = " << &c << std::endl;
a = 10 &a = 0x7ffcccb45ff4
b = 10 &b = 0x7ffcccb45ff4
c = 10 &c = 0x7ffcccb45ff0
値型の変数cに新しいアドレスを割り当てて、参照型の変数bの値(値型aと同じ値)を設定する。
⑧ 値型を参照型で再代入
int a = 10;
int& b = a;
std::cout << "b = " << b << " &b = " << &b << std::endl;
int c = 20;
std::cout << "c = " << c << " &c = " << &c << std::endl;
c = b; // 値型を参照型で再代入
std::cout << "c = " << c << " &c = " << &c << std::endl;
b = 10 &b = 0x7fff695cf714
c = 20 &c = 0x7fff695cf710
c = 10 &c = 0x7fff695cf710
値型の変数cのアドレスはそのまま、参照型の変数bの値で更新される。
⑨ 参照型を値リテラルで初期化
int& a = 10; // 参照型を値リテラルで初期化→コンパイルエラー
とおもいきや・・・
const int& a = 10; // 参照型を値リテラルで初期化
std::cout << "a = " << a << " &a = " << &a << std::endl;
a = 10 &a = 0x7ffeef7e1244
この挙動はわからない・・・
⑩ 参照型を値リテラルで再代入
int a = 10;
int& b = a;
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
b = 20; // 参照型を値リテラルで再代入
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
a = 10 &a = 0x7fff8547d2a4
b = 10 &b = 0x7fff8547d2a4
a = 20 &a = 0x7fff8547d2a4
b = 20 &b = 0x7fff8547d2a4
参照型変数b初期化時のアドレスはそのまま、値リテラルの値で更新される。
なお、bはaの参照なので、bを更新した場合はaの値も更新される。
一度aの参照としてbのアドレスが割り当て済なので、値リテラルで値の更新が可能なのは理解できる。
⑪ 参照型をアドレス演算子で初期化
int a = 10;
int& b = &a; // 参照型をアドレス演算子で初期化→コンパイルエラー
intの参照型は実体はintなのでポインタ型(int***)ではないためエラーとなる。
⑫ 参照型をアドレス演算子で再代入
int a = 10;
int b = 20;
int& c = a;
c = &b; // 参照型をアドレス演算子で再代入→コンパイルエラー
⑪と同じ。
⑬ 参照型を値型で初期化
int a = 10;
int& b = a; // 参照型を値型で初期化
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
a = 10 &a = 0x7ffe4d52f094
b = 10 &b = 0x7ffe4d52f094
いわゆるもっともシンプルな参照型の例。
参照型の変数bは値型の変数aと同じアドレスで、値も同じとなる。
つまり、参照型bとはaの別名である。
⑭ 参照型を値型で再代入
int a = 10;
int& b = a; // 参照型を値型で初期化
std::cout << "b = " << b << " &b = " << &b << std::endl;
int c = 20;
std::cout << "c = " << c << " &c = " << &c << std::endl;
b = c; // 参照型を値型で再代入
std::cout << "b = " << b << " &b = " << &b << std::endl;
b = 10 &b = 0x7fff0a50cbe4
c = 20 &c = 0x7fff0a50cbe0
b = 20 &b = 0x7fff0a50cbe4
参照型bのアドレスはそのまま、値型cの値で更新される。
bのアドレスがcのアドレスで更新されるわけではない。
※参照への再代入という言い方は適切でないというコメントをいただきました。便宜上再代入という用語を使用しています。
⑮ 参照型を参照型で初期化
int a = 10;
int& b = a;
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
int& c = b; // 参照型を参照型で初期化
std::cout << "c = " << c << " &c = " << &c << std::endl;
a = 10 &a = 0x7ffefeedc98c
b = 10 &b = 0x7ffefeedc98c
c = 10 &c = 0x7ffefeedc98c
参照型の変数cのアドレスは参照型の変数bのアドレスで初期化され、すなわち値もbと同じとなる(参照型bの実体は値型aと同じなので、とうぜんaの値とも同じ)。
⑯ 参照型を参照型で再代入
int a = 10;
int& b = a;
int& c = b;
std::cout << "c = " << c << " &c = " << &c << std::endl;
int d = 20;
int& e = d;
std::cout << "e = " << e << " &e = " << &e << std::endl;
c = e; // 参照型を参照型で再代入
std::cout << "c = " << c << " &c = " << &c << std::endl;
c = 10 &c = 0x7fff2a76acd4
e = 20 &e = 0x7fff2a76acd0
c = 20 &c = 0x7fff2a76acd4
参照型の変数cのアドレスはそのまま、値は参照型の変数eの値で更新される。
cのアドレスがeのアドレスで更新されるわけではない。
まとめ
参照について整理しようとしていろいろ動作確認してたら発散してしまいましたが、よく聞く
- 参照とは別名(エイリアス)である
というところがいろいろ動かしてみたおかげでその意味するところが体感として理解することができました。
さらに調べてみたいこと
- ではポインタとは?(TODO)
- 値渡しと参照渡しのふるまいの詳細を確認。以下で整理済。
- 【C++】引数の値渡しと参照渡しを動かしながら理解【超初心者向け】