関数

関数とは

 例えば、次のようなプログラムを考えてみましょう。

[main()だけのプログラム]

main()
{
  int x, y;

  x = 1;
  y = 0xf0;

  num0 = ....;        /* 変数「x」の値を表示する */
  num1 = ....;
  num2 = ....;
  num3 = ....;

  EZUSB_Delay(1000);  /* 1秒待つ */

  x = x << 4;

  num0 = ....;        /* 変数「x」の値を表示する */
  num1 = ....;
  num2 = ....;
  num3 = ....;

  EZUSB_Delay(1000);  /* 1秒待つ */

  y = x & y;

  num0 = ....;        /* 変数「y」の値を表示する */
  num1 = ....;
  num2 = ....;
  num3 = ....;

  EZUSB_Delay(1000);  /* 1秒待つ */

  return 0;
}
同じような処理が繰り返し書かれています。 これらを一つにまとめることができれば、 プログラムは短くなり、読みやすくなります。 また、手直ししようとしたときに、一ヶ所を書き換えれば済みます。

 このプログラムを関数を用いて書き換えると、 以下のようになります。

[ShowInt()関数を用いたプログラム]

/* 関数プロトタイプ */
void ShowInt(int d);  /* 引数「d」の値を表示する関数 */

/* 「main」関数 */
main()
{
  int x, y;

  x = 1;
  y = 0xf0;

  ShowInt(x);         /* 変数「x」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  x = x << 4;

  ShowInt(x);         /* 変数「x」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  y = x & y;

  ShowInt(y);         /* 変数「y」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  return 0;
}

/* 関数定義 */
void ShowInt(int d)   /* 引数「d」の値を表示する関数 */
{
  num0 = ....;        /* 引数「d」の値を表示する */
  num1 = ....;
  num2 = ....;
  num3 = ....;
}

 上記の「ShowInt()」のように、 何らかのまとまった処理をまとめて、名前を付けたものを「関数」と呼びます。 関数に記述された処理を実行する際には、 その関数を呼び出すことになります。 関数を呼び出すと、関数に記述された処理を実行します。 関数の実行が終わると、関数を呼び出した次の処理から プログラムの実行を継続します

 関数を呼び出す際に、データを引き渡すことができます。 このデータは「引数」と呼ばれ、括弧「()」の中に書きます。 引数によって、関数の動作を変えることができます。 上記の例では、表示する変数を変えています。 引数が無い関数もあります。

 関数は値を返すことができます。 この値を「返り値」や「戻り値」と呼びます。 「戻り値」のある関数としては、例えば、

  x = sin(a);   /* 三角関数 */
  y = sqrt(b);  /* 平方根 */
等の数学関数がわかりやすいかもしれません。 「戻り値」が無い関数もあります。 「ShowInt()」には戻り値がありません。

 なお、「main()」も「関数」です。 C言語では、「main()」という関数が最初に実行され、 この関数を終了したらプログラムの実行も終了するという約束になっています。

関数を使う利点

 main()だけのプログラムShowInt()関数を用いたプログラム を比べてみましょう。 関数ShowInt()を導入したことによって、

という効果があります。 「たいして変わらない」と思ったときは、 「num0 ...」の部分が何十行、あるいは、 何百行ある場合を想像してみると良いでしょう。 また、「コピーして貼り付けた方が楽かもしれない」と思ったときは、 「num0 ...」の部分に 変数「x」が何十回も現れる場合や、 表示させたい変数が何十個もある場合を想像すると良いでしょう。 全てを間違えずに書き換えできるでしょうか?

 プログラムが大きく複雑になったときに、 複数のソースファイルに分割することが広く行われています。 これには、

などの利点があります。

関数を定義する方法

 関数を使ったプログラムでは、 「void ShowInt(int d)」が二度登場することがわかります。 最初の「void ShowInt(int d);」 (「;」があることに注意!) を「 関数プロトタイプ」と呼び、 関数の名前、戻り値のデータ型、引数の数とデータ型を宣言します。 最後の「void ShowInt(int d) { ... }」が関数定義で、 関数の実際の動作を書きます。

 関数プロトタイプの書き方は、

戻り値の型 関数名(引数の並び);
となります。 「引数の並び」は、引数一個につき「引数の型 引数名」となり、 複数の引数を持つ場合は「,」で区切って並べます。 「引数名」は省略できます。 引数が無い場合は、引数の並びが「void」になります。 戻り値が無い場合は、戻り値の型が「void」になります。 関数プロトタイプの書き方は、
int  function1(int a);         /* 戻り値が int。引数は int が1個 */
int  function2(int);           /* 戻り値が int。引数は int が1個、引数名省略 */
int  function3(int a, int b);  /* 戻り値が int。引数は int が2個 */
int  function4(void);          /* 戻り値が int。引数は無い */

void function5(int a);         /* 戻り値は無い。引数は int が1個 */
void function6(void);          /* 戻り値も引数も無い */
のようになります。

 関数定義は、

戻り値の型 関数名(引数の並び)
{
  /* 関数の動作を書く */
}
のようになります。 「戻り値の型 関数名(引数の並び)」の部分は関数プロトタイプと似ていますが、 「引数名」は省略できません。 戻り値がある関数では、 「return」文で戻り値を指定します。
  return 戻り値;
return」文は複数あってもかまいません。 例えば、
int is_even(int d)  /* 偶数ならば「1」を返す関数 */
{
  if ((d % 2) == 0) return 1;
  else return 0;
}
のようになります。 戻り値が無い関数では、戻り値を書かずに
  return;
とします。 戻り値が無い関数では、 関数の最後に到達した場合にも関数の処理を終了します。

関数の呼び出し

 関数を呼び出すときは、「関数名(引数の並び)」を書きます。 戻り値の型や引数の型は書きません。 例えば、下記のようになります。

int  function1(int a);         /* 戻り値が int。引数は int が1個 */
int  function2(int);           /* 戻り値が int。引数は int が1個、引数名省略 */
int  function3(int a, int b);  /* 戻り値が int。引数は int が2個 */
int  function4(void);          /* 戻り値が int。引数は無い */

void function5(int a);         /* 戻り値は無い。引数は int が1個 */
void function6(void);          /* 戻り値も引数も無い */

main()
{
  int x, y, z;

  x = 0;
  y = function1(x);            /* 戻り値が int。引数は int が1個 */
  z = function4();             /* 戻り値が int。引数は無い */

  function5(z);                /* 戻り値は無い。引数は int が1個 */
  function6();                 /* 戻り値も引数も無い */
}

引数が無い関数であっても、「()」が必要であることに注意してください。 なお、「()」を付けずに
int  function4(void);          /* 戻り値が int。引数は無い */

main()
{
  int x;

  x = function4;
}
のように書いてもエラーにならない場合があります。 このとき、変数「x」には 関数「function4」へのポインタ (アドレス) が代入されます。 関数「function4()」は呼び出されず、 変数「x」の値は常に定数となります。

関数プロトタイプが必要な理由

 関数プロトタイプと関数定義の両方を書く理由の一つは、 コンパイラの都合です。 ソースプログラムを先頭から終わりまで一回読んだだけで済ませようとすると、 「main()」関数で「ShowInt()」関数を参照する前に 「void ShowInt(int d);」であることを知っておく必要があります。

 それならば、「void ShowInt(int d)」の実体を先に書いて、 以下のようにすれば良いではないか、と思うかもしれません。

/* 引数「d」の値を表示する関数 */
void ShowInt(int d)
{
  num0 = ....;        /* 引数「d」の値を表示する */
  num1 = ....;
  num2 = ....;
  num3 = ....;
}

/* 「main」関数 */
main()
{
  int x, y;

  x = 1;
  y = 0xf0;

  ShowInt(x);         /* 変数「x」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  x = x << 4;

  ShowInt(x);         /* 変数「x」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  y = x & y;

  ShowInt(y);         /* 変数「y」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  return 0;
}
これは、この例に関しては正しいです。 全ての関数定義を、参照する前に定義できるように並べ替えることが できれば、 関数プロトタイプを用いないプログラムができます。

 では、次の例はどうでしょう?

/* 引数「n」が偶数ならば「1」、そうでなければ「0」を返す関数 */
int is_even(unsigned int n)
{
    if (n == 0)
        return 1;
    else
        return is_odd(n - 1);
}

/* 引数「n」が奇数ならば「1」、そうでなければ「0」を返す関数 */
int is_odd(unsigned int n)
{
    if (n == 0)
        return 0;
    else
        return is_even(n - 1);
}
これは、 相互再帰と呼ばれるもので、 2個以上の関数がお互いを使って定義されているものです。 「is_even」が「is_odd」を使用し、 「is_odd」が「is_even」を使用するので、 どちらの定義を先に書いても、未定義の関数を参照することになります。 したがって、関数プロトタイプが必須となるプログラムも存在します。

プログラムのモジュール化と分割コンパイル

 関数プロトタイプが役立つ重要な場面は、 プログラムのモジュール化です。

 最初のプログラムを分割してみましょう。 以下の3個に分割します。

[showint.h]

/* 関数プロトタイプ */
void ShowInt(int d);  /* 引数「d」の値を表示する関数 */
[main.c]
#include "showint.h"

/* 「main」関数 */
main()
{
  int x, y;

  x = 1;
  y = 0xf0;

  ShowInt(x);         /* 変数「x」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  x = x << 4;

  ShowInt(x);         /* 変数「x」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  y = x & y;

  ShowInt(y);         /* 変数「y」の値を表示する */

  EZUSB_Delay(1000);  /* 1秒待つ */

  return 0;
}
[showint.c]
#include "showint.h"

/* 関数定義 */
void ShowInt(int d)   /* 引数「d」の値を表示する関数 */
{
  num0 = ....;        /* 引数「d」の値を表示する */
  num1 = ....;
  num2 = ....;
  num3 = ....;
}

 プログラムを複数のソースファイルに分割する際には、 定数などのマクロ、構造体などのデータ型、 大域変数、関数プロトタイプなどの宣言を 「ヘッダ」と呼ばれるファイルにまとめることがよく行われます。 通常は拡張子「.h」を付けます。 「ヘッダ」は「#include」で読み込みます。 こうすることにより、同じ宣言を何度も書く必要がなくなります。

 繰り返し利用できる機能を一つの独立したファイルにしておくと、 複数のプログラムを開発する時に、再利用が容易になります。 関数「main()」だけを新しく作り、 他のファイルはそのまま転用できます。