Edited at

【C++】引数の値渡しと参照渡しを動かしながら理解【超初心者向け】

【C++】参照がわからなかったので整理した【超初心者向け】で変数の初期化やら参照やらをいろいろなパターンで動作確認して理解しました。

これを踏まえて、では引数の値渡しと参照渡しって具体的に何?というのを理解するのが本記事の目的となります。


前提

値渡しと参照渡しの例として教科書でだいたい出てくるswap関数を実際に動かしながら理解を進めます。

swap関数は、引数を2つ受け取りそれぞれの引数の値を入れ替えます。そのうえで


  • 値渡しの場合、関数内で2つの値を入れ替えても呼び出し元の値は入れ替わらない。それはなぜか?

  • 参照渡しの場合、関数内で2つの値を入れ替えたら呼び出し元の値も入れ替わる。それはなぜか?

を理解したいと思います。


値渡し


swapValue.cpp

#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関数で変数abが初期化され、swapValue関数に引き渡されます。

swapValue関数で変数xと変数yが初期化されますが、xyはそれぞれabとは異なるアドレスで初期化されます。

なので、swapValue内で2つの引数の値を入れ替えても呼び出し元には一切影響が及びません。

引数の値渡しとは【C++】参照がわからなかったので整理した【超初心者向け】のパターン表で言うところの、①か③で行うこと。すなわち、関数内の引数が関数に渡された変数とは異なるアドレスに値がコピーされる、ということにほかなりません。


参照渡し


swapRef.cpp

#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関数で変数abが初期化され、swapRef関数に引き渡されます。

swapRef関数で変数xと変数yが初期化されますが、xyはそれぞれabとは同じアドレスで初期化されます。

なので、swapRef内で2つの引数の値を入れ替えたら呼び出し元に影響が及びます。

引数の値渡しとは【C++】参照がわからなかったので整理した【超初心者向け】のパターン表で言うところの、⑨か⑪で行うこと。すなわち、関数内の引数が関数に渡された変数と同じアドレスの値を参照する、ということにほかなりません。

もうひとつ特筆すべきは、swapValue.cppswapRef.cppの差分が、mainから呼ばれるswapXXXの引数の宣言のみ(intなのか、int&なのか)であるということです。

つまり、参照渡しと言いつつ宣言の仕方が少し異なるだけで、その呼び出し方も、変数の使い方も値渡しの場合と全く同じ、ということです。


でも、昔教科書で習ったのとちょっとちがう?

プログラマになったばかりの頃C言語の教科書で習った参照渡しってポインタを使っていたような?


参照渡し(クラシック版)


swapRefClassic.cpp

#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++は変数の別名を受け渡すだけで、より使いやすくなった。

ということが、実際に動かしながら確認してよく理解できました。