バケツリレーの限界

 これからポインタについて見ていくわけですが、そのためにもまず、参照をしっかりと憶えてください。参照をマスターすることで、ポインタの理解がダントツに早くなります。それはもう、ポインタだけを勉強するよりもずっとです。
 で、参照を憶える前に、フツーの引数の渡し方から。

ローカル変数のスコープ
ローカル変数の寿命  最初に、普通の変数がどのようにして作製され、破棄されるのか見てみましょう。
 変数が宣言されると、メモリのスタックと呼ばれる領域に変数のサイズ分(例えばintなら32ビット=4バイト)の領域が確保されます。
 作製された変数は、それ以降で「中カッコ({)の数−閉じ中カッコ(})の数<0」になったときに破棄され、使えなくなります。
 また、このようにして作製された変数は、他の関数では使用できません。つまり、ソースコードを見て、変数の宣言された中カッコの中からしか、変数へとアクセスできないというわけです。

 このように、ある一定区間(「スコープ」と呼びます)の中でのみアクセス可能な変数を「ローカル変数」といいます。

 ローカル変数と対照的に、まず最初に作製され、そしてどこからもアクセス可能な変数もあります。ソースコードの、すべてのネストの外で宣言された変数のことで、これをグローバル変数と呼びます。

 ローカル変数は非常に狭い区域からのみアクセス可能です。対してグローバル変数はあらゆる場所からアクセス可能です。
 変数は「アクセスする回数」が多ければ多いほど、管理が大変になります。変数がどっかで書き換えられることほど、迷惑なことはありません。そのため、この「アクセス回数」をコントロールすることが、バグの発生確率を下げることになります。
 グローバル変数はコントロールが不可能です。関数すべてに目を配らなくてはなりません。そういう理由から、基本的にグローバル変数はあまり使わない方がいいということになります。
 というわけで、ローカル変数中心に使用していくわけですが、当然、ローカル変数が持つデータを変数が届かない場所(スコープの外)で使用する必要が生まれます。そのための方法のひとつが「バケツリレー」です。

バケツリレー
バケツリレーの流れ  バケツリレー(一般に「値渡し(ねわたし)」と呼ばれる方法)は、変数間でデータをコピーすることでスコープの境界を超えます。
 では、右図のプログラムを追って、変数のスコープと、その中のデータの「バケツリレー」を見てみましょう。

 まず、Func()i1が宣言され、メモリ領域がスタックに作製されます。この変数のスコープは次の閉じ中カッコ、つまりFunc()の中だけです。そのため、その隣にあるTestFunc()からはアクセスできません。

 そこで「バケツリレー」です。i1TestFunc()の引数に渡して、データをバケツリレーします。
 TestFunc()が呼び出されると、その引数p_i1もスタックに作製されます。
 作製される位置(アドレス)は、必ずしもi1に近いとは限りませんし、そういうことを前提にしてプログラムを書いてはいけませんが、スタックに作製されることは確かです(ちなみに「スタック」とは「積み重なったもの」という意味です)。
 i1p_i1は、スコープの境界上にあり、お互いにアクセスできない関係ですが、このように「引数」として介すると、中のデータをコピーすることができます。この「バケツリレー」によって、スコープの外へとデータを渡すことが可能になります。

 データを渡したあと、TestFunc()が実行されます。この中からはp_i1にはアクセスできますが、スコープの外にあるi1にはアクセスできません。
 関数の処理を終え、p_i1をインクリメントしてみました。関数が終了すると、p_i1は破棄されてしまいます。その前にスコープの外へとバケツリレーしなければなりません。

 そこで、「戻り値」を使ってバケツリレーを継続します。
 returnを使用すると、関数が終了し、returnの右辺値が、Func()内のTestFunc()の呼び出し部分と入れ替わる形になります。つまり、
i1 = p_i1;
 のような形になるわけです。この部分でi1p_i1のデータがコピーされることで、スコープの境界を超えてデータを受け渡すことができます。
 この行が終わると、p_i1は破棄され、なくなってしまいます。が、i1にはちゃんとp_i1のデータが渡されています。i1のデータは、「引数」と「戻り値」によってスコープの境界を超え、これによってバケツリレーが完成するのです。

最悪のバケツリレー
 ところが、このバケツリレー方式には3つの点で問題があります。
 まずひとつは、同じ意味の変数が複数作製されるということです。上の例では、i1p_i1は同じ値を格納し、意味的に同じ変数です。が、実際には別の変数です。つまり「同じ目的に使っている変数が複数存在する」ということです。
 バケツリレー方式の場合、こういった複数の変数が持つデータが、最終的に一致するようプログラマーは努めなければなりません。p_i1を返し忘れただけで、ふたつの変数の整合性はなくなります。こういった部分に常に気を付ける必要があります。
 もしこれが、ひとつの変数だけを操作できたらとても楽になります。つまりp_i1など作らずに、TestFunc()からi1を直接利用できたら、整合性など気にせずに済むというわけです。returnなどしなくても、データは確実に反映されているのですから。

 もうひとつの問題点は、複数の変数で値を返せないということです。引数はいくらでも増やせるので「関数へ入れる」のは問題ないのですが、関数が終わったあと「関数から戻す」のはreturnを使ってのひとつだけしか返せません。また、エラー処理等でreturnを値の返還に使用したくない場合もあります。
 これがもし、スコープの外の変数をそのまま使えればなんの問題もありません。returnを使用しなくても、リアルタイムでデータを書き換えられるわけですから。

 最後の問題点は、データのコピーが大変ということです。intのように、複雑でもなく大きくもない変数なら問題はありません。が、構造体や配列といった複雑で大きい変数は、コピーに時間がかかったり特殊な処理を行わなければならなかったりします。その最も顕著な例はクラスです。例えば次のように、intCStringに置き換えてみます。


void Func()
{
	CString cStr = "おえいう";
	cStr = TestFunc( cStr );
}

CString TestFunc( CString p_cStr )
{
	p_cStr += "あ";
	return p_cStr;
}
	

最悪のバケツリレー  このコードを、順を追って見ていきます(ちょっと図が小さいので見づらいかもしれません)。
 クラスには「コンストラクタ」「デストラクタ」と呼ばれる特殊な関数を持ちます。前者は「クラス型の変数が宣言されたとき」に、後者は「同じく破棄されたとき」に呼び出される関数です。値渡しを使用すると、このふたつの関数が何度も呼び出されてしまうのです。

 まず、データを渡す引数が作製され、このときにコンストラクタが呼び出されます(正確には「コピーコンストラクタ」という、データのコピー専用のコンストラクタです)。
 次に、関数が終了する直前に戻り値のコンストラクタが呼び出されます。intのときと同じように、関数が戻り値と入れ替わるのですが、実はこのとき引数と同じように新しく変数が作製されます。これを一時オブジェクトといいます。この一時オブジェクトを__cStrとすると、
cStr = __cStr;
 という風に関数が置き換えられ、このとき__cStrのコピーコンストラクタが呼び出されてreturnの右辺値p_cStrが引数として渡されます。つまりreturnの右辺値がそのまま使われるわけではなく、わざわざ一時オブジェクトを作製しているというわけです。

 値を返すためのバケツリレーが終わったあと、関数内の変数がすべて削除され、このとき引数p_cStrデストラクタも呼び出されます。
 さらに、一時オブジェクトの寿命はその行のみなので、cStr = __cStr;の次の行に移った瞬間、__cStrデストラクタが呼び出され、削除されます。

 まとめると、引数のコンストラクタ−>一時オブジェクトのコンストラクタ−>引数のデストラクタ−>一時オブジェクトのデストラクタという順番でコンストラクタとデストラクタが呼ばれてしまうのです。これは、時には大きな負担になってしまいます。

「バケツリレー」に代わるもの、「ホース」
もしもホースがあったなら  結局「バケツリレー」は、スコープの外の変数にアクセスできないために使用せざるを得なくなっています。かといって、グローバル変数ではアクセスし放題のため、管理が大変になってしまいます。
 もし「ホース」があったらどうでしょう。水源から伸びるホースなら、ホースの先からしか水は出ません。ホースの一方から水を送り、また水を戻す。ホースの先だけを管理すれば、脇から水を横取りしたりはできませんし、バケツで水を受け渡す手間もなくなります。バケツリレーの問題が解決するわけです。
 このホースこそがC++参照です。次回、このホースの使い方をじっくり見てみましょう。

(C)KAB-studio 1998 ALL RIGHTS RESERVED.