2.6 関数の受け渡しメカニズム  (1999/04/04初版)

2.6.1 関数  プログラムをたくさんの部品すなわちモジュールの集まりとして構成することは、大 規模なソフトウェアを設計する上で必要な技術です。プログラムを機能ごとに分割する ことによって、問題をモジュール単位に局所化できます。これによりプログラムの管理 が容易になります。また、それぞれのモジュールが汎用性のある部品として扱えれば、 それを繰り返し利用することができてソフトウェアの再利用性が高まります。  C言語ではモジュールとは関数(またはその集まり)のことをいいます。C++ では関 数とクラスのことを指します。ここでは関数を組み合わせてプログラムを構成する際、 関数のあいだで情報をやりとりするメカニズムについて考えます。  プログラムは常に関数の集まりとなっています。その1つは main()関数で、プログ ラムの最初に呼び出される特別な関数です。これ以外の関数はすべて main()関数から 直接あるいは間接に呼ばれます。関数を呼び出すには、その関数の名前を指定し、関数 の実行に必要な情報を引数として渡します。呼び出された関数は実行が終了すると結果 を返します。つまり、関数を呼び出す側は、関数にインプット情報を与えることで関数 に所定の仕事を処理させ、そのアウトプット情報を受け取ります。呼び出す側にとって 重要なのは、目的とする処理についての情報のインプット・アウトプットであって、関 数がどのように遂行したかを知る必要はありません。これは一種の内部処理の隠蔽ある いは抽象化です。  C標準ライブラリには、数学計算、文字や文字列操作、入出力、エラー処理のための 一連の関数群が提供されています。これらはC++プログラミング環境でも利用されます。 これらのライブラリ関数は、拡張子.hなどのヘッダーファイルをプログラムに読み込ま せることで利用できます。例えば数学ライブラリ関数を使うときは、プログラムファイ ルの先頭でプリプロセッサ命令 #include <math.h> を書くことで、ヘッダファイル math.h をインクルードします。ヘッダファイルの中は 関数のプロトタイプ宣言が次のような形式で書かれています。 戻り値の型 関数の名前(パラメータリスト); パラメータリスト(仮引数リストともいいます)は関数が受け取るインプット情報のデ ータ型の並びで、戻り値の型は関数が呼び出し側に返すデータ値の型を示します。  例えば、平方根を計算するsqrt()関数や、べき乗を計算するpow()関数はそれぞれ、 double sqrt(double x); double pow(double x, double y); で与えられ、sqrt()は浮動小数点数xの平方根を、pow()はxのy乗を計算して結果を 返します。プログラムの中では、次のように呼び出すことができます。 double a = 5.1, b; b = sqrt(a); は引数で渡したaの平方根をbに代入して受け取っています。関数プロトタイプ宣言は、 関数が受け取る引数の数、型と順番及び、戻り型をコンパイラに教える役割をします。 コンパイラは関数が呼ばれたときに、関数呼び出しが正しいかどうかを型チェックして、 エラー検査を行います。関数プロトタイプ宣言はC++では重要な機能をなします。他の 言語FORTRAN 77や初期のCでは、この様な型チェックがありませんでした。そのため、 致命的な実行時エラーが起こり得ました。  関数の定義の一般形は、 戻り値の型 関数の名前(パラメータリスト) {   宣言と文で書かれた関数定義の内容 } と書かれます。パラメータリストの数は幾つあっても構いませんが、戻り値は1つです。 また引数を持たない関数や、戻り値のない関数を定義するには型として voidを指定し ます。括弧{}の内部が関数の本体で、ブロックと呼ばれます。関数が結果を返さない 場合は、関数ブロックの末尾の括弧(})に達したとき終了して呼び出し側に制御を戻しま す。あるいは、次の文で戻ります。 return; また、結果を返す関数ではreturn文で値を返します。つまり return 式; によって式の値が呼び出し側に返されます。 [注]C++では関数が引数を取らない場合、パラメータリストを省略して空にして書くこと もできます。例えばmain()関数がそうです。しかしC言語では、voidを指定しなければ なりません。

2.6.2 値呼び出しと参照呼び出し  関数が1つのモジュール(部品)として成立するには、プログラムの中で独立性が保た れる必要があります。関数ブロック内で“普通”に宣言された変数はローカル変数として 扱われます。これらは関数の中だけで有効な変数で、外から見えなくなっています。つま り外(グローバル変数や他の関数内の変数)で同じ名前が使われていても衝突することは ありません。  また、これらローカル変数は関数が実行されるときに作成されて、関数の終了で破壊さ れます。この様なローカル変数のことを自動変数と呼びます。  従って関数との情報のやり取りは、引数と戻り値で行われます。普通プログラミング言 語には、関数を呼び出す方法に、値呼び出しと参照呼び出しの2つがあります。  値呼び出しでは、呼び出し側の変数の値のコピーが作られて、呼び出された関数に渡さ れます。渡されるのは値のコピーですから、そのコピーを関数の中で変更しても、呼び出 し側にある元の変数の値は影響を受けません。これに対して、参照渡しでは実体のある変 数そのものが渡されます。そのために渡された変数値を関数の中で書き換えると、元の変 数値も変更を受けます。  C/C++の関数呼び出しは値呼び出しで行われます。これによって関数の独立性が保証さ れ、信頼性の高いソフトウェアシステムの開発上重要なメリットとなります。ただし値呼 び出しの欠点は、大きなデータが渡されると、そのデータのコピーを作るために実行時間 やメモリが消費されます。この様な負荷をオーバーヘッドといいます。オーバーヘッドと は、実際の計算以外のことに使われるマシンへの余分な負荷のことです。  次のプログラムは値渡しの機構を示します。呼び出された関数は2つの変数値を受け取 ってその値を関数の中で交換します。けれども渡されるのは値のコピーであるために元の 変数値は変わりません。
// lst02_07.cpp
// 2つの値を交換
#include <iostream.h>

void swap_values(int, int); // 関数プロトタイプ宣言

int main()
{
    int a = 10, b = 20;

    cout << "関数呼び出し前:"
         << " a = " << a << ", b = " << b << endl << endl;

    swap_values( a, b );

    cout << "関数呼び出し後:"
         << " a = " << a << ", b = " << b << endl;

    return 0;
}

// 関数定義
void swap_values(int x, int y)
{
    int temp = x;
    x = y;
    y = temp;

    cout << "関数内で交換後:"
         << " x = " << x << ", y = " << y << endl << endl;
}

ソースファイル lst02_07.cpp
[実行結果]
関数呼び出し前: a = 10, b = 20           

関数内で交換後: x = 20, y = 10 

関数呼び出し後: a = 10, b = 20 
 しかし、ポインタを使うことで参照渡しをシミュレートすることができます。次の例 を見てください。
// lst02_08.cpp
// 2つの値を交換 ポインタ値を渡す
#include <iostream.h>

void swap_values(int *, int *); // 関数プロトタイプ宣言

int main()
{
    int a = 10, b = 20;

    cout << "関数呼び出し前:"
         << " a = " << a << ", b = " << b << endl << endl;

    swap_values( &a, &b ); // アドレスを渡す

    cout << "関数呼び出し後:"
         << " a = " << a << ", b = " << b << endl;

    return 0;
}

// 関数定義
void swap_values(int *x, int *y)
{
    int temp = *x;
    *x = *y;
    *y = temp;

    cout << "関数内で交換後:"
         << " *x = " << *x << ", *y = " << *y << endl << endl;
}
ソースファイル lst02_08.cpp
[実行結果]
関数呼び出し前: a = 10, b = 20            

関数内で交換後: *x = 20, *y = 10 

関数呼び出し後: a = 20, b = 10 
2つの値を交換する関数の引数の型はポインタ変数(int *)型になっています。ここで 関数の呼び出し形式と、関数の定義に注目してください。  呼び出しでは、 swap_values( &a, &b); というようにアドレスを渡しています。また関数の定義の中では、変数xとyはすべて 間接参照*されています。x、yはポインタ変数なのでその値はアドレスです。この関 数の仕事はポインタが指す元の変数値を交換することですから、*x、*yになります。そ の結果、呼び出し側の2つの変数は値を交換します。  しかしこの関数呼び出しでも、値渡しのルールは守られています。つまり関数呼び出 しの際に、ポインタxとyは int *x = &a; int *y = &b; のようにして、アドレス値で初期化されて、この値を使って呼び出し側の変数にアクセス します。この様にポインタを使って参照渡しと同じ機能をシミュレート(つまりこれは参 照渡しもどき)することができます。 [注]上の2つの例のように関数のプロトタイプ宣言では、引数には型だけを並べるのが 普通です。プロトタイプ宣言に変数名を付けてもかまいませんが、コンパイラはそれら の変数名を無視します。


2.6.3 参照(リファレンス)  C言語では、関数の参照渡しの機能をポインタを使って解決しますが、C++ではこれに 加えて、参照型による参照渡しを導入しています。これによって、参照呼び出しをより簡 単な形式で行うことができます。  参照(リファレンス Reference)は指定した変数のエイリアス(別名)として定義され る変数で、次のようにアンパーサンド(&)を使って宣言します。 int i = 10; int &ref = i; // refは変数iの別名 この例ではrefは「int への参照」を意味し、その型は(int &)型です。refはiのエイリ アスとなり、refに対する操作はすべて、元の変数 i本体に対して行われます。つまり、 ref = 20; は、変数 iに20が代入されます。参照変数は元の変数の別の名前に過ぎず、参照変数の メモリが確保されるわけではありません。これがポインタと違うところです。従って参照 宣言は定義を意味し、必ず宣言時にエイリアスとなる相手の変数で初期化しなければなり ません。また別の変数のエイリアスとして再設定することはできません。ですから参照変 数自体のアドレスや値があるわけではありません。次の簡単なテスト・プログラムがこの ことを示しています。
#include <iostream.h> 

int main() 
{ 
    int i = 10; 
    int &ref = i; 

    cout << "i = " << i << ", ref = " << ref << endl; 
    cout << "&i = " << &i << ", &ref = " << &ref << endl; 

    return 0; 
}
[実行結果]
i = 10, ref = 10                       
&i = 0x0063FDF4, &ref = 0x0063FDF4
この様に、参照 refは変数 iそのもの、つまりエイリアス(別名)です。  参照を使うと、次のプログラムのように関数の参照呼び出しができます。前のポインタ を使った場合と良く比べてください。
// lst02_09.cpp
// 2つの値を交換 参照を使う
#include <iostream.h>

void swap_values(int &, int &); // 関数プロトタイプ宣言

int main()
{
    int a = 10, b = 20;

    cout << "関数呼び出し前:"
         << " a = " << a << ", b = " << b << endl << endl;

    swap_values( a, b );

    cout << "関数呼び出し後:"
         << " a = " << a << ", b = " << b << endl;

    return 0;
}

// 関数定義
void swap_values(int &x, int &y)
{
    int temp = x;
    x = y;
    y = temp;

    cout << "関数内で交換後:"
         << " x = " << x << ", y = " << y << endl << endl;
}

ソースファイル lst02_09.cpp
[実行結果]
関数呼び出し前: a = 10, b = 20             

関数内で交換後: x = 20, y = 10 

関数呼び出し後: a = 20, b = 10 
 関数の定義とそれを呼び出す形式に注目してください。関数の引数はintへの参照 になっています。 void swap_values(int &x, int &y); 関数の中のコードは引数x、yの変数の値を交換させています。また関数呼び出しで は、変数a、bをそのまま渡しています。  これを前のポインタ変数の場合と比べると、その形式が簡単であることが分かりま す。アドレスの参照や変数値の間接参照を明示的に行う必要がありません。  参照呼び出しは、元の変数値を変更する必要があるとき以外にも性能面で意義があ ります。大きなデータを渡すときに、コピーを生成するためのオーバーヘッドを避け ることができます。しかし一方で、値渡しの場合に保証されていた関数の安全性が破 られます。関数の中から呼び出し側の値を変更可能となるからです。  変更する必要のない引数を渡すときには、この安全性と性能の両方を維持するため に引数に修飾子 const を付けます。次の例 double func( const double &x ); の様に、参照変数にconstを付けるとxは変更することができません。ただし、この 例は説明のためのものです。実際には、intdoubleなどの組込型のコピーのため のオーバーヘッドは無視できるほど小さなものです。ですから値渡しで行うのが普通 です。ここで想定する大きな引数とは、ユーザー定義の型である構造体やクラスなど のオブジェクトを指します。  以上を整理すると、関数に変更できない小さな引数を渡すときは値渡しを使います。 変更可能な引数を渡すときはポインタ(あるいは参照)を使います。変更する必要の ない大きな引数を渡す場合は、const付きの参照(場合によってはポインタ)を使い ます。 [注]C++では、ポインタや参照を次のように宣言することもできます。 int* ptr = &i; // int型の変数 iはすでに定義済みとして int& ref = j; // int型の変数 jはすでに定義済みとして この表記法の方が「int *ptr」「int &ref」と書くよりもポインタ型は int*で、参照 型は int&であることがよく分かります。ただしこの場合注意すべきことは、2つのポイ ンタや2つの参照を宣言するときに int i = 10, j = 20; int* ptr1, ptr2; // ptr1だけがポインタ int& ref1 = i, ref2 = j; // ref1だけが参照 とすると間違いになります。ptr2とref2はint型の変数です。

| 目次 | 前のページ | 次のページ |

Copyright(c) 1999 Yamada,K