参照を使おう!

 というわけで参照です。参照は今回だけ、1回切りしか説明しませんが、たぶんそれだけで分かると思います。使い方とかは、簡単なんで。

参照の作り方
 前回バケツリレーの欠点を説明しました。それを解決するのがホース、つまり参照です。
 よく「この本の32ページ24行目を参照してください」とか言うように、「参照」とは何かを指し示すものという意味です。英語ではReferenceと呼ばれます。

 具体的には、参照は他の変数を表す変数です。ホースを想像してみましょう。片側から水を入れれば、もう片方にも同じ量だけ水が出てきます。当然逆からでも、これは言えます。このようにホースを使えば、水源がホースの先まで来てるかのごとく使用できるわけです。
 これを実現するのが、参照というわけです。参照は、参照先の変数に継ながったホースの先となり、まるで参照が参照先かのように操作することができるのです。これを使用すれば、バケツリレーは要らなくなります。
 では、実際にプログラム上で使ってみましょう。

とりあえず使ってみましょう
 というわけで、次のコードを試してみてください。


	int i1 = 3;

	// ベーシックな使用法。
	int &ri1 = i1;
	TRACE( "i1: %d, ri1: %d\n", i1, ri1 );
	// i1: 3, ri1: 3

	++i1;
	TRACE( "i1: %d, ri1: %d\n", i1, ri1 );
	// i1: 4, ri1: 4

	++ri1;
	TRACE( "i1: %d, ri1: %d\n", i1, ri1 );
	// i1: 5, ri1: 5

	//////////////////////////////////
	// 以下、できないこと。

//	int &ri2;	//参照だけの宣言。
	// 「error C2530: 'ri2' : 参照が初期化されずに宣言されています。」

//	int &ri3 = 4;	//定数値への参照。
	// 「error C2440: 'initializing' : 'const int' から
	//  'int &' に変換することはできません。」

	int i2 = 6;
	ri1 = i2;	//参照先の変更(ただの代入)。
	TRACE( "i1: %d, ri1: %d\n", i1, ri1 );
	// i1: 6, ri1: 6
	

 参照を宣言するときには、必ず次のような形になります。

参照先の型 &参照の変数名 = 参照先の変数;

 普通の変数宣言に似ていますが、型の隣にを入れる必要があります。

参照  また、このとき必ず参照先の変数を指定する必要があります。参照とは「他の変数を指し示す変数」ですので、その参照先の変数がすでに存在している必要があります。
 参照先を用意せずに参照だけ宣言すると、コンパイルエラーが発生します。また、一度参照を作製したら、参照先を変えることはできません。つまり、ホースだけ先に用意したり、いろんな変数に使い回したりはできないということです。

 作製された変数は、参照先の変数とまったく同じになります。一方の値を変えれば、もう一方の値も変わります。つまりふたつの変数は、ホースによって常に一致するようになっているわけです。参照先の値がホースで送られ、参照に水が入れられればそれはホースで参照先に送られるというわけです。
 さて、この方法はそれほど参照を生かしていません。そう、参照が活きてくるのは関数の引数に使ったときです

参照渡し
 データの中身ではなく参照を渡す方法を、「参照渡し」と呼びます。「値渡し」との違いは、ほんのちょっと。引数の型にを入れるだけです。


void Func()
{
	CString cStr = "おえいう";
	TestFunc( cStr );
	TRACE( "cStr: %s\n", (LPCTSTR)cStr );
}

void TestFunc( CString &p_rcStr )
{
	p_rcStr += "あ";
	return;
}
	

参照渡し  引数に参照を渡すということは、ホースを使ってスコープの壁を乗り越えるということです。上のコードを実行すれば分かるとおり、cStrp_rcStrまったく同じ変数として扱えます。つまり、このふたつの変数はホースで継ながっているのです!! そのため、参照を操作することで、本来スコープの外にある変数を操作できるというわけです。

 これで、「バケツリレー」のみっつの問題をクリアできます。
 まずひとつ、「ふたつの変数が作製されてしまう」という問題は、もちろんクリアされます。ふたつの変数では必ず値が一致しますし、参照は単なるホースなのでメモリを食いません。
 もうひとつ、「複数の値を返せない」という問題ももちろん解決されます。参照は引数のみ使用するので、返したい値の分だけ引数を作製すればいいだけです。
 最後のひとつ、「コピーが大変」という問題も当然解決されます。やっぱり参照は単なるホースなので、コンストラクタやデストラクタは呼び出されません。また、上のコードでは戻り値を使っていないので、一時オブジェクトも作製されません。

やっちゃまずいこと
 参照は、基本的に引数でのみ使用するのがいいでしょう。なぜなら、次のようなミスをする可能性があるからです。


void Func()
{
	CString &rcStr = TestFunc();
	rcStr += "いうえお";	//例外発生。
}

CString &TestFunc( )
{
	CString cStr = "あ";
	CString &rcStr = cStr;
	
	return rcStr;
}
	

参照渡し  参照を引数に使わず戻り値に使うと、関数内の変数への参照を返すことができます。ところが、この変数は関数が終了すると削除されてしまいます。関数の外へと出て、返された参照を使ってこの「削除された変数」にアクセスすれば、もちろんその変数は存在しないので例外が発生します
 このコードの問題点は戻り値に参照を使うことです。実際には使用しても問題ない場合もありますが(例えばCString::operator +=()とか)、基本的に参照は、引数に使うのが一番安全で確実な方法と言えるでしょう。

 また、もうひとつ問題があります。前述のみっつの問題のうち、「ふたつの変数が作製される」という問題がクリアされることで、新たな問題が出てくる場合もあるでしょう。
 例えば、大きなクラスを使うために「コピーしたくない」のだけれども、同時に参照先の変数の値を変えたくないという場合があります。参照をうっかり操作したら、参照先のデータも変わることになってしまいます。

 このように参照先を不用意に変えかねないという問題は、グローバル変数のように大きな問題になります。他の人が作った関数に何気なく変数を渡していたらいつの間にか値が変わっていた、なんてシャレになりません。
 そこで使うのが「const」です。

constを使う
 参照には「参照先の変数のデータを変化させない」ことができます。そのためのキーワードがconst(「コンスト」もしくは「コンスタント」)です。
 const「定数」という意味です。「参照先の変数は定数だから変えちゃダメだよ」という宣言になるわけです。
 constは次のように簡単に使えます。


void Func()
{
	CString cStr = "おえいう";
	TestFunc( cStr );
	TRACE( "cStr: %s\n", (LPCTSTR)cStr );
}

void TestFunc( const CString &p_rcStr )
{
//	p_rcStr += "あ";	//コンパイルエラー。
	TRACE( "cStr: %s\n", (LPCTSTR)p_rcStr );
	return;
}
	

const  このように、引数の型の前にconstを付けるだけで、参照先のデータを操作しようとするとコンパイルエラーが発生するようになります。ですが、操作しない限りはどんなことでもできます。

 これに関連して、こんなconstの使われ方があります。CString::GetLength()のリファレンスを見ると、次のように書かれています。
int GetLength( ) const;
 この最後のconstを付けると、「このメンバ関数を使用してもクラス内のメンバ変数のデータは書き換えられません」という意味になります。要するに、メンバ変数をconstにするというわけです。こういったメンバ関数は、const参照でも問題なく使えます。
 逆に言えば、基本的にこのconstが付いていないメンバ関数は「値を書き換える」と判断すべきです。つまり、そういったメンバ関数は「const参照の引数では使えない」ということです。

constの使いどころ
 このconstは、「値を返す」場合以外で引数に参照を使う場合には必ず付けるようにしましょう。つまり「コピーしたくない」という目的の時のみには、勝手にデータを書き換えないようにconstを付けてしまった方がいいというわけです。
 これは、そういうクセを着けてしまいましょう。コードが増大・複雑化すると、隅々まで目が行き届かなくなってきます。そんなとき、constが役に立ちます。値が勝手に変えられてないかどうか、自分の代わりにコンパイラがチェックしてくれるのですから

 それに、constを付けることで、関数のプロトタイプ宣言を見れば、参照の引数がどういう意味合いのものかばっちり分かります。例えば、
BOOL CopyStr( CString &p_rcDestinationStr, const CString &p_rcSourceStr );
 という関数があれば、「第2引数から第1引数にコピーする関数」と簡単に想像できます。こういったときのためにも、constはまめに付けておきましょう。まとめると次のようになります。

  書式 使用する理由
constを使う const 型 &引数名 通常使用する
(使わない) 型 &引数名 参照先書換え時のみ

 というわけで、constを付けられる場合は、すべて付けてしまいましょう。

参照の使いどころ
 順序が逆になってしまいましたが、「参照をいつ使うべきか」について解説しておきましょう。
 参照は、実際には次回から解説する「ポインタ」というものを使用しています。ポインタは32ビットのデータです。これをふまえて考えてみましょう。

 まず、引数を使って値を返したい時には、有無をいわさず参照を使用します。値渡しのバケツリレーではこれは無理ですから。
 クラスや構造体を渡したい時には、コンストラクタやデストラクタを呼び出したり、データを丸ごとコピーするオーバーヘッドを考えれば使用すべき場合が多いでしょう。ただ、クラスの場合にはコピーコンストラクタや型変換演算子といったメンバ関数が揃っているため、型を超えた受け渡しが可能です。そういった機能を使用したい場合には、値渡しの方がいい場合もあります。これは使い分けてください。
 最後に基本型を渡す時には、参照は使わない方がいいでしょう。intなど基本型はほとんどが32ビット前後ですし、コンストラクタやデストラクタを持ちません。参照を使ってconstを付け忘れると値を変えてしまう危険性が出てきますから、それを考えれば値渡しの方がいいでしょう。
 以上をまとめてみましょう。

引数のタイプ 参照を使う理由 値渡しを使う理由
引数を戻り値に使う 値渡しでは不可能 ナシ(不可能)
クラスや構造体を渡す オーバーヘッドが
大きいとき
フレキシブルな型変換を
行いたいとき
基本型を渡す 値を返したいときのみ 通常は値渡しを使用

 この分類と、constの使い方を組み合わせて、適切な引数型を使用してください。

「参照」に封じ込まれしもの
 さて、以上で参照の解説は終わりです。簡単でしょう。ホント、簡単に使えます。実際、参照は他の多くの言語でサポートされ、とてもメジャーな存在です。ごくごく自然なものなのです。
 ところが、実はC言語には参照はありません。参照が実装されたのはC++からです。
 「ええっ、じゃあどうやって構造体渡すの? 値はひとつしか返せないの?」いえいえ、そんなことはないんです。Cでも参照と同じ機能が実現できるのです。そしてそれは参照以上に高い機能を持ち、フレキシブルであり、それ故危険な存在でした。参照は、その機能の中の一部を使いやすくしたものなのです。

 その機能は、他の言語にはない能力を持ち、それゆえ「C言語」は他の言語とは一目置かれる存在になりました。しかし、その機能はあまりにも高機能すぎ、また、「ローレベル」、つまり根本的な機能のため、システムを根底から揺るがすほど能力を持っています。その危険度故「初心者キラー」の名を欲しいままにしています。そのあまりの難しさ、そして危険度に、機能の一部を封じ込まれたものが「参照」としてC++に登場したくらいです。
 しかし、C言語においてこの機能はもはや切り離せない存在になっています。プログラムのありとあらゆる部分に現れ、C++が標準になりつつある今でさえ、使用せずに済ませることは不可能と言っても過言ではないでしょう。
 でも、ホントは難しくないんです。基本的な原理は、参照と同じなのですから。参照の中に封じ込められている力をちょっとずつ解放していけば、その姿に戸惑うことはないはずです。

 では、その姿を少しずつお見せすることにいたしましょう。
 そう、その機能の名は……。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.