関数とは(2)、再帰呼び出し

[PDF版] [関連課題]

変数の有効範囲と有効期限

関数の中で宣言されている変数を局所変数(local variable, 自動変数: automatic variableともいう)といいます。パラメータとして宣言されている 変数も局所変数です。はじめに引数の値がセットされる以外は局所変数と同じ です。局所変数は宣言されている関数の中でしか使うことができません。 これ に対して、関数の外で宣言された変数を大域変数(global variable)といいま す。このような変数は、関数の間で共通に使うデータのための変数を宣言する ために使います。関数のプロトタイプ宣言と同じように、大域変数は変数を使 う前に(できれば、プログラムの先頭で宣言しなくてはなりません。voidで宣 言した手続きとしての関数は、大域変数で宣言されたデータに対してなんらか の操作を行うために使います。このように大域変数の値を変更することを関数 の副作用(side effect)といいます。

局所変数は、その宣言された関数が終了すると値が無効になってし まいます(変数のメモリが解放される)。大域変数は関数が終わっても値は保 持されますので、もしも、関数が終わってもなにか値を保持する必要がある場 合には大域変数にいれておくことができます。また、関数から返すことのでき る値は1つなので、たくさんの値を返したい場合には、大域変数を用意してそ こにいれて、関数からも戻ったときにその変数を参照することでできます。関 数が終わっても値を保持したい場合には、静的変数を使うことができます。こ れは局所変数の宣言の先頭にstaticというキーワードをつけたものです。

変数を参照できる範囲のことを変数の参照範囲(scope)と いいます。局所変数の有効範囲は関数の中、大域変数の有効範囲は変数が宣言され以降のプログラムです。 変数の値が保持される期間を有効期間(extent)といいます。 局所変数の有効期間は関数の実行中、大域変数の有効期間はプ ログラム実行中です。静的変数とは参照範囲が関数内で、有効期間はプログラ ム実行中という変数です。

引数の値渡し

下のプログラムを考えてみましょう。

main() { 
   int i; 
   i = 10; 
   j = foo(i);
   printf("i=%d\n",i);
 }

int foo(int i)
{
    i = 100; 
    return i; 
}
fooの中で、iに100を代入して、iの値を変更していますが、mainの変数iは変 わりません。したがって、printfの結果は10となります。これは、mainのiの 値(つまり10)が、関数fooにわたり、関数のパラメータ変数iにセットされま す。パラメータ変数は関数の中では局所変数と同じに扱われるので、これを変 更しても、逆にmainの変数iはかわりません。このことを引数の値渡し (call by value) といいます。

なお、配列や文字列の場合は、呼び出し側の配列が変わってしまいます。これについてはポインターのところで説明します。

関数の再帰呼び出し

ある関数が、自分自身を呼び出すことを再帰呼び出し(recursive call)といいます。例えば、階乗を求める関数は次のように書くことがで きます。
int factorial(int n)
{
    if(n == 0) return 1;
   else return n*factorial(n-1);
}
つまり、n!は、(n-1)!にnをかけたものというわけです。但し、これをどこま でも続けても止まらなくなってしまうので、1のときには1!=1としていま す。階乗の定義は、1からnまで掛けたものといことあれば、このようなこと をしなくても、単にループで1からnまでかければいいのであまり意味がわか らないかもしれません。しかし、再帰呼び出しの考え方はもっと重要な意味を もっています。この考え方を身につけることによって、いろいろな問題が簡単 に解ける強力な考え方です。

では、有名なハノイの塔の問題を考えることにしましょう。ハノイの塔とは、 3本の柱があり、一番左の柱に下から大きな盤、その上に順に小さな盤が乗っ ているというものです。問題は

  1. 一度に1枚の盤しか移すことができない(片手しか使えない)
  2. 小さな盤の上に大きな盤を乗せてはいけない(壊れてしまう)
の条件で、左の柱から、右の柱に移すというパズルです。中間的な置き場とし て真ん中の柱を使うことができますが、上の条件は満たしていなくてはなりま せん。

この問題の解き方としてまず、大小2枚の場合を考えてみましょう。これだっ たら、まず小さい盤を真ん中に移して、大きな盤を左に移し、最後に小さい盤 を左に移せばよいことになります。では3枚ではどうでしょうか。この場合、 まず、2枚の手順を使って、上2枚を真ん中に移します。それで、一番下にあ る盤を右の柱に移し、また2枚の手順を使って、真ん中から右に移せばいいと いうことなります。

これを一般化して、n枚の場合には、

  1. n-1枚をあいているところに移す。
  2. 1番下の番を目的のところ(右の柱)に移す。
  3. n-1枚を目的のところに移す。
という手順でやればいいということになります。具体的なプログラムについて は講義の中で解説することにしますが、 n枚の盤を移すという関数を定義して、その関数に再帰呼び 出しを使うことで、エレガントにプログラミングすることができます。

もう、一つの問題を考えてみましょう。任意の整数を一文字の出力関数 (putchar)を使って、プリントアウトするという問題です。例えば、123という 数字の場合は”1”,”2”,”3”と出力するという問題です。一桁の場合であ れば、単にその数字を’0’に加えることによって、出力します。では2桁は どうでしょう。この場合はまず、10で割って、その商が2桁目なので、その数 字を印刷します。で、あまりを1桁にしてプリントすればいいわけです。これ を一般化してn桁の数とすると、数dに対し、

  1. まず、一桁、つまり10よりも小さければ、putchar(d+’0’)
  2. 最下位以外のn-1桁について、プリントする。つまり、d/10を印刷。
  3. 最下位の桁、d%10を印刷。
とすればいいわけです。
void print_number(int d)
{
  if(d >=10) print_number(d/10); /* 上の桁を出力 */
 putchar('0'+d%10); /* 最下位の桁を出力 */
}

再帰の考え方は、あるnについて問題を解くときに、n-1についての解答でとく ことができるときに使うことができます。プログラムを考えるときに、特定の ケース、特定の数についてプログラムを考えるだけでなく、それを一般的な入 力、一般的なnについて考えることはプログラミングについての非常に重要な 考え方です。