C++ の値渡しと参照渡し
この記事のゴール
- 値渡し (pass‑by‑value) と 参照渡し (pass‑by‑reference) の振る舞いを、コードと図解で直感的に理解する
- コピーコスト、所有権、
const
修飾など実務で重要になる観点を押さえる - よくあるバグとベストプラクティスを先回りで学ぶ
1. 値渡しとは?
- 関数呼び出し時に実引数のコピーを作成して渡す方式
- 関数内で値を変更しても呼び出し元には影響しない
- 終了時にコピーは破棄される
void increment(int x) { // 値渡し
++x; // ここだけ 1 増える
}
int n = 5;
increment(n);
// n は 5 のまま
ポイント
- 小さな POD 型 (
int
,double
など) ならコピーコストは軽微 - 大きなオブジェクト (
std::vector
, 自前のクラス) はムダなメモリ確保が発生しやすい
2. 参照渡しとは?
- 引数に エイリアス (別名) を渡すイメージ
- 関数内での変更が呼び出し元にも反映される
void increment(int& x) { // 参照渡し
++x;
}
int n = 5;
increment(n); // n == 6
参照 (&
) の特徴
-
nullptr
にならない (安全) - 初期化必須
- 一度バインドすると別オブジェクトに付け替え不可
3. const
参照 ― “変更しない参照”
- 読み取り専用アクセスを保証
- 大きなオブジェクトを安全かつ高速に受け取れる
void printSize(const std::vector<int>& v) {
std::cout << v.size() << "\n";
}
ベクトルをコピーすると O(N) だが、
const&
ならポインタ相当の O(1)。
4. 参照 vs ポインタ
-
ポインタ (
*
) はnullptr
可・再代入可・算術演算可 - 参照 は C++ 的に「より安全なポインタ」
- モダン C++ ではまず参照を使い、必要ならスマートポインタへ
void setNull(int* p) { p = nullptr; } // ポインタは null 可
void addOne(int* p) { ++(*p); }
5. コピーコストとムーブセマンティクス (C++11+)
- 値渡し→コピーが高い場合はムーブで受け取るテクニック
void takeVec(std::vector<int> v); // ① コピー
void takeVec(std::vector<int>&& v); // ② ムーブ (Rvalue 参照)
std::move
を組み合わせることで不要なコピーを回避できるが、まずは const&
を基本にする。
6. よくあるミスとバグ
-
参照渡しなのに
const
を付け忘れ → 関数内で誤って書き換え - 値渡しだと勘違いして参照を渡す → 呼び出し側のデータが変わる
- ローカル変数の参照を返す → ぶら下がり参照 (ダングリング)
int& bad() {
int local = 42;
return local; // ❌ local は関数終了で破棄
}
7. ベストプラクティス
-
小さな型 → 値渡しで OK (
int
,double
,bool
) -
大きな型 or 変更しない →
const&
-
呼び出し元を更新したい → 参照渡し (
&
) -
リソース移譲 → Rvalue 参照 (
&&
) +std::move
- API 設計 ではコピーコストと安全性のトレードオフを明示
8. まとめ
- 値渡しはコピー、参照渡しはエイリアス
-
const
を活用して 読み取り専用参照 を基本形に - 大きなデータ構造はコピーせず参照で渡す → パフォーマンス改善
- ムーブセマンティクス(C++11+)を覚えるとコピーコストをさらに削減
実務で迷ったら次の順に考えよう:
- 引数を変更する必要がある?
- コピーコストは許容範囲?
- 読み取り専用なら
const&
、変更するなら&
。