● 関数

C言語での関数の意味は数学での関数の意味である写像とは異なり、必ずしも値 を受けて何らかの値を返す必要はない。C言語での関数の意味は、プログラム の実行をそこに移して何らかの作業を行ってまた戻ることである。

■ 関数の設計

円の面積を計算する関数を例に関数の設計の仕方を紹介する。 目的は以下の様に半径 r の円の面積が関数 CircleArea() によって計算されて変数 area に 格納されることである。
double r, a;
r = 1.0;
a = CircleArea( r );
関数を実行することを「関数を呼ぶ」と表現する。関数の小括弧の中で指定 する変数を引数と呼ぶ。そして関数が呼び出し元へ戻す値を戻り値と呼ぶ。

関数 CircleArea() を設計するには、まず最初に関数の表向きの姿を 宣言する。関数の名前 (使える文字の制限は変数名と同じ。) には、その作業内容が自明になるような名前をつけるべきである。 そして受け取る引数の型を指定し、関数が最終的に戻す値の型を指定する。 次の様になる。

double CircleArea( double );
関数名の前の double はこの関数の戻す値の型を表し、 小括弧 () 内のひとつの double はこの関数が double 型の値をひとつ引数として受け取ることを表す。 この様な表向きの姿の宣言のことをprototype宣言と呼ぶ。

次に関数の具体的な作業内容を設計する。次の様になる。

double CircleArea( double radius )
{
  double area;
  area = M_PI*radius*radius;
  return( area );
}
最初の行は、先のprototype宣言と似ているが、小括弧の中で 引数の変数を定義する。またセミコロンがない。 最初の例でこの関数を呼ぶ際に引数に指定した変数 r の値が この関数の引数の変数 radius に代入されるのである。

関数の作業内容は中括弧 {} の内側に記述する。 area という 変数を用意し、それに円の面積を計算して代入する。なお M_PI は円周率として定義されている定数であるとする。 最後に return として変数 area の値を関数の呼出元へ戻す。 これで関数 CircleArea() が完成である。

一般の関数の設計において、引数が複数ある場合には、各引数を , (カンマ)で区切って並べて定義すれば良い。引数がひとつもない関数も 作ることができ、その場合の関数の定義では受け取るものが無いことを示す void を小括弧の中に書いておけば良い。呼び出す際には小括弧の 中身は空で良い。戻り値が無い関数も作ることができ、その場合の関数の 定義では返すものが無いことを示す void を関数名の前に書いておけば 良い。 return 文は無くても良い。

作業を関数にまとめることの利点はいろいろがあるが主な利点は次の 通りである。

  • 再利用
    どこからでも何度でもこの関数を呼ぶことができ、 関数の内容を毎回いちいち記述しなくて済む。
  • 明確化
    一連の作業を関数にまとめることで作業の内容が明確になり、 プログラムが読みやすくなる。
  • 信頼性
    どこかに潜んでいるバグを特定するには、関数ごとに信頼性を 検査すればよく、バグの特定が容易になる。
  • 分業化
    大きなプログラムを小さな関数の組合せにして、 プログラムの開発を複数の人間で関数単位で分業できる。

  • ■ 変数の有効範囲と寿命

    関数内部で宣言した変数は関数内部のみで有効である。 正方形の面積を計算する次の関数 SquareArea() 内で 定義されている変数 area は先の例の関数 CircleArea() 内の変数 area とは全く別物である。
    double SquareArea( double size )
    {
      double area;
      area = size*size;
      return( area );
    }
    
    関数内部で定義され、その中でのみ有効な変数の ことをlocal(局所)変数と呼ぶ。これと反対に関数外部で定義され、 プログラムの至る所で有効な変数のことをglobal(広域)変数と 呼ぶ。global変数は便利ではあるが、意図しない所で値が変更される危険性 があることに注意しなければならない。 local変数は普通、関数の実行のたびに新しく用意され、関数の終了とともに 破棄される。つまり変数の寿命も関数内に限られる。従ってlocal変数の値は 次にこの関数を呼ぶ時まで保存されない。

    関数の終了後にもlocal変数の値を保存させることもできる。次の関数 Visit() はこの関数を訪れた回数を戻す。

    int Visit( void )
    {
      static int visit=0;
      visit++;
      return( visit );
    }
    
    local変数の宣言の前に static を冠するとこの変数の寿命は プログラム開始から終了までとなり、 関数の終了後も値は保存される。この様な変数をstaitc変数と呼ぶ。 変数の有効範囲が関数内であることには変わりはない。 static変数の初期化は最初の宣言の時だけ行われる。 これに対して、普通の変数はstack変数と呼ばれ、 宣言の度に初期化が毎回行われる。

    また、巨大な配列変数はstatic変数として用意する方が良い。


    ■ 参照渡し

    関数が戻り値として戻すことができる値は変数ひとつ分だけである。 複数の値をその関数の呼出元へ返したい場合には関数の引数に 参照(alias)として渡すのが便利である。これはC++の機能である。 以下の例は円の半径を与えて面積と周の長さを計算して返す関数 Circle() である。
    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 );
    
    関数の定義の第2,第3引数の型名の直後に & (アンパサンド) マークが付いている。この関数を呼ぶと通常の引数である第1引数 radius には呼び出し元の変数 r の値が代入されるが、 第2,第3引数の area, length は 呼び出し元の変数 a, l そのものと同一視される。つまり 変数 area, length に対する読み書きは、呼び出し元の変数 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 );
    

    ■ 配列渡し

    配列変数の全部の要素を一度に関数へ渡すことができる。 1次元配列の場合、次の様に関数の定義において、引数の変数として あたかも配列変数を定義するかの様にする。
    void SetArray( double array[10] )
    {
      for( int i=0 ; i<10 ; i++ ){
        array[i] = i;  // set some value
      }
    }
    
    そして、関数の呼び出し元では次の様に配列名だけを関数に渡す。
    double a[10];
    SetArray( a );
    
    関数内の配列 array[] は呼び出し元の配列 a[] と同一視される。各要素の値がコピーされるのではない。 従って、配列 array[] の各要素への読み書きは 呼び出し元の配列 a[] の各要素への読み書きとなるのである。 これで実質的に配列のデータの関数への受渡しが可能になる。 なお、1次元配列の場合には、関数の引数の定義で array[10] のサイズ の指定は省略でき、単に array[] と書くことができる。なぜなら、 ここで配列を実際に用意するわけではないからである。

    2次元以上の高次元配列も同様に関数への受渡しができる。

    void Set2DArray( double array[10][20] ){ /* do something */ }
    
    ここで配列のサイズの指定を省略することはできない (1つ目の次元のサイズの指定は省略できる。)。なぜなら、サイズ 指定がないと、要素 array[2][1] が配列の先頭から 2*20+1 番目に在ることを compilerに理解してもらえないからである。

    それでも任意サイズの2次元配列を受け取れる関数を作りたいならば、 関数の呼び出し側で、次の細工を施す必要がある。

    double a[10][20], *ap[10];
    for( int i=0 ; i<10 ; i++ ) ap[i] = a[i];
    Set2DArray( ap, 10, 20 );
    
    つまり2次元配列の各行の1次元配列の先頭アドレスたちを格納する ポインタ配列 *ap[] を用意して、それを関数に渡すのである。 関数はこれを次の様に受け取る。
    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
        }
      }
    }
    
    ポインタ変数のアドレスを2回たどって目的の要素に アクセスする。配列のサイズが2の巾乗でもアクセス速度は遅い。

    ■ inline関数

    関数を用いるとプログラムは実行を関数と呼び出し元を往復することになり、 移動の作業のための手間がわずかながらかかる。作業内容が少量の関数を 頻繁に呼ぶ場合に、この移動のための実行効率の低下が表れる。

    C++には関数のこの欠点を克服する機構に inline関数がある。 例えば次の様に2乗を計算する関数 sqr() を作る。

    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;
    
    実行の際には関数を呼ぶ手間がなくなるのである。 inline関数内で定義した変数はやはり関数内のみで有効であり、 呼び出す所に展開されても、そこの変数と衝突しないように 適切に取り計られる。

  • 次へ
  • 目次
    Copyright(C) by Naoki Watanabe. Oct 21st, 1995.
    渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp