C言語ワークショップ 第5回  関数

●はじめに

 ここまでのところでは、C言語の細かい部分からだんだん範囲を広げて説明してきました。次は実際にプログラムを記述するために関数を学びます。
 さて、”関数”という言葉は数学で出てきますが、その言葉通りCでもその関数です。
 Cにおいて関数はプログラム自身も関数であり、様々な処理や演算をまとめたものが関数といえるでしょう。
 ベーシックやアセンブラでは「命令」というのがあり、それは言語の中で特別な扱いをうけます。しかしCでは命令がなく、関数のみでプログラムが構成され、またどの関数も特別な扱いがなく同じレベルで扱われるものです。

●キーワード
関数英語ではfunction(ファンクション)
よく説明で「ブラックボックス」という言葉がよく使われる。
引数関数に与えてる数値(関数が得る数値)
返り値関数が返してくる数値(関数が送る数値)

●関数とは

 数学にも出てくる関数。この考え方は同じです。

  y=2x

 これは、普通の関数ですが。これと何のかわりもありません。
 xは与える数、そしてyにその結果が出ます。
 Cの関数も値を与えれば、結果が返るわけです。
 そして、結果がどんな仮定で得られるかが関数の中身になるわけです。
 ここで、重要なのは値を与えることが出来、そして結果が返るってところだと思います。

 さて、アセンブリ言語においてサブルーチンコールというものがありました。
 C言語での関数は、このサブルーチンコール再現しているものでもあります。
 もう一度アセンブリ言語でみたサブルーチンコールの表を見てみましょう。  
サブルーチンコール
番地プログラムオペレーション
0000h
処理A 
0010hCALL 0023h(SP)←PC(0013h)
PC←0033h
0013h
処理B 
0020hCALL 0023h(SP)←PC(0023h)
PC←0033h
0023h
処理C 
0030hJP 0000h処理A
0033h
処理X 
0040hRETPC←(SP)

 この表では処理A、処理B、処理Cが一つの集まり(ルーチン)であり、処理Xがサブルーチンとなっています。  これをブロック化すれば

  {
    処理A
      CALL 処理X
    処理B
      CALL 処理X
    処理C
  }

  {
    処理X
    RET
  }

 のように見ることができます。このブロックで囲まれた部分がアセンブリ言語での処理に対応しており、このブロックをまとめて関数とみなすわけです。そしてCALLの部分が実際に関数の呼び出しをおこなう部分に対応していると考えることができます。
 ここでの処理Xの部分などのブロックの中身を定義することで、処理の内容を決めるのが次に出てくる、関数の定義となります。

●関数の定義

 基本構造:返り値の型 function-name(パラメータ){ }

 たとえば

  int funcname(char c){ }

 のような形になります。

 この場合この関数は、戻り値の型をint型で定義しているため、int型の値を結果として返します。そしてパラメータをchar型で定義していますから、この関数の呼び出しにはchar型の値を必要とし、そのパラメータは変数cへ入れられることになります。

  int funcname(c)
  char c;
  {

  }

 と変えて書いてみます。実は元々C言語を設計した人はこのような書式で定めていましたが、現在のANSI規格になったCでは前者の方で定義されています。
 旧式の書き方の方がピンとくるかもしれません。
 この定義ではcをパラメータ(引数)をして定義されそのcはint型変数だと宣言してます。
 この変数cは{}外で宣言されていますが、変数自体は{}内でするのと変わりありませんが、宣言した変数へ値を受け取るようにする場合の宣言の書き方です。
 ですから外で宣言されても、その後、関数の中では、中で宣言した変数と同様の機能をもちます。ただ、はじめに値を受け取るバラメータとしての動作が行われるところだけ違うのです。

 そして定義された関数を実際に呼び出す場合は

  ret=funcname(10);

 のような形になります。

 retには関数の結果が入ります。上の関数定義でint型で関数の返り値を宣言しているので入る値当然intです。

 さて呼び出される際にどのようなイメージを持てばいいかを考えてみます。
 まず、

  funcname(10);

 の関数の呼び出しがあると、まずその関数へ飛ぶ前に、10という値を送る必要があります。
 この値はまずスタックへ積まれます、そして、関数を呼び出す前に、呼びだしたアドレスへの戻り値をスタックに積みます。このことから、関数の呼び出しは、アセンブリ言語におけるサブルーチンコールと同様の動きをする事がわかります。  そして、呼び出された関数は送られてくる(スタックへ積まれた)値を得る必要があります。
 関数の定義で

  int funcname(c)
  char c;
  {

  }

 となっているため10という値はスタックから変数cへ入れられるわけです。

 この場合だと、呼び出しがわでは

  funcname(10);

 となっているため、返り値を必要としていませんので値は返りません。もし戻り値が存在したなら、関数は戻り値をスタックへ積み上げてRET命令で元の処理へ帰るという動作を行い戻り値の受け渡しを行います。

●main関数と実際の動作

 C言語において、唯一の特別な関数が存在します。それはmain関数と呼ばれるものです。この関数はプログラムが実行される場合に、はじめに呼び出される関数(OSが初めに呼び出す関数)です。エントリーポイントとも呼ばれ、要するにプログラムはmain関数から始まるということになります。
 このmain関数もさきほどの関数の定義の説明通りに定義されます。

  int main() {
  }

 となります。これはCにおける実際に動作する最小サイズのプログラムとなりmain関数の中身(処理内容)が定義されていませんので、なにも起こりませんが、このmainがあって初めて立派に動作するプログラムとなります。
 さて実際にプログラムを作成する場合、次のような形になります。

  int main() {
    int a;     a=func1(10);
  }
  int func1(char c)
  {
    c=c*2;
    return c;
  }

 この場合、アセンブリ言語的にみれば、main関数の中身がメインルーチン、そしてfunc1関数の中がサブルーチンと見ることができます。
 このように、関数の中で別の関数を呼び出し(サブルーチンコール)ています。呼び出す側では必要なパラメータと、返り値の型、内容さえわかれば、呼び出す関数の中身を考慮しなくても自由に好きな場所で利用できることが分かると思います。

●プロトタイプ宣言(関数の宣言)

 変数などでは

  int c;

 というように変数の宣言が行われ、これを行わなければ変数は使えません。

 実は関数にもこれと同じように関数の宣言があり、それをプロトタイプ宣言と呼ばれています。
 その形式は

  int funcname(char c);

 のように{ }のない形で宣言されます。
 実は、関数も変数同様、宣言がされないと、使うことができません。
 ただし、関数の呼び出しがあるソースファイルに関数の定義もされている場合は、関数の定義で、プロトタイプ宣言をしていることになるため無くても問題ありません。
 もちろん、関数定義のあるファイルにプロトタイプ宣言を書いても問題ありません。
 しかし、複数のソースファイルで別のファイルの関数を使う場合などは必ずプロトタイプ宣言が必要になってきます。

 そのもっとも簡単な例が

  #include<stdio.h>

 です。
 このヘッダファイルの読み込みをしないで、printf関数など使ったソースをコンパイルしたら「関数が定義されていない」などのエラーになります。
 このヘッダファイルの中を見てみると

  int printf(char *format, ...);

 というのがあると思います。

 関数を呼び出しているソースファイルで関数自体が定義されていないのにprintf関数を呼び出しても正常にコンパイルされるのはヘッダファイルでプロトタイプ宣言がされているためだからなのです。

●ライブラリ関数

 ライブラリ関数はCコンパイラに標準で用意されている関数であり、printf,やscanfなどがそれにあたり、数多くのものが用意されています。
 もちろん先ほどの話通り、関数の定義を行わないで利用できますから、必要なヘッダファイルを読み込めばそこに関数のプロトタイプ宣言がされているため。自由に利用できるわけです。

●ユーザ関数

 ユーザ関数は関数の定義でお話しした通りの形で次のようにユーザ任意の関数が作れます。ライブラリ関数もユーザ関数も同じ関数ですけど、特別、このように呼んで分類しています。極端にいえば、別にライブラリ関数一切使用しなくてもプログラムは作れますし、ライブラリ関数と同じ処理の関数をユーザが定義してもよいわけで、関数の扱いとしては全く同じであるわけです。あくまで、コンパイラにあらかじめ準備してあり、利用者が簡単に使えるようになっているわけです。

  main ()
  {
   char a,b;
   a=10;
   b=func(a);
   printf("%d",b);
  }

  char func(char a)
  {
   a++;
   return(a);
  }

 この場合

  char func(char a)
  {
   a++;
   return(a);
  }

 の部分で関数funcが定義されたことになります。
 そしてmain ()関数では

   b=func(a);

 でその関数が呼ばれることになり
 その呼び出しを行っているmain()関数は

  main ()
  {
   char a,b;
   a=10;
   b=func(a);
   printf("%d",b);
  }

 で定義されていることになります。

 また、つぎように関数の定義は絶対に関数の中で行われることはありません。

  main ()
  {
   char a,b;
   a=10;

   char func(char a)
   {
     a++;
   }
   printf("%d",a);
  }

 とかいったようなものは、コンパイラにとっては理解できないことになります。

 ですから、たとえば

  char func1(char a)
  {
   func2();
  }

  char func2(char b)
  {
   func3();
  }
  char func3(char c)
  {
   a=1;
  }

 の場合をみると、

  char func1(char a);
  char func2(char a);
  char func3(char a);

 の3つの関数が定義されたということになり、またfunc1()はその定義でfunc2()を呼び出し、そのfunc2の定義ではfunc3()を呼び出していると考えるわけです。

●ライブラリ関数の利用とプロトタイプ