このページでは C++ のリファレンス(日本語では参照)について解説します。リファレンスは、ポインタの一種だと勘違いしている人がかなりいるようで、世の中には、そのような間違ったことを、多分悪意は無く、教える人までいて、初心者にとっては混乱の元となり得ますが、正しくは、リファレンスはポインタでは全く無いので、ここで、正しく理解してください。C++ のリファレンスは、ポインタよりもずっと簡単な概念です。また、ポインタを全く知らなくても理解できるものなのです。
リファレンスとは何かということを解説するために、まず、最も簡単な例をあげましょう。次のコードを見てください。int 型の変数 iABitComplex のリファレンス riSimple を定義しています。
int main() { int iABitComplex; // ちょっと複雑な名前の変数 int& riSimple = iABitComplex; // 変数のリファレンスを定義する。 riSimple = 1; // リファレンスを使って iABitComplex にアクセスする。 return 0; }
少し解説する必要はありますが、これが、リファレンスの全てといっていいでしょう。ポインタはでてきていませんね。では、早速解説を。
まず、int 型の変数のリファレンスは、形式的には int& 型となります。つまり、変数型として int に & をつけて、int& とするわけです。この & が、変数のアドレスを取るための演算子 & と誤解されやすく、リファレンスとは何かポインタに関連するのではないかと思い込んでしまう人のパーセンテージががかなり高いのですが、それは全く間違いです。
では、上記の「変数のリファレンスを定義する。」と書かれた行の正しい意味はどのようなものかというと、iABitComplex というちょっと複雑な名前の int 型の変数を、もう少し簡単な riSimple と言う別の名前でも扱えるようにする、ということになります。つまり、変数に別の名前を付けたわけです。一旦、別名をつけると、その別名、つまりリファレンスをコード中に記述することは、元の変数の名前を記述することとまったく同じ意味になります。例外的に、const をつけた参照では、元の変数に書き込むことができないという制約はありますが、リファレンスを扱うことによって、元の変数を扱うことになる、ということには変りありません。
上記のようなメカニズムなので、別名であるリファレンスを定義するときには、その別名の元となる変数が必ず必要です。つまり、元の変数あってこそのリファレンスなわけです。変数型として int& を使うと上に書きましたが、リファレンスは、それ自体は C++ の変数型ではなく、ただの名前なので、コンピュータ上にリファレンスのためのメモリ領域は基本的には無いと考えた方が良いでしょう。ですから、リファレンス自体を変数として読んだり、書いたりすることはできません。そのようにしようとすると、元の変数を読んだり書いたりすることになるというわけです。
英語では reference、日本語では「参照」と訳されるリファレンスですが、これが、英語では another_name とし、日本語では「別名」と訳しておけば、実際の意味に近いものになったのかもしれませんね。
店主 : 正しく理解してもらえているかどうか、ちょっと心配だなぁ。
ナオ : 大丈夫よ、たぶん分かってもらえてるって。
上記のように、リファレンスは単純なものですが、クラスのメンバとして使用する場合には、その初期化をどうするかということが問題になります。まぁ、コンストラクタの初期化子並びで初期化するだけなんですが。次の例を見てください。
int iValue = 0; class CManiac { private: int& m_riValue; // メンバであるリファレンス public: CManiac() : m_riValue(iValue) {} // コンストラクタの初期化子並びで初期化する。これで OK };
上記のように、初期化子並びでの初期化は、形式的には、m_riValue のコンストラクタを呼び出す形になるので、m_riValue の後に () をつけてコンストラクタの呼び出しとし、初期値を iValue としているわけです。
関数への引数の渡し方には、「値渡し」 と 「参照渡し」(=リファレンス渡し) の二種類があります。ここでは、それぞれの特徴とメリット、デメリットについて解説します。
値渡しは、変数(=オブジェクト)をコピーして渡すということです。「コピーして渡す」ので、変数が渡った先の関数でその「コピーされた変数」を変化させても、元の変数には影響が及びません。そういう意味で、元の変数は安全に保たれるという特徴があります。逆に、「変数に処理を施しても、その結果を呼び出し元では利用できない」とも言えます。
オブジェクトを「コピーする」ので、大きなオブジェクトを値渡しすることには、高いコストがかかります。例えば次のクラス CManiac のオブジェクトは約 1GB のメモリを消費します。これをコピーすれば、当然、更に 1GB が必要になるわけです。
// オブジェクト一つで 1GB のメモリを消費するクラス #define DATABYTES 1000000000 class CManiac { private: char* m_pcData; public: ~CManiac() { delete[] m_pcData; } CManiac() : m_pcData(new char[DATABYTES]) {} // デフォルトコンストラクタ CManiac(const CManiac& rmaniac) : m_pcData(new char[DATABYTES]) { // コピーコンストラクタ copy(&rmaniac.m_pcData[0], &rmaniac.m_pcData[DATABYTES], m_pcData); } };
どんなコードを書くと、コピーが起こるのかというと、次のようになります。
#include <stdio.h> #include <algorithm> using namespace std; void FuncC(CManiac maniac) { // maniac は、@の maniac のコピーである。 // ここで、maniac を使う。 } int main() { CManiac maniac; // @ FuncC(maniac); // 値渡しになる(=コピーして渡す)。 return 0; }
関数の引数を普通に CManiac maniac とするだけで、コピーが起こるということです。上記を実行すると、FuncC() に制御が渡ったときには 2GB のメモリが主記憶から消費されます。メモリが 2GB 程度の PC では、メモリのスワップが起こることになるでしょう。これは、あまり望ましい状況ではありません。
C++ に最初から組み込まれている int、short、long、float、double などの変数型は、小さいので、上記のような問題は生じません。これらは、おそらく CPU のレジスタに入って渡されるので、高速でもあります。小さなオブジェクトは値渡しでも問題ないわけです。
参照渡しは、変数(=オブジェクト)を別名で扱えるようにするということです。参照渡しは、オブジェクトのコピーを伴わないため、大きなオブジェクトを扱うのに適しています。上の値渡しの場合で述べたような問題が生じないということですね。
参照渡しのコードを見てみましょう。
void FuncR(CManiac& rmaniac) { // rmaniac は、@の maniac の別名である。 // ここで、rmaniac を扱うと、@の maniac を扱うことになる。 } int main() { CManiac maniac; // @ FuncR(maniac); // 参照渡しになる(=コピーしないで別名で扱えるようになる)。 // ここで、maniac に行われた処理の結果を使うこともできる。 return 0; }
上記では、FuncR() に制御が渡ったときには、rmaniac は元の変数である main() の中の maniac を別名で扱う単なる名前であるため、メモリとしては元の maniac の分である約 1GB が消費されるだけで済みます。たぶん、メモリのスワップも避けられるでしょう。また、コピーを伴わないため、実行スピードも速くなります。
大きなオブジェクトを渡したいが、変化してほしくないというときには次のように const を使用します。これで、rmaniac は、制御が渡った先の関数 FuncX() では、書き込めなくなります。
void FuncX(const CManiac& rmaniac) { // ここで、rmaniac を使う。rmaniac には、書き込めない。 }
C++ では、関数を作成するときに、引数を値渡しにするか、参照渡しにするかということは、常に意識してプログラミングを行うべきだということが分かるでしょう。
配列のリファレンスは、記述の仕方が少し複雑なので、ここで、例を示しておきます。次の例を見てください。
int main() { int aiArray[3]; // int[3] 型の配列 int (&raiArray)[3] = aiArray; // int[3] 型の配列の参照、raiArray return 0; }
int と & の位置に注目してください。ただ、これだけのことですが、知らないと窮するかもしれませんね。
リファレンスを並べて配列にしたい、と思う人がいるかもしれませんが、残念ながらこれは原理的に不可能です。
何故かというと、冒頭でも述べたように、リファレンスは C++ の変数型ではなく、元の変数の別名にすぎないからです。
次のように考えれば、良く理解できるでしょう。
C++ の変数は、例えばその型を ObjectType と書くと、この変数型のオブジェクトの配列は ObjectType object[3]; のように定義できます。
sizeof(ObjectType) が例えば 5 であれば、object[3] は 15 バイトの記憶領域を要するでしょう。
これに対し、リファレンス(=別名)は、そもそも C++ の変数型ではないので、記憶領域を持たず、メモリ上に配列として記憶すること自体が不可能です。
int が 8 バイトの場合 sizeof(int&) は 8 になりますが、これはリファレンスの元となる型 int のサイズであって、リファレンス(=別名)の記憶領域の大きさを示しているわけではありません。
記憶領域を持たないリファレンスを、メモリ上に記憶するには、いったいメモリを何バイトとれば良いでしょうか? それは、誰にもわかりません。なぜなら、そんなものは初めから存在しないからということですね。
店主 : 蛸褌だな、こりゃ。
ナオ : 「タコフン」って、なんですか?
店主 : ちと、古すぎたか。どうすりゃいいのかな。
ナオ : なになに、なんですか? 「タコフン」ってなに?