● 関数で計算の分担

■ 関数

printf 文は文字列を画面に出力する命令ですが、実はこの printf文を 実行する際にコンピュータはとてもたくさんの仕事をしています。文字列 を画面に表示する仕事は思ったよりもずっと複雑な仕事なのです。 たくさんの基本的な命令を組み合わせることによってprintf命令は 作られているのです。
けれども私達はそのようなことは意識せずにただ単にprintf(" ")と 唱えるだけで文字列を画面に出すことができます。

このようにいくつかの命令をまとめて、それらをあたかもひとつの 命令にすることができます。
そして、そのまとまったひとつの命令を単に関数と呼びます。
さらに命令をどんどん関数にまとめていったプログラムのことを構造化 プログラムと呼びます。
関数という言葉は数学の言葉ですが、その意味は或る値を受けてそれに 応じてひとつの値を算出するという操作でした。しかしプログラムに おいての関数の意味はそれとは少し異なっていて、別に或る値を受ける 必要はなく算出する必要もありません。
とにかく関数を呼び出せば何かが起こるというぐらいの意味なのです。

このprintf関数は既にコンパイラ製作者によって作られているので 私達はこれを使うことができます。この関数の仕事内容の定義はライブラリ というファイルの中にあります。


■ 関数の設計

自分で関数を作ることもできます。 無限等比級数の総和を求めるプログラムを例にこれを関数に してみましょう。


▲ 関数化の第1段階:関数化しないで設計する (sample08.cc)

まずいつものようにプログラムを設計します。

// 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 );
}

このプログラムに解説は要らないでしょう。


▲ 関数化の第2段階:比例乗数の入力と結果の出力も含めて関数化する (sample09.cc)

先のプログラム(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()を唱えるだけで何も苦労せずに無限等比級数の総和を 知ることができます。この状況からわかるように、大きなプログラムを 作成する際に、まとまりごとに関数化することで複数の人に 作成作業を分担することができるのです。ひとりでプログラムを 作る場合でも関数化するとプログラムのどこで何が起こるかが はっきりわかるのでプログラムを作成しやすくなります。


▲ main関数

ところで、main( void )にも中括弧 { とその閉じ括弧 } があるので 関数の様な格好をしていることになります。実は main( void )はメイン 関数と呼ばれるプログラムの実行時に自動的に実行される 特別な関数なのです。

▲ 関数のprototype宣言

main()関数の定義の前でCalcSeries()関数を定義しましたが、 定義の順序を逆にすることもできます。ただしその場合には プログラムの冒頭の #include 文の直後辺りに次のようにして CalcSeries()が関数であることの宣言をしなければなりません。
#include <stdio.h>

void CalcSeries( void );
()の直後に;(セミコロン)を忘れずに。この宣言を関数のprototype宣言 と呼びます。prototype宣言はcompilerにそれが関数であることや、 その関数の正しい呼び出し方を教えることになります。


▲ 変数のスコープ

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変数 (広域変数)と呼びます。しかし上記のような衝突の現象があるので、 広域変数の利用は慎重にしなければなりません。


▲ 変数の寿命

変数のスコープと共に大事な事項に変数の寿命があります。 変数は宣言することで誕生するのですから、やがて消滅します。 変数が消滅すると、それに記憶されていた値は、もはや利用できないと 思って良いでしょう。 関数開始冒頭で普通に int a; などと宣言した変数が消滅するのは 関数が終って呼び出しもとに返るときです。従ってこの場合の 変数の寿命は関数内ということになります。 次の簡単な関数のように変数をforループの中括弧内で宣言することも できます。
void func( void )
{
  for( int i=0 ; i<10 ; i++ ){
    int j=0;
  }
}
この変数jのスコープの範囲はこのforループの中括弧内と なります。寿命はどうでしょう。forループではループの一回転が終ると forの直後の()の中の作業をするのでした。つまり中括弧から出ています。 なのでこの変数jはこのとき消滅されます。そして直後の次の一回転 の冒頭で再び宣言されます。この例では0で初期化されます。 ループの毎回に変数の誕生と消滅が繰り返されるのです。 実行効率が下がるような気がしますが、コンパイラがうまく 効率が下がらないようにしてくれます。 心に留めておいて欲しいことは、このように宣言した変数は ループの次の回まで値が保存されている保証がないことです。

関数の外で定義した広域変数の寿命はプログラムの開始から終了までです。
関数内の変数の寿命をもっと長くすることができます。

void Visit( void )
{
  static int times = 0;
  times++;
  printf("You have visitted this function %d times.\n", times );
}
この関数は呼び出されるたびに、呼ばれた回数を表示します。 変数 times の宣言に staticが冠されています。こうするとこの 変数は関数が終了しても消滅せずに残るので、再度この関数を 呼び出した時にも変数の値が保存されています。このtimesの値を0に 初期化する作業が行われるのはこの関数を最初に呼び出した時だけです。 次回の呼び出しではこの初期化は行われません。なのでこの例では 呼ばれた回数が表示されます。このような変数をstatic変数と呼びます。 スコープが関数内であることには変わりありません。

▲ 関数化の第3段階:値を取り引きする関数を設計する (sample10.cc)

先のプログラムで設計した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 の意味は関数が値を戻さない ことを意味しています。


▲ main関数の戻り値

先のプログラム sample10.cc では main()関数にも return があり、 値 0 を返しています。これに対応して main()関数の頭も int main(void) と int型の値を返すように指定されています。この値は、main()関数を 呼び出したところ、つまりコマンドラインや shell を司っているOSに 返されます。普通、プログラムが予定どおりに仕事を終えて正常に終了する 際には 0 を OSに返すことにより正常終了を OSに伝える決まりになって います。それまでの例では簡単のため void main() としていましたが、 OSへの気配りを考えるなら int main() として、0から255までの適当な値を 返すように心掛けると良いでしょう。


▲ main関数の引数

main()関数も引数を受け取ることができます。 その引数とはプログラム実行時に与えられたコマンドライン引数のことです。
例えばプログラム sample11 を次のように実行します。

sample11 first second third
この "sample11", "first", "second", "third" の文字列が コマンドライン引数です。

しかしmain()関数でのこれらのコマンドライン引数の受け取り方は 普通の関数での引数とは少々異なります。OSはこのようなコマンドライン引数 を次のような"感じ"で文字列用のポインタ配列変数に格納します。

int   argc    = 4;
char* argv[4] = {"sample11", "first", "second", "third"};
その後にOSはmain()関数を次のような"感じ"で呼び出します。
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次元配列変数を関数に渡す例 (sample12.cc)

配列変数のその要素すべてまるごとを関数の引数として渡すことが できます。以下の例は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つの配列間で値をコピーする関数を作ることができます。


▲ 1次元配列変数を関数間で取り引きする例 (sample13.cc)

配列変数を関数に渡すと、関数から呼び出し元の配列を操作することが できることを利用して配列のコピーを行う関数 Copy1DArray()を設計します。
// 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次元配列変数を関数間で取り引きする例 (sample14.cc)

厄介なのが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次元配列の要素数をこのように明記する必要があります。 ただし最高次の次元の要素数の指定は省略することができます。


  • 次へ
  • 目次
    Copyright(C) by Naoki Watanabe. Oct 21st, 1995.
    This page was modified on Aug 3rd, 1998.
    渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp