コンテンツにスキップ

【比較】値渡しと参照渡しの違い

概要

関数に引数を渡すとき、「値渡し(pass by value)」は実引数の値をコピーして仮引数を初期化します。仮引数は呼び出し元の変数とは別の変数なので、関数内で仮引数を書き換えても呼び出し元の変数には影響しません。一方「参照渡し(pass by reference)」は仮引数を呼び出し元の変数そのものの別名(エイリアス)にします。C++ の T& 引数がこれに当たり、関数内で仮引数に代入すると呼び出し元の変数自体が書き換わります。

この2つが激しく混同される原因は、Java・Python・JavaScript のように「変数がオブジェクトへの参照を保持する」言語の挙動です。これらの言語では、関数に渡したオブジェクトの中身(フィールドや要素)を変更すると呼び出し元にも反映されるため、「参照渡しだ」と誤解されがちです。しかし正確には、これらは参照の値をコピーして渡す値渡し(call by sharing/call by object reference と呼ばれる)であり、真の参照渡しではありません。本記事はこの区別を一次仕様に基づいて明確にします。

例えるなら:住所のコピー vs 同じ部屋の合鍵

値渡しは「友人に家の住所をメモして渡す」イメージです。友人が自分のメモを書き換えて別の住所にしても、あなたの住所は変わりません(再代入は伝播しない)。ただしメモに書かれた住所の家に友人が行って家具を動かせば、あなたが帰宅したとき家具は動いています(オブジェクトの中身の変更は共有される)。これが Java/Python/JS の挙動です。一方、真の参照渡しは「あなたの部屋の合鍵を渡し、しかも「これ以降あなたの部屋=この鍵が指す部屋」と決める」ようなもので、相手が鍵を別の部屋に付け替えると、あなたの部屋自体が別の部屋にすり替わります(呼び出し元変数の再束縛が伝播する)。比喩は再代入の伝播有無を掴むのに有効ですが、実装上のスタック/ヒープ配置とは別の話である点に注意してください。

詳細比較

項目 値渡し (Pass by value) 参照渡し (Pass by reference)
仮引数の正体 実引数の値で初期化された新しい独立変数(コピー) 呼び出し元変数の別名(エイリアス)。同一の記憶域を指す
呼び出し元変数への再代入の伝播 しない(仮引数 = 別変数なので) する(仮引数への代入 = 呼び出し元変数への代入)
代表的な言語/構文 C の通常引数、Java の全引数、Python・JavaScript の全引数、Go の通常引数 C++ の T&/T&&、C# の ref/out、Pascal の var、Fortran(既定)(T&& はムーブ用で一時オブジェクト束縛。呼び出し元変数の書き換え判定は T& で見る)
参照型/オブジェクトの「中身」変更 共有される(参照の値がコピーされ同じオブジェクトを指すため)。これが「参照渡しに見える」原因 共有される(そもそも同一変数なので当然)
呼び出し元のポインタ/参照を関数で差し替えたい C ではポインタを値渡しし *p 経由で間接操作(ポインタのポインタ等)。Java/Python は不可(再束縛は伝播しない) C++ の T*&/T& で呼び出し元の変数・ポインタを直接差し替え可能
C 言語での扱い 常に値渡し。配列は先頭要素へのポインタに decay し、そのポインタ値がコピーされる(配列自体のコピーではない) 言語機能としては存在しない(ポインタの値渡しで模倣する)
Java の仕様上の定義 JLS 8.4.1:実引数式の値が新しく生成された仮引数変数を初期化する=値渡し。参照型では「値」がオブジェクト参照(ポインタ)である Java に参照渡しは存在しない

C++ の void swap(int& a, int& b) は a が呼び出し元の x そのものになる真の参照渡しで、ISO C++ FAQ も参照を「オブジェクトの別名であり、i is x」と説明します。対して Java は JLS 8.4.1 が「メソッド/コンストラクタ呼び出し時、実引数式の値が新しく生成された仮引数変数を初期化する」と定め、参照型ではその「値」がオブジェクトへの参照(ポインタ)です。Python 公式 FAQ も「引数は代入によって渡され、代入は単にオブジェクトへの参照を作るだけなので呼び出し元と呼び出し先で別名は生じず、結果として call-by-reference は存在しない」と明記しています。

よくある誤解

  • 「Java は参照渡し(オブジェクトを渡すから)」は誤り。 Java は例外なく値渡しです(JLS 8.4.1)。オブジェクトを渡したとき共有されているのはオブジェクトへの参照の値のコピーであって、呼び出し元の変数そのものではありません。だから obj.setX(1) のような中身の変更は呼び出し元に反映されますが、メソッド内で obj = new Foo() と再代入しても呼び出し元の変数は元のオブジェクトを指したままです。真の参照渡しなら後者も伝播します。この一点が決定的な見分け方です。

  • 「Python は変更が反映されるから参照渡し」も誤り(同じ誤解)。 Python 公式 FAQ は「引数は代入で渡され、call-by-reference は存在しない」と明言しています。リストへの a[0] = ... のようなミューテーションは同じオブジェクトを共有しているので反映されますが、a = [...] のようなリバインド(再束縛)は呼び出し元に伝播しません。挙動が分かれるのは「値渡しか参照渡しか」ではなく「ミュータブルなオブジェクトを破壊的に変更したか、名前を別オブジェクトに付け替えたか」の違いです。

  • 「ポインタを渡せば C は参照渡しになる」は不正確。 C には参照渡しという言語機能はありません。f(&x) でできるのは「x のアドレスという値をコピーして渡す」値渡しであり、関数は受け取ったポインタを通じて *p で間接的に x を書き換えているだけです。ポインタ変数自体を関数内で別アドレスに変えても呼び出し元には伝わりません(伝えたければポインタのポインタを渡す)。一方 C++ の T& は言語レベルの真の参照渡しで、両者は別物です。

  • 「値渡しは必ず重い(全部コピーするから遅い)」は過度な一般化。 値渡しでコピーされるのはその変数の値だけです。Java/Python/JS でオブジェクトを渡すときコピーされるのは参照(ポインタ)1個分であり、巨大なオブジェクト全体を複製しているわけではありません。C++ で構造体を値渡しすると本当に全体がコピーされますが、それを避けたいときに const T&(const 参照渡し)を使う、という最適化判断とは別の話です。

実務での選び分け

  • C++ で関数に呼び出し元の変数を書き換えさせたい → 非 const 参照 T&(または出力用ポインタ)。意図が型に表れる。
  • C++ で大きなオブジェクトを変更せず渡したい(コピーコスト回避) → const T&。値の不変性を保ちつつコピーを避ける。
  • Java/Python/JS で「関数に値を返してもらいたい」 → 戻り値を使う。引数の再代入で返そうとしても伝播しないので設計が破綻する。複数値ならタプル/レコード/専用クラスを返す。
  • Java/Python/JS で意図せぬ副作用を避けたい → 共有オブジェクトをミューテートする関数は要注意。防御的コピーや不変(immutable)オブジェクトの採用を検討する。
  • C で関数に呼び出し元を変更させたい → アドレス(ポインタ)を渡し、ポインタ自体を差し替える必要があるならポインタのポインタを渡す。

ひとことまとめ

値渡しは「実引数の値をコピーして別の仮引数を作る」方式で再代入は伝播せず、真の参照渡しは「仮引数を呼び出し元変数の別名にする」方式で再代入も伝播します。Java/Python/JS は参照の値を渡す値渡し(call by sharing)であって参照渡しではなく、見分け方は「仮引数への再代入が呼び出し元に伝わるか」です。

出典・参考