関数

    2012/05/26

    本章では下記の内容を学習します。

    • 処理をまとめる: 関数の作成
    • 処理内容を指示する: 引数
    • 処理結果を受け取る: 戻り値
    • 関数一覧を作る: 関数プロトタイプ宣言

    関数の作成

    関数の作成

    例1-関数の作成
    #include <stdio.h>
    
    // Hello!と挨拶してくれる関数
    void    sayHello( void )
    {
      printf( "Hello!\n" );
      return;
    }
    
    int     main()
    {
      sayHello();
      sayHello();
      return  0;
    }
    

    実践課題などを作成してみて、だんだんとプログラムを書く内容が増えてきたな、と感じ始めてきていることと思います。規模が大きくなればなるほど、処理内容もどんどん増えていくわけですが、そうなると main 関数のなかに全部の処理を書くということは難しくなってきます。そこで各プログラミング言語では、煩雑化していく処理内容を、すっきりと分割していくための機構を用意しています。それが関数です。

    関数は、一連の処理を一つのかたまりとして記述することができる処理単位です。たとえばスロットマシンの課題では『メダルをベットする』、『抽せんする』、『当選役を決定する』、などの処理単位があったと思います。これらの処理単位を関数として切り分けておくと、プログラム全体の見通しがよりすっきりとしていきます。

    皆さんが今まで利用してきた、printf scanf_s rand なども全て関数です。これらの関数は、C 言語があらかじめ備えている関数になります。これに加えて、皆さんが自作の関数を付け加えていくことができるというのが、今回の学習の肝になります。

    関数の 3 要素

    // Hello!と挨拶してくれる関数
    void    sayHello( void )
    {
      printf( "Hello!\n" );
      return;
    }
    

    例では単純に、Hello と呼びかけてくれる関数を作成しています。関数を作成する際には、以下の要素を含むようにします:

    • 戻り値: 関数の仕事結果を受け取る。結果は 1 つの値として返ってきます
    • 関数名: 関数の名前。変数名と同じように、最初の文字に数値は使えません
    • 引数: 関数へ仕事内容を指示する。1 つ以上の値を渡すことができます

    例の関数 sayHello では、void sayHello( void ) という定義がなされており、左の void が戻り値、真ん中の sayHello が関数名、右のカッコが引数になります。今回の例では、戻り値なし、引数なし、という単純な関数を作っており、『なし』を表す void が指定されています。

    関数の呼び出し

    int     main()
    {
      sayHello();
      sayHello();
      return  0;
    }
    

    作成した関数を呼び出すときは、関数名(); で記述します。例では引数がないので ( ) の中身がなく、『sayHello();』と簡単に記述しています。関数は呼び出されると、作成した処理の方へ自動的に流れが移動し、上から順に実行します。処理が完了したら、呼ばれたところまで戻って、また実行を再開します。これを繰り返して、プログラムは形作られていくのです。

    なお、関数を作成しても、それを呼び出されなければ、その関数は処理をしません。

    関数の終了

    関数は、return 文に到達した時点で終了します。もし return 文が関数の途中にあったとしても、そこで処理は強制終了です。これは while 文における break 文に良く似ています。関数は終了すると、呼び出されたところに戻って、再び呼び出し元の関数から処理を再開します。

    練習問題: 小さい方を表示
    1. 関数の中で 2 つの数値を入力してもらい、小さい方を表示する関数、void showMin(void) を作成してください
    2. 同じ方式で、大きい方を表示する関数 showMax を作成してください
    3. 2 つの関数を main 関数から呼び出して、挙動を確認してください

    引数

    例2-引数の利用(m
    #include <stdio.h>
    
    // m から n までの合計を表示する関数
    void showSum( int m, int n )
    {
      int sum = 0; // 合計値を覚える変数
    
      // mから始まり、nまで続けるfor文
      for( int i=m; i<=n; i++ )
      {
          sum += i;           // 足し続ける
      }
    
      printf( "%dから%dまでの整数値の合計: %d\n", m, n, sum );
      return;
    }
    
    int main()
    {
      showSum( 1, 10 );
      return  0;
    }
    

    先述の例では、関数は呼ばれるたびに同じ動作をしていました。しかしこれでは状況に合わせて柔軟な対応をすることは難しいといえます。関数が真の実力を発揮するのは、頼まれた内容に応じた仕事をするという状況です。これを実現するのが、引数になります。

    引数は、関数が行う仕事の内容を指示するために利用するものです。例では m から n までの合計値を計算し表示する関数 showSum を定義していますが、先ほどと違い、 ( ) 内に 2 つの変数が設定されています。

    関数の引数は、 ( ) 内に好きなだけ配置することができます。配置するには、変数の型名+変数名というセットで定義してやり、複数の引数をセットしたい場合は、それをカンマで区切ってください。例では m から n までの合計値を求めたいので、m と n という 2 つの整数型変数を定義しています。

    呼び出し側は、関数を呼び出す際に、対応する値をセットしてやります。例では main 関数で、showSum 関数に 1 と 10 を設定しています。この値が、関数の処理に入る際に、自動的に引数 m と n に代入されて実行されます。

    関数の内部では、 ( ) 内で定義した変数は、すでに宣言済みであるとしてそのまま利用できます。例では showSum 関数に処理が移行した際に、int m; と int n; が最初に宣言されている扱いになり、さらに引数で与えられた 1 と 10 が自動的に代入されているという状況になります。これで showSum 関数側は、受け取った引数をもとに、すぐさま仕事に移ることができるのです。

    練習問題: 数学関数の作成
    1. m の n 乗を表示する関数、void power( int m, int n ) を作成してください。たとえば main 関数で power( 2, 3 ); と呼ばれたら、『2 の 3 乗は 8 です』と表示します
    2. 階乗を計算して表示する関数、void frac( int n ) を作成してください。(階乗とは、1 から N までを掛け算したものです。4! = 1×2×3×4 = 24)たとえば main 関数で frac( 4 ); と呼ばれたら、『4 の階乗は 24 です』と表示します

    戻り値

    戻り値の利用

    例3-戻り値の利用(抽せん結果によって払出枚数を決定する関数)
    #include <stdio.h>
    
    // 抽せん結果によって払出枚数を決定する関数
    int getHaraidashi( int chuusen )
    {
      switch( chuusen )
      {
        case 7:
          return 15;
        case 8:
          return 8;
        case 9:
          return 1;
        case 0:
        case 1:
          return 3;
        default:
          return 0;
      }
    }
    
    int main()
    {
      int haraidashi;
    
      // 抽せん7は15枚払出
      haraidashi  = getHaraidashi( 7 );
      printf( "%d枚の払出\n", haraidashi );
    
      // 抽せん9は1枚役
      haraidashi  = getHaraidashi( 9 );
      printf( "%d枚の払出\n", haraidashi );
    
      // 抽せん3はハズレ
      // 関数の戻り値がint(整数型)なので、
      // このようにprintf関数へそのまま渡してもOK
      printf( "%d枚の払出\n", getHaraidashi( 3 ) );
    
      return  0;
    }
    

    引数によって、関数に仕事内容の指示ができるようになりました。関数内で仕事が完結するような場合は、これで十分対応できるようになりましたが、関数が仕事をした結果を、呼び出した側が受け取りたい場合は、どのようにすればよいでしょうか? それを解決してくれるのが、戻り値です。

    戻り値は、関数が行った仕事の結果を、値として受け取ることができる機構です。例では抽せん結果に応じて、メダルの払出枚数を決定する関数を作成しています。抽せん結果を引数として渡し、その値に応じて払出枚数を戻り値として受け取ることができています。

    戻り値には任意の変数型を指定することができます。例では払出枚数なので整数型である int 型を指定しています。このほかに浮動小数点型や、文字型などを指定することもできます。

    戻り値として受け取ることができるのは、1 つの値のみです。引数には任意の数を指定することができましたが、戻り値は 1 つだけしか受け取れないので注意が必要です。ただし、戻り値という形以外で、作業結果を受け取る機構も C 言語には用意されています(参照渡し、構造体など)ので、それについては後日紹介したいと思います。

    戻り値の返し方

    戻り値は return 文を利用して返します。例では switch 文内にて、それぞれの払出枚数を return の直後に記述しています。関数は return 文に到達した時点で強制終了しますから、switch 文の break などは省略されています。少しテクニカルな記述方法です。

    戻り値の直接利用

    戻り値はそのまま値として利用することができます。例の最後の printf 関数を見てみるとわかります。この printf 関数では、%d に対応する整数値のところに、関数が直接記載されています。これは関数が実行されて、その戻り値が値として返ってきて、その値を%d にあてはめて利用する、という一連の流れが、わずか 1 行で実行されています。ぎゅっと凝縮された 1 行なのですね。

    このように、関数の戻り値を、別の関数に引き渡すという使い方は多くなされますので、もし関数呼び出しの中で関数が呼ばれていたら、それは戻り値をアテにしてプログラミングされているものだと理解してあげてください。

    練習問題: 1リールスロット課題の移植
    1. srand 関数による乱数の初期化と、rand 関数の空回しを行ってくれる関数 void initChuusen( void ) を作成してください
    2. 乱数を利用して 0 ~ 9 の抽せんを行い、その結果を返す関数 int getChuusen( void ) を作成してください
    3. (1)と(2)と例題を利用して、乱数による抽せんを行い、払出枚数を決定する、という処理を 1 回だけ行ってくれるプログラムを、main 関数に書いてください

    関数プロトタイプ宣言

    例4-関数プロトタイプ宣言
    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    // 関数プロトタイプ宣言
    int  getHaraidashi( int chuusen );
    void initChuusen( void );
    int  getChuusen( void );
    
    int main()
    {
      initChuusen();
    
      int     chuusen = getChuusen();
      printf( "抽せん結果=%d, 払出枚数=%d\n", chuusen, getHaraidashi( chuusen ) );
    
      return  0;
    }
    
    // 以下、3 つの関数の具体的な実装が続く
    

    関数が多くなってくると、このプログラムにはいったいどんな関数があるのか、というのがざっと見ではわからなくなってきます。こういったときに、関数のリストがあると便利ですね。

    関数プロトタイプ宣言は、関数のリストをファイルの先頭に記述することで、そのプログラムではどのような関数が利用されるかを表明するための仕組みです。この宣言は、ただ単に見通しが良くなるという利点だけではなく、もっと重要な意味があります。

    関数プロトタイプ宣言を行うと、関数の実体がどこにあっても OK になります。記述の仕方ですが、関数の定義と同じ文を記述し、最後にセミコロンをつけて完成です。

    コラム: 実は前回までの例題はすべて…

    実は前回までの例題はすべて、main 関数の前に、自作関数が配置されていました。これには訳があります。試しに main 関数の後ろに自作関数を置いてみてください。エラーになると思います。

    main 関数の後ろに自作関数を置くと、main 関数がコンパイルされる段階では、まだ自作関数がどういったものかがわからないことになります。ここでコンパイラは、そんな関数知らない!とエラーを提示するのです。自作関数を main 関数の手前に置けば、先にコンパイラは自作関数をコンパイルしますから、その後の main 関数のコンパイルの段階で自作関数の正体を知っているので、うまくコンパイルしてくれるのです。

    では登場順に自作関数を並べれば済むのかと言われると、そうなのですが、自作関数を複数箇所で使い回すようになると、それも難しくなります。

    それよりも簡単に自作関数の存在をコンパイラに知らせることができる機能が、この関数プロトタイプ宣言なのです。関数プロトタイプ宣言は、関数がこういった仕様ですよとコンパイラに教えるための宣言ということになります。これがあれば、自作関数は main 関数の後ろだろうが前だろうが、好きな所に置くことが許されます。

    こういったメリットと、前述の『私はこんな関数を持っています』的なリスト機能があいまって、関数プロトタイプ宣言は、ほぼ 100%のプログラムで記述されています。皆さんもぜひ記述してください。

    実践問題: 1 リールスロットの関数化完成
    1. 上の例題を発展させて、関数プロトタイプ宣言を利用し、1 リールスロットを関数を使って完成させてください
    2. main 関数に残ったプログラムのうち、関数化できそうなものがあれば、やってみてください