C で「関数から呼び出し元の変数を書き換えたい」と思ったら、ポインタで渡すのが定石でした。swap(&x, &y) のように呼び出し側で & を書き、関数内で *p と逐一間接参照するあのパターンです。C++ では 参照渡し(T&)が加わり、この多くを置き換えられます。呼び出し側は普通に swap(x, y)、関数内も a をそのまま触るだけ。本章ではまず参照渡しの書き方を C と対比して身につけ、そのあとで「なぜそんなことができるのか=参照は変数の別名」という仕組みを補足で整理します。
T&(C の T* を置き換える)const T&(コピーを避ける定石)& 不要、関数内も * 不要 ― 普通の変数のように触れるconst T& は一時オブジェクトも受け取れるfor (auto& x : v) の意味C 経験者にとって参照渡しの価値が一番わかりやすいのは swap 関数です。同じ「2 つの値を入れ替える」を、C(ポインタ渡し)と C++(参照渡し)で書き比べます。
違いは大きく 2 つ。
swap(&x, &y) とアドレスを取る必要があるが、C++ では swap(x, y) でよい*a = *b と間接参照が入るが、C++ では a = b と通常の代入のままswap は呼び出し側の & を忘れると、整数をアドレスだと解釈して落ちる(未定義動作)。C++ の参照では構文レベルで「アドレスを渡し忘れる」ことが起き得ません。
T& で受け取る§1 の swap で見たように、C++ では「関数内で呼び出し元の変数を書き換えたい」ときは、引数の型を T& と書くだけで済みます。C でポインタを使っていた場面を置き換える最頻出パターンです。もう少し小さな例で書き方を固めましょう。
差分は小さく見えて、実務で効く違いが 3 つあります。
inc(&n) の & を外して inc(n) でよい(*p)++ のような間接参照が消え、r++ と通常の代入・演算がそのまま書けるnullptr を渡して関数がクラッシュ、が構文レベルで起きなくなるT* p を T& r に書き換え、関数内の *p を r に、呼び出し側の f(&x) を f(x) に置換する ― これだけで「書き換え用の引数」はたいてい参照渡しに寄せられます。T* は「NULL を許したい/関数内で指し直したい」場面にだけ残すのが一般的な方針です。
C と共通の値渡しも含め、書き換えの有無で選び分けます。
T&」が C++ の第一選択肢。「読むだけだけどコピーしたくない」ときは次節の const T&。「NULL を許す・繋ぎ直す」ときだけ T*。詳しい判断フローは §7 にまとめます。
const T& ― 読み取り専用の定石関数引数で最もよく登場するのが const T&(const 参照)です。コピーコストを避けつつ、関数内で書き換えてしまう事故も防ぐという二重のメリットがあります。
これは意外と大事なポイントです。非 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& で受けるのはこのためです。
次章以降でよく出てくる書き方です。ここでは「こう書くものだ」とだけ覚えておけば十分です。
std::vector<std::string> names = { "Alice", "Bob" }; for (auto s : names) { /* s はコピー */ } for (auto& s : names) { /* s は参照、書き換え可 */ } for (const auto& s : names) { /* s は const 参照、読み取り専用/コピーなし */ }
ここまでで T& と const T& の書き方は掴めたはずです。「なぜ呼び出し側の変数が書き換わるのか」を少しだけ掘り下げておきます。使うだけなら §5(vs ポインタ)へ進んで OK。
int& r = n; と書くと、r は n と同じメモリ位置を指し、r への操作は n への操作と完全に等価になります。
int n = 42; int& r = n; と書くと r は n の別名。同じ箱を 2 つの名前で呼んでいる状態。関数引数の場合も話は同じで、void inc(int& r) に inc(n) と渡すと、r は呼び出し側の n の別名になります。だから関数内で r++ すると、呼び出し元の n がそのまま増える ― ポインタ経由で辿り直すのではなく、最初から同じ変数を指しているイメージです。
// ① 宣言時に必ず初期化(後から束縛できない) 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 の & は「アドレスを取る」演算子。記号は同じでも役割がまったく違います。
C 経験者の疑問はたいてい「ポインタがあるのに、なぜ参照があるのか?」です。答えは用途の棲み分け。それぞれの得意分野を比較表で確認します。
| 項目 | ポインタ T* | 参照 T& |
|---|---|---|
| 初期化 | あとで代入してよい(T* p;) |
必ず初期化(T& r = x;) |
| NULL | あり(nullptr) |
なし(常に何かを指している) |
| 再束縛 | できる(p = &y;) |
できない |
| 間接参照 | *p を明示 |
そのまま r |
| ポインタ算術 | できる(p + 1) |
できない |
| 配列の先頭 | 表現できる | 単体参照のみ |
| 主な用途 | 動的配列・NULL を許すケース・古い C API | 関数引数・戻り値・範囲 for |
関数は参照を返すこともできます。うまく使えば効率的ですが、ローカル変数への参照を返すと破綻するという典型的な罠があります。
迷ったら値を返すのが安全です。C++11 以降は戻り値最適化(RVO)とムーブセマンティクスにより、大きな値を返してもコピーコストはほぼ発生しません。この話は第 26 回(ムーブセマンティクス)で詳しく扱います。
参照・ポインタ・値渡しの選択に迷ったら、以下の順で考えれば大体決まります。
T&(参照)T*(ポインタ)std::string, std::vector, 自作の大きな構造体など) → Yes なら const T&int, double, bool, enum, 小さい構造体)→ T(値渡し)std::optional(第 50 回)、が基本です。
参照が入ったことで、C++ の関数の書き方が一気に自然になります。次章では、もうひとつ C++ らしい機能である 関数オーバーロードを扱います。同じ関数名で引数の型ごとに違う実装を提供できる仕組みで、これも参照と並んで「C++ ならではの書き方」の重要ピースです。
ここまでの理解を 3 問で確認してみましょう。
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++ の合法な書き方です。std::string を受け取りたい。最適な引数型は?std::string s(値渡し)std::string& s(参照)const std::string& s(const 参照)std::string* s(ポインタ)std::string は可変長でサイズが大きくなり得るため、値渡しにはコピーコストが発生します。書き換えないなら const std::string& が定石で、コピーなし+誤変更を防ぐ+一時オブジェクト(文字列リテラルなど)も受け取れる、の 3 つの利点があります。++ で移動できるが、参照も ++ で別の変数に繋ぎ替えられる