関数を実行することを「関数を呼ぶ」と表現する。関数の小括弧の中で指定 する変数を引数と呼ぶ。そして関数が呼び出し元へ戻す値を戻り値と呼ぶ。double r, a; r = 1.0; a = CircleArea( r );
関数 CircleArea() を設計するには、まず最初に関数の表向きの姿を 宣言する。関数の名前 (使える文字の制限は変数名と同じ。) には、その作業内容が自明になるような名前をつけるべきである。 そして受け取る引数の型を指定し、関数が最終的に戻す値の型を指定する。 次の様になる。
関数名の前の double はこの関数の戻す値の型を表し、 小括弧 () 内のひとつの double はこの関数が double 型の値をひとつ引数として受け取ることを表す。 この様な表向きの姿の宣言のことをprototype宣言と呼ぶ。double CircleArea( double );
次に関数の具体的な作業内容を設計する。次の様になる。
最初の行は、先のprototype宣言と似ているが、小括弧の中で 引数の変数を定義する。またセミコロンがない。 最初の例でこの関数を呼ぶ際に引数に指定した変数 r の値が この関数の引数の変数 radius に代入されるのである。double CircleArea( double radius ) { double area; area = M_PI*radius*radius; return( area ); }
関数の作業内容は中括弧 {} の内側に記述する。 area という 変数を用意し、それに円の面積を計算して代入する。なお M_PI は円周率として定義されている定数であるとする。 最後に return として変数 area の値を関数の呼出元へ戻す。 これで関数 CircleArea() が完成である。
一般の関数の設計において、引数が複数ある場合には、各引数を , (カンマ)で区切って並べて定義すれば良い。引数がひとつもない関数も 作ることができ、その場合の関数の定義では受け取るものが無いことを示す void を小括弧の中に書いておけば良い。呼び出す際には小括弧の 中身は空で良い。戻り値が無い関数も作ることができ、その場合の関数の 定義では返すものが無いことを示す void を関数名の前に書いておけば 良い。 return 文は無くても良い。
作業を関数にまとめることの利点はいろいろがあるが主な利点は次の 通りである。
関数内部で定義され、その中でのみ有効な変数の ことをlocal(局所)変数と呼ぶ。これと反対に関数外部で定義され、 プログラムの至る所で有効な変数のことをglobal(広域)変数と 呼ぶ。global変数は便利ではあるが、意図しない所で値が変更される危険性 があることに注意しなければならない。 local変数は普通、関数の実行のたびに新しく用意され、関数の終了とともに 破棄される。つまり変数の寿命も関数内に限られる。従ってlocal変数の値は 次にこの関数を呼ぶ時まで保存されない。double SquareArea( double size ) { double area; area = size*size; return( area ); }
関数の終了後にもlocal変数の値を保存させることもできる。次の関数 Visit() はこの関数を訪れた回数を戻す。
local変数の宣言の前に static を冠するとこの変数の寿命は プログラム開始から終了までとなり、 関数の終了後も値は保存される。この様な変数をstaitc変数と呼ぶ。 変数の有効範囲が関数内であることには変わりはない。 static変数の初期化は最初の宣言の時だけ行われる。 これに対して、普通の変数はstack変数と呼ばれ、 宣言の度に初期化が毎回行われる。int Visit( void ) { static int visit=0; visit++; return( visit ); }
また、巨大な配列変数はstatic変数として用意する方が良い。
この関数を次の様に呼ぶ。void Circle( double radius, double& area, double& length ) { area = M_PI*radius*radius; length = 2.0*M_PI*radius; }
関数の定義の第2,第3引数の型名の直後に & (アンパサンド) マークが付いている。この関数を呼ぶと通常の引数である第1引数 radius には呼び出し元の変数 r の値が代入されるが、 第2,第3引数の area, length は 呼び出し元の変数 a, l そのものと同一視される。つまり 変数 area, length に対する読み書きは、呼び出し元の変数 a, l に対する読み書きとなる。double r, a, l; r = 1.0; Circle( r, a, l );
結果として他の関数のlocal変数への読み書きが可能になり、 実質的に複数の値を呼び出し元へ返すことができる。 この様に変数の値ではなく変数自身を関数へ渡すことを参照渡しと呼ぶ。
ちなみにC言語には参照渡しの機構はなく、アドレス渡しとポインタを 利用して同様の機構を得ていたが、いささか複雑で見にくいものであった。 C言語でのアドレス渡しの方法を同じ例で示しておく。
この関数を次の様に呼ぶ。void Circle( double radius, double *area, double *length ) { *area = M_PI*radius*radius; *length = 2.0*M_PI*radius; }
double r, a, l; r = 1.0; Circle( r, &a, &l );
そして、関数の呼び出し元では次の様に配列名だけを関数に渡す。void SetArray( double array[10] ) { for( int i=0 ; i<10 ; i++ ){ array[i] = i; // set some value } }
関数内の配列 array[] は呼び出し元の配列 a[] と同一視される。各要素の値がコピーされるのではない。 従って、配列 array[] の各要素への読み書きは 呼び出し元の配列 a[] の各要素への読み書きとなるのである。 これで実質的に配列のデータの関数への受渡しが可能になる。 なお、1次元配列の場合には、関数の引数の定義で array[10] のサイズ の指定は省略でき、単に array[] と書くことができる。なぜなら、 ここで配列を実際に用意するわけではないからである。double a[10]; SetArray( a );
2次元以上の高次元配列も同様に関数への受渡しができる。
ここで配列のサイズの指定を省略することはできない (1つ目の次元のサイズの指定は省略できる。)。なぜなら、サイズ 指定がないと、要素 array[2][1] が配列の先頭から 2*20+1 番目に在ることを compilerに理解してもらえないからである。void Set2DArray( double array[10][20] ){ /* do something */ }
それでも任意サイズの2次元配列を受け取れる関数を作りたいならば、 関数の呼び出し側で、次の細工を施す必要がある。
つまり2次元配列の各行の1次元配列の先頭アドレスたちを格納する ポインタ配列 *ap[] を用意して、それを関数に渡すのである。 関数はこれを次の様に受け取る。double a[10][20], *ap[10]; for( int i=0 ; i<10 ; i++ ) ap[i] = a[i]; Set2DArray( ap, 10, 20 );
ポインタ変数のアドレスを2回たどって目的の要素に アクセスする。配列のサイズが2の巾乗でもアクセス速度は遅い。void Set2DArray( double *array[], int n, int m ) { for( int i=0 ; i<n ; i++ ){ for( int j=0 ; j<m ; j++ ){ array[i][j] = i; // set some value } } }
C++には関数のこの欠点を克服する機構に inline関数がある。 例えば次の様に2乗を計算する関数 sqr() を作る。
関数の定義に inline を冠するのである。そしてこの関数を呼ぶ。inline double sqr( double t ) { return( t*t ); }
コンパイラは inline関数が呼び出されるすべての箇所において、 関数の内容を次の様に自動的に展開する。double a, b, c; c = sqr( a+b );
実行の際には関数を呼ぶ手間がなくなるのである。 inline関数内で定義した変数はやはり関数内のみで有効であり、 呼び出す所に展開されても、そこの変数と衝突しないように 適切に取り計られる。double a, b, c; double t; t = a+b; c = t*t;