背景
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++】引数の値渡しと参照渡しを動かしながら理解【超初心者向け】