【C++】参照がわからなかったので整理した【超初心者向け】で変数の初期化やら参照やらをいろいろなパターンで動作確認して理解しました。
これを踏まえて、では引数の値渡しと参照渡しって具体的に何?というのを理解するのが本記事の目的となります。
前提
値渡しと参照渡しの例として教科書でだいたい出てくるswap関数を実際に動かしながら理解を進めます。
swap関数は、引数を2つ受け取りそれぞれの引数の値を入れ替えます。そのうえで
- 値渡しの場合、関数内で2つの値を入れ替えても呼び出し元の値は入れ替わらない。それはなぜか?
- 参照渡しの場合、関数内で2つの値を入れ替えたら呼び出し元の値も入れ替わる。それはなぜか?
を理解したいと思います。
値渡し
#include <iostream>
void swapValue(int x, int y)
{
int tmp = y;
std::cout << "x = " << x << " &x = " << &x << std::endl;
std::cout << "y = " << y << " &y = " << &y << std::endl;
std::cout << "tmp = " << tmp << " &tmp = " << &tmp << std::endl;
y = x;
x = tmp;
std::cout << "x = " << x << " &x = " << &x << std::endl;
std::cout << "y = " << y << " &y = " << &y << std::endl;
}
int main()
{
int a = 10;
int b = 20;
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
swapValue(a, b);
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
}
a = 10 &a = 0x7ffd2d5e2fbc
b = 20 &b = 0x7ffd2d5e2fb8
x = 10 &x = 0x7ffd2d5e2f7c
y = 20 &y = 0x7ffd2d5e2f78
tmp = 20 &tmp = 0x7ffd2d5e2f8c
x = 20 &x = 0x7ffd2d5e2f7c
y = 10 &y = 0x7ffd2d5e2f78
a = 10 &a = 0x7ffd2d5e2fbc
b = 20 &b = 0x7ffd2d5e2fb8
main関数で変数a
とb
が初期化され、swapValue
関数に引き渡されます。
swapValue
関数で変数x
と変数y
が初期化されますが、x
、y
はそれぞれa
、b
とは異なるアドレスで初期化されます。
なので、swapValue内で2つの引数の値を入れ替えても呼び出し元には一切影響が及びません。
引数の値渡しとは【C++】参照がわからなかったので整理した【超初心者向け】のパターン表で言うところの、①か③で行うこと。すなわち、関数内の引数が関数に渡された変数とは異なるアドレスに値がコピーされる、ということにほかなりません。
参照渡し
#include <iostream>
void swapRef(int& x, int& y)
{
int tmp = y;
std::cout << "x = " << x << " &x = " << &x << std::endl;
std::cout << "y = " << y << " &y = " << &y << std::endl;
std::cout << "tmp = " << tmp << " &tmp = " << &tmp << std::endl;
y = x;
x = tmp;
std::cout << "x = " << x << " &x = " << &x << std::endl;
std::cout << "y = " << y << " &y = " << &y << std::endl;
}
int main()
{
int a = 10;
int b = 20;
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
swapValue(a, b);
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
}
a = 10 &a = 0x7ffdbe17396c
b = 20 &b = 0x7ffdbe173968
x = 10 &x = 0x7ffdbe17396c
y = 20 &y = 0x7ffdbe173968
tmp = 20 &tmp = 0x7ffdbe17393c
x = 20 &x = 0x7ffdbe17396c
y = 10 &y = 0x7ffdbe173968
a = 20 &a = 0x7ffdbe17396c
b = 10 &b = 0x7ffdbe173968
main関数で変数a
とb
が初期化され、swapRef
関数に引き渡されます。
swapRef
関数で変数x
と変数y
が初期化されますが、x
、y
はそれぞれa
、b
とは同じアドレスで初期化されます。
なので、swapRef内で2つの引数の値を入れ替えたら呼び出し元に影響が及びます。
引数の値渡しとは【C++】参照がわからなかったので整理した【超初心者向け】のパターン表で言うところの、⑨か⑪で行うこと。すなわち、関数内の引数が関数に渡された変数と同じアドレスの値を参照する、ということにほかなりません。
もうひとつ特筆すべきは、swapValue.cpp
とswapRef.cpp
の差分が、main
から呼ばれるswapXXX
の引数の宣言のみ(int
なのか、int&
なのか)であるということです。
つまり、参照渡しと言いつつ宣言の仕方が少し異なるだけで、その呼び出し方も、変数の使い方も値渡しの場合と全く同じ、ということです。
でも、昔教科書で習ったのとちょっとちがう?
プログラマになったばかりの頃C言語の教科書で習った参照渡しってポインタを使っていたような?
参照渡し(クラシック版)
#include <iostream>
void swapRef(int* x, int* y)
{
// ポインタの参照先がない場合があるので本来はx、yのnullチェックが必要。本記事では省略。
int tmp = *y;
std::cout << "x = " << *x << " &x = " << x << std::endl;
std::cout << "y = " << *y << " &y = " << y << std::endl;
std::cout << "tmp = " << tmp << " &tmp = " << &tmp << std::endl;
*y = *x;
*x = tmp;
std::cout << "x = " << *x << " &x = " << x << std::endl;
std::cout << "y = " << *y << " &y = " << y << std::endl;
}
int main()
{
int a = 10;
int b = 20;
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
swapValue(&a, &b);
std::cout << "a = " << a << " &a = " << &a << std::endl;
std::cout << "b = " << b << " &b = " << &b << std::endl;
}
a = 10 &a = 0x7fffdd75c7bc
b = 20 &b = 0x7fffdd75c7b8
x = 10 &x = 0x7fffdd75c7bc
y = 20 &y = 0x7fffdd75c7b8
tmp = 20 &tmp = 0x7fffdd75c78c
x = 20 &x = 0x7fffdd75c7bc
y = 10 &y = 0x7fffdd75c7b8
a = 20 &a = 0x7fffdd75c7bc
b = 10 &b = 0x7fffdd75c7b8
結果はswapRef.cpp
と同じです。
swapRef
関数の引数がポインタになっており、ポインタを介して呼び出し元の変数にアクセスします。
関数呼び出しのタイミングでアドレス演算子でポインタを取得したり、swapRef
関数で参照外しを行ったり、swapRef.cpp
に比べて記述が少し複雑になります。
また、このswapRef
呼び出し方自体は厳密にはポインタの値渡しであって、呼び出し元のポインタと関数で初期化されたポインタは参照する先は同じでも実体としては別ということになります。
まとめ
- 値渡しは引数に渡す値を別アドレスにコピーした別の実体なので、関数内で値を書き換えても呼び出し元に影響ない。
- 参照渡しは引数に渡す値と同じ実体なので、関数内で値を書き換えたら呼び出し元に影響がある。
- C言語でいうところの参照渡しはポインタを介した参照の受け渡しだったが、C++は変数の別名を受け渡すだけで、より使いやすくなった。
ということが、実際に動かしながら確認してよく理解できました。