printf 文は文字列を画面に出力する命令ですが、実はこの printf文を
実行する際にコンピュータはとてもたくさんの仕事をしています。文字列
を画面に表示する仕事は思ったよりもずっと複雑な仕事なのです。
たくさんの基本的な命令を組み合わせることによってprintf命令は
作られているのです。
けれども私達はそのようなことは意識せずにただ単にprintf(" ")と
唱えるだけで文字列を画面に出すことができます。
このようにいくつかの命令をまとめて、それらをあたかもひとつの
命令にすることができます。
そして、そのまとまったひとつの命令を単に関数と呼びます。
さらに命令をどんどん関数にまとめていったプログラムのことを構造化
プログラムと呼びます。
関数という言葉は数学の言葉ですが、その意味は或る値を受けてそれに
応じてひとつの値を算出するという操作でした。しかしプログラムに
おいての関数の意味はそれとは少し異なっていて、別に或る値を受ける
必要はなく算出する必要もありません。
とにかく関数を呼び出せば何かが起こるというぐらいの意味なのです。
このprintf関数は既にコンパイラ製作者によって作られているので 私達はこれを使うことができます。この関数の仕事内容の定義はライブラリ というファイルの中にあります。
自分で関数を作ることもできます。 無限等比級数の総和を求めるプログラムを例にこれを関数に してみましょう。
まずいつものようにプログラムを設計します。
// sample08.cc #include <stdio.h> main( void ) { double r, term=1.0, sum=0.0; printf("Input a number below 1.0 ="); scanf("%lf", &r ); // 比例乗数 r の入力受け付け while( term > 0.0 ){ // 足し上げる項が0より大きい間繰り返す sum += term; // 項の足し上げ term *= r; // 乗数をかけて次の項の値とする } printf("The total of this series is %lf\n", sum ); }
このプログラムに解説は要らないでしょう。
先のプログラム(sample08.cc)の 比例乗数の入力から結果の出力までの部分(つまりほとんど全部)を ひとつの新しい関数にまとめましょう。この新しい関数の名前は CalcSeriesとしましょう。つまりCalcSeries();と唱えるだけで 比例乗数の受け付け、総和の計算、出力を行ってもらえるように するのです。次のようにプログラムを記述します。
// sample09.cc #include <stdio.h> void CalcSeries( void ) // 関数CalcSeries() の定義の開始 { double r, term=1.0, sum=0.0; printf("Input a number below 1.0 ="); scanf("%lf", &r ); // 比例乗数 r の入力受け付け while( term > 0.0 ){ // 足し上げる項が0より大きい間繰り返す sum += term; // 項の足し上げ term *= r; // 乗数をかけて次の項の値とする } printf("The total of this series is %lf\n", sum ); } // 関数の定義の終了 main( void ) { CalcSeries(); // 関数CalcSeries() の実行 その1 CalcSeries(); // 関数CalcSeries() の実行 その2 }
main( void )の手前にいろいろ登場しました。
void CalcSeries( void )の行が新たなこの関数の名前を宣言します。
2つの void については後で解説します。
続く中括弧 { とその閉じ括弧 } の範囲内で関数の作業内容を
定義します。
main( void )の手前にいろいろあるにも関わらずプログラムは
このmain( void )の所から実行が開始されます。そして
CalcSeries()関数を2回呼び出して終了します。
関数にしたので、単にCalcSeries()と唱えるだけで計算が行えるので、
このように何度もその計算を行わせるのも簡単です。
また、CalcSeries()関数の定義の所だけを友達にあげれば、その友達は
単にCalcSeries()を唱えるだけで何も苦労せずに無限等比級数の総和を
知ることができます。この状況からわかるように、大きなプログラムを
作成する際に、まとまりごとに関数化することで複数の人に
作成作業を分担することができるのです。ひとりでプログラムを
作る場合でも関数化するとプログラムのどこで何が起こるかが
はっきりわかるのでプログラムを作成しやすくなります。
#include <stdio.h>()の直後に;(セミコロン)を忘れずに。この宣言を関数のprototype宣言 と呼びます。prototype宣言はcompilerにそれが関数であることや、 その関数の正しい呼び出し方を教えることになります。
void CalcSeries( void );
CalcSeries()関数内では3つの変数 r, term, sum が宣言かつ定義 されています。この変数はこのCalcSeries()関数内でしか利用することは できません。つまり例えばmain関数内で r=0.5; としても代入できませんし、 printfで出力することもできません。このように変数にはそれを利用できる 範囲があります。それをスコープと呼びます。スコープはその変数の宣言を 囲む最も内側の中括弧{}の範囲と決まっています。 ですからCalcSeries()関数内の変数はその関数内がスコープとなります。
でもなぜ変数にスコープがあるのでしょうか。友達が作った関数を もらって自分のプログラムに組み込むとしましょう。もし変数にスコープが なく、関数内で定義した変数が他の関数でも利用できるとしたらどうなるで しょう。自分が用意した変数と同じ名前の変数がその友達の関数内で 使われていたら、予想外の動作として、自分の変数の値が上書きされ 壊されてしまうのです。例えば、
このようなことが起こるようでは友達から気安く関数をもらうわけには いきません。とういわけで関数内で定義した変数はその関数内でしか 利用できないようになっているのです。main( void ) //自作のプログラム { double term = 2.0; CalcSeries(); // 友達から借りた関数を呼び出す printf("%lf\n", term ); // あれ? 値が変わっているぞ ?? どうして ??? }
スコープが関数内など狭い範囲に限定された変数のことをlocal変数 (局所変数)と呼びます。 関数の外側で変数を宣言するとそれを囲む中括弧が無いので、その変数の スコープはそれ以降のプログラム全体となります。 つまりプログラムの冒頭の関数の外あたりで変数を宣言すれば、それは どこからでも読み書きできます。このような変数のことをglobal変数 (広域変数)と呼びます。しかし上記のような衝突の現象があるので、 広域変数の利用は慎重にしなければなりません。
この変数jのスコープの範囲はこのforループの中括弧内と なります。寿命はどうでしょう。forループではループの一回転が終ると forの直後の()の中の作業をするのでした。つまり中括弧から出ています。 なのでこの変数jはこのとき消滅されます。そして直後の次の一回転 の冒頭で再び宣言されます。この例では0で初期化されます。 ループの毎回に変数の誕生と消滅が繰り返されるのです。 実行効率が下がるような気がしますが、コンパイラがうまく 効率が下がらないようにしてくれます。 心に留めておいて欲しいことは、このように宣言した変数は ループの次の回まで値が保存されている保証がないことです。void func( void ) { for( int i=0 ; i<10 ; i++ ){ int j=0; } }
関数の外で定義した広域変数の寿命はプログラムの開始から終了までです。
関数内の変数の寿命をもっと長くすることができます。
この関数は呼び出されるたびに、呼ばれた回数を表示します。 変数 times の宣言に staticが冠されています。こうするとこの 変数は関数が終了しても消滅せずに残るので、再度この関数を 呼び出した時にも変数の値が保存されています。このtimesの値を0に 初期化する作業が行われるのはこの関数を最初に呼び出した時だけです。 次回の呼び出しではこの初期化は行われません。なのでこの例では 呼ばれた回数が表示されます。このような変数をstatic変数と呼びます。 スコープが関数内であることには変わりありません。void Visit( void ) { static int times = 0; times++; printf("You have visitted this function %d times.\n", times ); }
先のプログラムで設計したCalcSeries()関数は入力も出力も 行ってくれますが、機能を削減して、純粋に無限等比級数の総和を 計算するだけの関数にしましょう。となるとこの関数を呼び出す際には 比例乗数の値を渡す必要があるでしょう。また計算結果をこの関数から 受け取る必要もあります。つまり
のように関数を呼び出すことになります。これは数学関数と同じような 扱い方ですね。関数をこのような目的に設計するには次のようにします。main( void ) { double r=0.5, sum; sum = CalcSeries( r ); }
// sample10.cc #include <stdio.h> double CalcSeries( double ); // 関数CalcSeries() のprototype宣言 int main( void ) { double r, sum; printf("Input a number below 1.0 ="); scanf("%lf", &r ); // 比例乗数 r の入力受け付け その1 sum = CalcSeries(r); // 関数CalcSeries() の実行 その1 printf("The total of this series is %lf\n", sum ); printf("Input a number below 1.0 ="); scanf("%lf", &r ); // 比例乗数 r の入力受け付け その2 sum = CalcSeries(r); // 関数CalcSeries() の実行 その2 printf("The total of this series is %lf\n", sum ); return(0); } double CalcSeries( double r ) // 関数CalcSeries() の定義の開始 { double term=1.0, sum=0.0; // 局所変数の宣言と定義 while( term > 0.0 ){ // 足し上げる項が0より大きい間繰り返す sum += term; // 項の足し上げ term *= r; // 乗数をかけて次の項の値とする } return( sum ); // 合計の値を呼び出し元へ戻す } // 関数の定義の終了
prototype宣言を用意したので関数の定義をmain関数の後ろに置くことが
できます。CalcSeries()関数の定義の所では関数の名前の直前に
doubleが付いています。これはこの関数が計算結果としてdouble型の値を
呼び出し元に戻すことを意味しています。これに対応して関数の最後に
return( sum ); として確かに double型の値を返しています。このような
戻す値を戻り値と呼びます。
また関数の名前の直後の( double r ) はこの関数が呼び出される際に
double型の値ひとつを受け取って、その値をこの関数のlocal変数rに
代入することを意味しています。このような受ける値のことを
引数と呼びます。
一方prototype宣言の方では関数の名前の直後は( double ) だけで rが
ありません。prototype宣言には受け取った値を代入する変数の名前までを
compilerに教える必要は無いのです。
このように関数を定義すると、この関数の呼び出し元であるmain関数
で sum = CalcSeries(r); とすると main関数内の local変数 r の
値がCalcSeries()関数内の引数としてlocal変数 r にコピーされます。
両者の変数は名前は同じでも別物の変数です。CalcSeries()関数内でrを
上書きしてもmain関数内の r の値は不変です。
そして CalcSeries()関数の計算結果 sum を return( sum ) とすることで、
この変数の値が関数の呼び出し元へ戻り値としてコピーされます。
最後にそれが main関数内のlocal変数 sumに代入されます。
このようにして値を呼び出し元と取り引きする関数を設計することが できます。以前に紹介したプログラムでは関数の引数は ( void ) と なっていました。これは引き数を取らないという意味です。 同様に関数名の前に書いてある void の意味は関数が値を戻さない ことを意味しています。
先のプログラム sample10.cc では main()関数にも return があり、 値 0 を返しています。これに対応して main()関数の頭も int main(void) と int型の値を返すように指定されています。この値は、main()関数を 呼び出したところ、つまりコマンドラインや shell を司っているOSに 返されます。普通、プログラムが予定どおりに仕事を終えて正常に終了する 際には 0 を OSに返すことにより正常終了を OSに伝える決まりになって います。それまでの例では簡単のため void main() としていましたが、 OSへの気配りを考えるなら int main() として、0から255までの適当な値を 返すように心掛けると良いでしょう。
main()関数も引数を受け取ることができます。
その引数とはプログラム実行時に与えられたコマンドライン引数のことです。
例えばプログラム sample11 を次のように実行します。
sample11 first second thirdこの "sample11", "first", "second", "third" の文字列が コマンドライン引数です。
しかしmain()関数でのこれらのコマンドライン引数の受け取り方は 普通の関数での引数とは少々異なります。OSはこのようなコマンドライン引数 を次のような"感じ"で文字列用のポインタ配列変数に格納します。
その後にOSはmain()関数を次のような"感じ"で呼び出します。int argc = 4; char* argv[4] = {"sample11", "first", "second", "third"};
文字列用のポインタ配列変数については次節で解説します。main( argc, argv );
従ってmain()関数でこれらの引数を受け取るには 次のようにmain()関数を定義します。
// sample11.cc #include <stdio.h> int main( int argc, char* argv[] ) { for( int n=0 ; n<argc ; n++ ){ printf("%d %s\n", n, argv[n] ); } return(0); }
関数から戻される値はただひとつだけですが、関数の複数の結果の値を 呼び出し元で得たい場合があります。C++言語ではこの場合、参照と 呼ばれる機構を利用します。C言語ではポインタ変数を利用するのですが その方法は本書の別のどこかで説明します。C++での参照を使う方法を 紹介します。
参照を使わないとどうしようにもならない例としてswapを紹介します。 swapは2つの変数の値を互いに入れ換える操作のことです。このような 操作を行う関数 swap() は次のようにして設計されます。
// sample12.cc #include <stdio.h> void swap( int& x, int& y ) { int tmp; tmp = x; x = y; y = tmp; } int main( void ) { int a=1, b=2; swap( a, b ); printf("a=%d, b=%d\n", a, b ); }
関数の呼び出し方はいつもと同じですが、関数の引数の定義の所で int& x のように & マークがついています。このように引数を 宣言すると、呼び出し元で渡された変数 a とswap関数の引数の変数 x とが同一物となります。このxが参照と呼ばれる特殊な変数です。 コピーでなく同一物なので、xに対する代入 操作は呼び出し元の aに対する代入になります。そのためこの例では swap関数に渡した2つの引数 a, b の両方にswap関数の作業結果である 交換した値が入ることになります。
参照を使うことにより複数の値を関数から呼び出し元へ返すことが できます。これはC++独特の表現であり、C言語では使えません。
配列変数のその要素すべてまるごとを関数の引数として渡すことが できます。以下の例は1次元配列変数の各要素の値を表示する関数 Print1DArray()の作成例です。
// sample12.cc #include <stdio.h> void Print1DArray( int [] ); int main( void ) { int a[10]={0,1,2,3,4,5,6,7,8,9}; // 配列の宣言と初期化 Print1DArray( a ); // 配列を関数の引数として渡す return(0); } // 配列の要素を表示する関数 void Print1DArray( int x[] ) { int n; for( n=0 ; n<10 ; n++ ){ printf("%d ", x[n] ); } }
つまり呼び出し元で配列を関数に渡すには単に配列の団体名を 指定するだけです。そして関数側で1次元配列を受け取るには( int x[] ) のように1次元配列を表す[]と型のintと、この関数内での配列のlocalな 団体名となる x を指定すれば良いのです。こうして関数内では x[n] と して渡された1次元配列の要素を読むことができます。
プログラムsample10.ccでは関数の呼び出し元で引数に指定された 変数と関数内で引数を受け取る変数とは別物でした。しかし、 プログラムsample12.ccでの main関数内の配列変数 a の各要素と Print1DArray()関数の配列変数 x の各要素はコピーではなく全く同一物です。 先に紹介した参照と同じようなものです。 コピーしないので、配列を関数に渡す操作は負担の軽い操作となっています。 Print1DArray()関数内で x[5] = 1.0; とすると main関数内の a[5] が 1.0 になるのです。このことを利用して 2つの配列間で値をコピーする関数を作ることができます。
このようにして関数間で大規模なデータの取り引きが行えます。// sample13.cc #include <stdio.h> void Copy1DArray( int [] , int [] ); void Print1DArray( int [] ); int main(void) { // 配列の宣言、一方は初期化する。 int a[10], b[10]={0,1,2,3,4,5,6,7,8,9}; Copy1DArray( a, b ); // 配列ごとのコピー Print1DArray( a ); // 配列ごとの表示 return(0); } // 配列をコピーする関数 void Copy1DArray( int x[] , int y[] ) { int n; for( n=0 ; n<10 ; n++ ){ x[n] = y[n]; } } // 配列の要素を表示する関数 void Print1DArray( int x[] ) { int n; for( n=0 ; n<10 ; n++ ){ printf("%d ", x[n] ); } }
厄介なのが2次元配列を関数間と取り引きする方法です。 ここでは簡単な逃げ技を紹介します。ちゃんとした対策は別のどこかで 紹介します。
要素数があらかじめわかっている2次元配列に値を設定する関数 Set2DArray()関数の作成例を以下の載せます。
// sample14.cc #include <stdio.h> void Set2DArray( int [][10] ); // 配列の低次の次元の要素数を明記する int main( void ) { int a[10][10]; // 配列の宣言 Set2DArray( a ); // 配列を関数の引数として渡す return(0); } // 2次元配列に値を設定する関数 void Set2DArray( int x[][10] ) // 配列の低次の次元の要素数を明記する { int n, m; for( n=0 ; n<10 ; n++ ){ for( m=0 ; m<10 ; m++ ){ x[n][m] = n*m; // 配列の要素に適当な値を代入する } } }
2次元配列を関数に渡すには、関数の引数の定義で 受け取る2次元配列の要素数をこのように明記する必要があります。 ただし最高次の次元の要素数の指定は省略することができます。