ホームに戻る
出典 :
参照(C++) - 超初心者向けプログラミング入門 C++ 参照【関数におけるポインタ渡しと参照渡しの違い】 C++ 値渡し、ポインタ渡し、参照渡しを使い分けよう - Qiita
関連 :
const修飾の使い方 関数オーバーロードとデフォルト引数 参照渡し
目次 :

参照型とは

変数(オブジェクト)への参照を保持する型のこと。参照型変数を操作することで、参照先である元の変数を更新することができる。
「参照」を渡せる点で、機能的にはポインタに類似するが、 といった特徴から、ポインタよりも安全、かつ簡便に用いることができる。

以下で示すように、参照先の型に & を付与したものが参照型となる。
( int ⇒ int& )
void main() { // int 型変数 num の宣言 int num = 10; // num を指し示す 参照型変数 ref の宣言 int& ref = num; // ref に変更を加えると、参照先である num が変更される ref = 20; // num の内容を表示 std::cout << num << std::endl; }
実行結果 :
20

参照型の考え方

そもそも変数宣言自体がメモリの特定の領域に名前(ラベル)を割り当てることであり、参照型変数の宣言はさらに別名を割り当てることであると言える。
画像
このため資料ごとに "reference" (参照)と "alias" (別名)とで表記が分かれているが、意味は等価である。
尚、参照は内部的にはポインタを用いて処理されているが、プログラマがそのことを意識する必要はない。

値渡し、ポインタ渡し、参照渡し

画像
C++での関数呼び出しは3つに大別される。ここで、「ポインタ渡し」はオブジェクトへの参照を渡している点では広義の「参照渡し」であるが、
本記事においては、ポインタではない参照型を渡すものを「参照渡し」と呼ぶものとする。

値渡し

// 値渡しの twice() void twice( int a ) { std::cout << "a == " << a << std::endl; a *= 2; std::cout << "a == " << a << std::endl; } void main() { int v = 16; std::cout << "v == " << v << std::endl; twice( v ); std::cout << "v == " << v << std::endl; }
実行結果 :
v == 16 //< twice() 実行前の v の値 a == 16 //< twice() 突入後の a の値 ⇒ 引数に渡された v のコピー a == 32 //< 演算後の a の値 v == 16 //< twice() 実行後の v の値 ⇒ 変化なし
関数に対して、変数(またはリテラル)を直接渡すことを値渡しと呼ぶ。値渡しの場合、引数の厳密なコピーが生成される。
上記の例では、twice() 関数に変数 v が渡されているが、渡されるのは v の値だけで、twice() における a は v のコピーでしかない。
このため関数内で a の値を変更しても、呼び元の v には一切影響を及ぼさない。

ポインタ渡し

// ポインタ渡しの twice() void twice( int* a ) { std::cout << "*a == " << *a << std::endl; *a *= 2; std::cout << "*a == " << *a << std::endl; } void main() { int v = 16; std::cout << " v == " << v << std::endl; twice( &v ); std::cout << " v == " << v << std::endl; }
実行結果 :
v == 16 //< twice() 実行前の v の値 *a == 16 //< twice() 突入後の *a の値 *a == 32 //< 演算後の *a の値 v == 32 //< twice() 実行後の v の値
上記の例では、twice() 関数に変数 v を指し示すポインタ(アドレス)が渡されている。
a の指し示す先は v であるため、twice() 実行後に v の値が変化している。

参照渡し

// 参照渡しの twice() void twice( int& a ) { std::cout << "a == " << a << std::endl; a *= 2; std::cout << "a == " << a << std::endl; } void main() { int v = 16; std::cout << "v == " << v << std::endl; twice( v ); std::cout << "v == " << v << std::endl; }
実行結果 :
v == 16 //< twice() 実行前の v の値 a == 16 //< twice() 突入後の a の値 a == 32 //< 演算後の a の値 v == 32 //< twice() 実行後の v の値
上記の例では、twice() 関数に変数 v への参照が渡されている。
twice() 関数に突入した際に、参照型変数 a は v の別名として生成される。このためポインタ渡しの場合と同様、twice() 実行後に v の値が変化している。
この参照渡しにおいては仮引数宣言でのみ & が使用されているが、それ以外の記述は値渡しと変わらず、ポインタ渡しよりも簡潔に記述できる。

参照における制約

// ポインタ : いずれも有効 int* p1; //< 宣言のみで初期化を行わない int* p2 = NULL; //< NULL で初期化 int* p3 = (int*)1; //< 値を代入 // 参照 : いずれも無効 int& r1; //< 宣言のみで初期化を行わない ⇒ エラー int& r2 = NULL; //< NULL で初期化 ⇒ エラー int& r3 = (int&)1; //< 値を代入 ⇒ エラー
参照型変数は、宣言と同時に既存のオブジェクトで初期化されなければならず、また指し示す先を変更することもできない。
これは逆に言えば、参照型変数は特定のオブジェクトを指し示すことが保証されていることを意味する。
このため、ポインタを用いた場合のような NULL 確認は不要である。
(ポインタ型変数は、それがポインタ型であること以外は何も保証されていない。)

効果的な使い方

渡した先で値を書き換えない場合でも、参照渡しは変数への参照のみが渡されコピーが作成されないため、
サイズの大きなオブジェクト(クラス、構造体など)を渡す場合は参照渡しを用いることでオーバーヘッドを抑制することができる
関数内で値を書き換えないことを保証したい場合は、const 修飾を用いるとよい
// 値渡し : a は関数内での書き換えが許容されない int func1( const int a ) { // a *= 2; //< この処理はエラーとなる : } // 参照渡し : a は関数内での書き換えが許容されない int func2( const int& a ) { // a *= 2; //< この処理はエラーとなる : }
上記は int 型の値渡しと、参照渡しの例である。いずれも const 修飾されているため、関数内で変数 a の値は変更できない。
int のような数値型であれば、値渡しと参照渡しで、関数呼び出しのオーバーヘッドの差は大きくはないが、
これがクラス型や構造体、共用体であればその型のサイズに比例してコピーの負荷は高くなるため、値渡しのオーバーヘッドは大きくなる。
参照渡しであれば先頭アドレスのみが渡されるため、オーバーヘッドは非常に小さい。
このため、組み込み型(プリミティブ型)以外は原則として参照渡しを用いるとよい

余談

ポインタ変数と const

void main() { int v = 16; // アスタリスクの左に const : ポインタ変数 pc の指し示す先が書き換え不可 // ( int const* は const int* と同義) int const* cp = &v; // *cp += 5; //< この処理はNG cp = NULL; //< この処理はOK // アスタリスクの右に const : ポインタ変数 pc 自体が書き換え不可 int* const pc = &v; *pc += 5; //< この処理はOK // pc = NULL; //< この処理はNG // アスタリスクの左右に const : pc と pc の指し示す先のいずれも書き換え不可 int const* const cpc = &v; // *cpc += 5; //< この処理はNG // cpc = NULL; //< この処理はNG }
ポインタ変数と const を組み合わせた場合、const とアスタリスクの位置関係によって振舞が変化する。const修飾の使い方も併せて参照。

参照仮引数とオーバーロード

// f() 関数のオーバーロード void f(int a); //< 値仮引数 void f(int& a); //< 参照仮引数 : int i = 10; f(i); //< 曖昧
関数オーバーロードに際して、一方の仮引数が値、他方の仮引数が参照となると、コンパイラはいずれの関数を呼び出すかが判別できないためエラーとなる。
関数オーバーロードとデフォルト引数も併せて参照。