C++ Learning

第8回 参照と参照渡し

C で「関数から呼び出し元の変数を書き換えたい」と思ったら、ポインタで渡すのが定石でした。swap(&x, &y) のように呼び出し側で & を書き、関数内で *p と逐一間接参照するあのパターンです。C++ では 参照渡し(T&が加わり、この多くを置き換えられます。呼び出し側は普通に swap(x, y)、関数内も a をそのまま触るだけ。本章ではまず参照渡しの書き方を C と対比して身につけ、そのあとで「なぜそんなことができるのか=参照は変数の別名」という仕組みを補足で整理します。

このページで押さえること
✅ 最低限ここだけ覚える
  • 関数内で書き換えたい → 参照渡し T&(C の T* を置き換える)
  • 読むだけ・T が大きい → const T&(コピーを避ける定石)
  • 呼び出し側は & 不要、関数内も * 不要 ― 普通の変数のように触れる
  • 参照は「変数の別名」なので NULL にならず、繋ぎ直せない
⭐ 余裕があれば読む
  • 参照 vs ポインタの使い分け基準
  • const T&一時オブジェクトも受け取れる
  • 参照を返す関数とダングリング参照の罠
  • 範囲 for の for (auto& x : v) の意味

1. swap で見る ― 参照渡しの威力(C との対比)

C 経験者にとって参照渡しの価値が一番わかりやすいのは swap 関数です。同じ「2 つの値を入れ替える」を、C(ポインタ渡し)と C++(参照渡し)で書き比べます。

swap.cC
// ポインタで受け取る void swap(int* a, int* b) { int t = *a; *a = *b; *b = t; } int main(void) { int x = 1, y = 2; swap(&x, &y); // & を忘れるとバグ }
呼び出し側に & が必須/関数内は *a / *b
swap.cppC++
// 参照で受け取る void swap(int& a, int& b) { int t = a; a = b; b = t; } int main() { int x = 1, y = 2; swap(x, y); // そのまま渡す }
呼び出し側は 通常の変数渡しのまま

違いは大きく 2 つ。

バグが減る: C の swap は呼び出し側の & を忘れると、整数をアドレスだと解釈して落ちる(未定義動作)。C++ の参照では構文レベルで「アドレスを渡し忘れる」ことが起き得ません。

2. 参照渡しの書き方 ― T& で受け取る

§1 の swap で見たように、C++ では「関数内で呼び出し元の変数を書き換えたい」ときは、引数の型を T& と書くだけで済みます。C でポインタを使っていた場面を置き換える最頻出パターンです。もう少し小さな例で書き方を固めましょう。

inc.cC(ポインタ渡し)
void inc(int* p) { (*p)++; // * を書く必要あり } int n = 5; inc(&n); // & を書く必要あり // n は 6 になる
& / * を両方書く/NULL を渡せてしまう
inc.cppC++(参照渡し)
void inc(int& r) { r++; // 普通の変数と同じ書き方 } int n = 5; inc(n); // そのまま渡す // n は 6 になる
呼び出し側も関数内も 記号ゼロ/NULL は不可能

差分は小さく見えて、実務で効く違いが 3 つあります。

C から C++ への移行イメージ: T* pT& r に書き換え、関数内の *pr に、呼び出し側の f(&x)f(x) に置換する ― これだけで「書き換え用の引数」はたいてい参照渡しに寄せられます。T* は「NULL を許したい/関数内で指し直したい」場面にだけ残すのが一般的な方針です。

値渡しとの並び

C と共通の値渡しも含め、書き換えの有無で選び分けます。

値渡し(コピー)C / C++ 共通
void inc(int x) { x++; } int n = 5; inc(n); // n は 5 のまま
呼び出し側は 変わらない(コピーが作られるだけ)
参照渡しC++ で加わった
void inc(int& x) { x++; } int n = 5; inc(n); // n は 6 になる
呼び出し側を 書き換えられる/ポインタ不要
この時点でのまとめ: 「関数内で書き換えたい引数は T&が C++ の第一選択肢。「読むだけだけどコピーしたくない」ときは次節の const T&。「NULL を許す・繋ぎ直す」ときだけ T*。詳しい判断フローは §7 にまとめます。

3. const T& ― 読み取り専用の定石

関数引数で最もよく登場するのが const T&(const 参照)です。コピーコストを避けつつ、関数内で書き換えてしまう事故も防ぐという二重のメリットがあります。

値渡し(大きな型)遅い
struct Student { std::string name; int scores[100]; }; void print(Student s) { // ← コピー発生 std::cout << s.name; }
呼ぶたびに 大きな構造体をコピー
const 参照推奨
struct Student { std::string name; int scores[100]; }; void print(const Student& s) { std::cout << s.name; // s.name = "X"; ← コンパイルエラー }
コピーなし+誤変更も防げる

もう一つの利点:一時オブジェクトを受け取れる

これは意外と大事なポイントです。非 const 参照は一時オブジェクトを束縛できませんが、const T& は受け取れます。

void print_a(std::string& s)       { std::cout << s; }
void print_b(const std::string& s) { std::cout << s; }

print_a("hello");  // ❌ エラー: 一時オブジェクトは非 const 参照に束縛できない
print_b("hello");  // ✅ OK: const 参照は一時オブジェクトも受け取れる
実用上の結論: 読み取り専用の引数は迷わず const T&。呼び出し側が文字列リテラルや式の結果を直接渡せる、ということは API の使い勝手に直結します。標準ライブラリの関数の多くが const T& で受けるのはこのためです。

範囲 for での使い方(予告)

次章以降でよく出てくる書き方です。ここでは「こう書くものだ」とだけ覚えておけば十分です。

std::vector<std::string> names = { "Alice", "Bob" };

for (auto s : names)           { /* s はコピー */ }
for (auto& s : names)          { /* s は参照、書き換え可 */ }
for (const auto& s : names)    { /* s は const 参照、読み取り専用/コピーなし */ }

4. 補足:参照とは(変数の別名)

ここまでで T&const T& の書き方は掴めたはずです。「なぜ呼び出し側の変数が書き換わるのか」を少しだけ掘り下げておきます。使うだけなら §5(vs ポインタ)へ進んで OK。

一行で言うと: 参照は「既存の変数にもう一つの名前を付けた」もの。int& r = n; と書くと、rn と同じメモリ位置を指し、r への操作は n への操作と完全に等価になります。
0x1000
int n
42
(同じアドレス)
int& r
42
int n = 42; int& r = n; と書くと rn別名。同じ箱を 2 つの名前で呼んでいる状態。

関数引数の場合も話は同じで、void inc(int& r)inc(n) と渡すと、r呼び出し側の n の別名になります。だから関数内で r++ すると、呼び出し元の n がそのまま増える ― ポインタ経由で辿り直すのではなく、最初から同じ変数を指しているイメージです。

基本ルール 3 つ

// ① 宣言時に必ず初期化(後から束縛できない)
int n = 10;
int& r = n;        // OK
// int& r;         ← エラー: 参照は必ず初期化

// ② r への操作は n への操作と同じ
r = 20;
std::cout << n;   // 20 が表示される

// ③ 別の変数に「繋ぎ直す」ことはできない
int m = 99;
r = m;             // これは n に 99 を代入するだけ(r は相変わらず n の別名)
宣言の & と式中の & は別物: 宣言文の int& r& は「参照型」を表す印、式中の &n& は「アドレスを取る」演算子。記号は同じでも役割がまったく違います。

5. 参照 vs ポインタ

C 経験者の疑問はたいてい「ポインタがあるのに、なぜ参照があるのか?」です。答えは用途の棲み分け。それぞれの得意分野を比較表で確認します。

項目ポインタ T*参照 T&
初期化 あとで代入してよい(T* p; 必ず初期化T& r = x;
NULL あり(nullptr なし(常に何かを指している)
再束縛 できる(p = &y; できない
間接参照 *p を明示 そのまま r
ポインタ算術 できる(p + 1 できない
配列の先頭 表現できる 単体参照のみ
主な用途 動的配列・NULL を許すケース・古い C API 関数引数・戻り値・範囲 for
覚え方: 参照は「変数そのもの(の別名)」と思えば間違いません。NULL にならない/動き回らない/繋ぎ直せないのは、「変数にそれができないから」です。参照は変数のふりをする仕組みなので、変数と同じ制約に従います。
余裕があれば読む ― ここから先は応用
最低限は上の 5 節で完結(swap/参照渡しの書き方/const 参照/参照とは/vs ポインタ)。関数で参照を返す話はダングリング参照など罠が多いので、必要になったときに読むので OK。

6. 参照を返す関数と落とし穴

関数は参照を返すこともできます。うまく使えば効率的ですが、ローカル変数への参照を返すと破綻するという典型的な罠があります。

危険な例未定義動作
int& bad() { int n = 42; return n; // ← ローカル変数の参照 } int& r = bad(); std::cout << r; // ❌ n は既に消えている(ダングリング参照)
n は関数終了で寿命が尽きる
安全な例OK
class Box { int data; public: int& get() { return data; } // メンバ data はオブジェクトが // 生きている限り有効 }; Box b; b.get() = 10; // ✅ OK
メンバへの参照はオブジェクトの寿命に従う
ダングリング参照(dangling reference): 参照先が既に破棄された状態の参照のこと。未定義動作になり、運が悪いとしばらく動いてから突然クラッシュするタイプのバグを生みます。「参照を返す関数では、その参照先の寿命を必ず確認する」を鉄則にしてください。

迷ったら値を返すのが安全です。C++11 以降は戻り値最適化(RVO)とムーブセマンティクスにより、大きな値を返してもコピーコストはほぼ発生しません。この話は第 26 回(ムーブセマンティクス)で詳しく扱います。

7. 使い分けの判断フロー

参照・ポインタ・値渡しの選択に迷ったら、以下の順で考えれば大体決まります。

関数引数の選び方(上から順にチェック)

  1. 関数内で書き換える? → Yes なら T&(参照)
  2. NULL を許したい/繋ぎ直したい? → Yes なら T*(ポインタ)
  3. T は大きいか?std::string, std::vector, 自作の大きな構造体など) → Yes なら const T&
  4. それ以外(int, double, bool, enum, 小さい構造体)T(値渡し)
関数戻り値の選び方: ① 所有権を返したいなら(RVO + ムーブで効率的)、② 既存オブジェクトの内部を触らせたいなら参照(ただし寿命に注意)、③ NULL を返しうるならポインタまたは std::optional(第 50 回)、が基本です。

次章へ

参照が入ったことで、C++ の関数の書き方が一気に自然になります。次章では、もうひとつ C++ らしい機能である 関数オーバーロードを扱います。同じ関数名で引数の型ごとに違う実装を提供できる仕組みで、これも参照と並んで「C++ ならではの書き方」の重要ピースです。

広告スペース

確認クイズ

ここまでの理解を 3 問で確認してみましょう。

Q1. 次のコードのうち、コンパイルエラーになるものはどれ?

int n = 10; int& r = n;
int& r;(宣言のみ、初期化なし)
int n = 10; const int& r = n;
const int& r = 42;(一時オブジェクトを束縛)
参照は 宣言時に必ず初期化する必要があります。int& r; は「何の別名か」が決まらないためコンパイルエラーになります。なお const int& r = 42; は一時オブジェクトを const 参照で受けており、C++ の合法な書き方です。

Q2. 関数で書き換えずstd::string を受け取りたい。最適な引数型は?

std::string s(値渡し)
std::string& s(参照)
const std::string& s(const 参照)
std::string* s(ポインタ)
std::string は可変長でサイズが大きくなり得るため、値渡しにはコピーコストが発生します。書き換えないなら const std::string& が定石で、コピーなし+誤変更を防ぐ+一時オブジェクト(文字列リテラルなど)も受け取れる、の 3 つの利点があります。

Q3. 次のコードで「参照」と「ポインタ」の違いとして正しいのは?

ポインタは NULL になり得るが、参照は常に何かを指している
参照のほうがポインタより実行時オーバーヘッドが大きい
ポインタは ++ で移動できるが、参照も ++ で別の変数に繋ぎ替えられる
参照は宣言後にいつでも別の変数を指すように変更できる
参照は必ず初期化が必要で、以降別のものに繋ぎ替えることはできません。NULL も存在しないので、ポインタとの最大の違いは「常に何かの別名である」ことです。実行時コストは同等(コンパイラが参照をポインタで実装することも多い)で、ポインタより重くなることはありません。
この記事をシェア