同じものに対する別の名前
関数パラメータのページでは、すべての引数が関数の中へコピーされていました。そのコピーこそが、関数が呼び出し元の変数を変更できない理由です。関数は自分の複製しか見ていないのです。参照はその壁を打ち破ります。参照はエイリアス、つまり既存の変数に束縛され、まったく同じメモリを共有する第二の名前です。
参照は宣言時に & を使って作ります。一度束縛されると、参照と元の変数は区別できません:
二つのルールが参照を安全で予測可能にします。参照は宣言された瞬間に必ず初期化されなければならず(int& r; はコンパイルエラー)、後から別のものを指すように再束縛することは決してできません。参照への代入は、常に最初に束縛された対象へ書き込みます。
参照渡し:関数に手を伸ばし返させる
本当の恩恵は関数にあります。パラメータに & を付けると、関数は呼び出し元の引数のコピーではなくエイリアスを受け取ります。これで関数内の変更が外側にも反映されます:
& を外すと addBonus は使い捨てのコピーを増やすだけで、total は 100 のままです。このたった一文字がすべての違いです。これは複数の結果を返したり、入力をその場で編集したりする関数を書くための定石です。古典的な例は二つの変数の入れ替えです:
参照がなければ swapValues はローカルのコピーを入れ替えるだけで、x/y は 1 2 のままになります。(標準ライブラリには既に std::swap がありますが、自分で書いてみると参照パラメータが何をもたらすかが正確に分かります。)
const 参照:速く読み、触らないと約束する
参照渡しはコピーも避けられます。大きなオブジェクトではそのコピーが高コストになり得ます。しかし素の T& パラメータは「これを変更するかもしれない」というシグナルを発し、読むだけのときには誤解を招きます。その解決策が const T& です。参照のコピーなしの速度に加えて、関数が引数を変更しないというコンパイラに強制される約束が得られます。
非 const の参照は変更可能な変数にしか束縛できませんが、const 参照はリテラルや一時オブジェクトにも束縛できます。だから greet("literal works too") はコンパイルが通るのです。パラメータの型を選ぶ実用的な目安は次のとおりです:
void f(int x) // 安価な型、読み取り専用 -> ただコピーする
void f(const string& s) // 重い型、読み取り専用 -> const 参照
void f(string& s) // 呼び出し元のオブジェクトを変更するつもり
読むだけのクラス型(string、vector、自作の構造体)には既定で const T& を使い、本当に書き戻したいときのために非 const の参照を取っておきましょう。
参照を返す
関数は参照を返すこともでき、すでに存在するものへのエイリアスを呼び出し元に渡せます。これはコンテナのようなコードでよく使われます。v[i] = 5 が動くのも、operator[] が裏で行っているのもこれです:
at は int& を返すので、呼び出し式 at(data, 1) 自体が代入可能な左辺値になります。代わりに素の int を返すと at(data, 1) = 42 はコンパイルできません。一時的なコピーへ代入することになるからです。
大きな落とし穴:ダングリング参照
参照は何も所有しません。どこか別の場所に存在するメモリを指しているだけです。参照がまだ使われている間にそのメモリが消えると、それはダングリング参照であり、それを通じて読むことは未定義動作です。ゴミを出力するかもしれないし、クラッシュするかもしれないし、本番環境であなたの一日を台無しにするまで動いているように見えるかもしれません。古典的なミスはローカル変数への参照を返すことです:
int& broken() {
int local = 42;
return local; // バグ: local は broken() が戻るときに破棄される
} // 返された参照は宙ぶらりんになる
int main() {
int& r = broken();
cout << r << "\n"; // 未定義動作 - 死んだメモリを読む
}
変数 local は broken が戻った瞬間に消えるので、参照は回収済みのスタック領域を指します。呼び出しより長く生き残るものへの参照だけを返しましょう。参照で渡されたパラメータ、データメンバ、あるいは static です。値が関数内で計算される場合は、代わりに値で返し、コンパイラにコピーを最適化させましょう。同じ罠は範囲ベースのループや、一時オブジェクトに束縛されたあらゆる参照にも当てはまります。参照が名指すものの寿命を超えて、その参照を保持してはいけません。
次へ:関数のオーバーロード
参照はすべてのパラメータに二つ目のつまみ(コピーかエイリアスか、可変か const か)を与え、そのつまみは次のトピックと直接かかわります。次の関数のオーバーロードでは、同じ名前で異なるパラメータリストを持つ複数の関数を定義でき、コンパイラは引数の型を照合して正しいものを選びます。値渡しか、参照渡しか、const 参照渡しかも含めて、です。
よくある質問
C++における参照とは何ですか?
参照とは既存の変数のエイリアス、つまり同じメモリに対する別名です。宣言時に & を使って作ります:int& r = x;。これ以降、r と x は交換可能になり、一方を変えればもう一方も変わります。参照は宣言時に必ず初期化しなければならず、後から別の変数を指すように再束縛することはできません。
C++における値渡しと参照渡しの違いは何ですか?
値渡し(void f(int x))は引数をコピーするので、関数は自分のコピー上で動作し、呼び出し元の変数はそのまま残ります。参照渡し(void f(int& x))は関数に呼び出し元の変数への直接アクセスを与えるため、変更は呼び出し後にも反映され、コピーも作られません。これは大きなオブジェクトで重要になります。
C++ではいつ const 参照パラメータを使うべきですか?
関数がパラメータを読み取るだけでよいが、その型のコピーが高コストな場合(string、vector、大きな構造体)には const T& を使います。参照のコピーなしの速度に加え、関数が呼び出し元の値を変更しないというコンパイラ保証が得られます。int や double のような安価な型なら、素直な値渡しのほうが単純で同じくらい速いです。