2021 年度「計算数学a」 2021-10-22

§4.1 関数の自作

(4.1.1) C 言語では、関数を自分で作り、元からある関数と同じように使える。 K&R2 §1.7, 1.8。

(4.1.2) ※ ここは、少し難しい。 その難しさは、初めて学ぶ西洋語で 「次の二つの文を、関係代名詞を使って一つの文にしてみましょう」 という練習問題に出会ったときに似ていると思う。 与えられた二つの文はいままで習ってきた知識で容易にわかるのに、 どうやって一つの文にしてよいかわからなかった、 という経験はないだろうか。 C 言語では、逆に、 全体を一つの main() にしていたときはわかっていたのに、 この部分を自作関数にして二つに分けよ、と言われると、とまどう人が多い。 関係代名詞を学んだときと同様に、ここは、慣れて乗り切るしかない。

(4.1.3) 次のプログラムは、xy 乗と yx 乗を計算して出力する。 いままでの知識で容易に理解できると思う。 p は答えがはいる変数、 i はループを回った回数を覚えておく変数である。 1 に xy 回かけることで、 xy 乗を求めている。 このやりかたでは、0 の 0 乗は 1 になる。 (C 言語にはべき乗を求める演算子は存在しない。 「^」も「**」も、別の意味である。)

(4.1.4)

#include <stdio.h>

int main() {
  int x, y, i, p;

  x = 3; y = 5;       /* このように一行に書いてもよい */

  p = 1;
  for (i = 0; i < y; i++) {
    p = p * x;
  }
  printf("%d の %d 乗は %d です.\n", x, y, p);

  p = 1;
  for (i = 0; i < x; i++) {
    p = p * y;
  }
  printf("%d の %d 乗は %d です.\n", y, x, p);
}

(4.1.5) 上のプログラムの、 べき乗を計算する部分を自作関数として独立させたのが次である。

#include <stdio.h>

int power(int base, int n);     /* プロトタイプ宣言 */

int main() {
  int x, y;

  x = 3; y = 5;       /* このように一行に書いてもよい */

  printf("%d の %d 乗は %d です.\n", x, y, power(x, y));
  printf("%d の %d 乗は %d です.\n", y, x, power(y, x));
}

int power(int base, int n) {    /* 以下、自作関数 power() の定義 */
  int i, p;

  p = 1;
  for (i = 0; i < n; i++) {
    p = p * base;
  }
  return p;
}

n 回ループを回すのに for (i = 1; i <= n; i++) ではなく for (i = 0; i < n; i++) とするのは C 言語の慣習だと思う。 文字数が少なくて済む、という利点もある。(まねしなくてもよい。)

(4.1.6) main() の終わりまでは、 「プロトタイプ宣言」とコメントをつけた行を除けば、 いままでの知識で理解できる。 ただし、power(x, y)xy 乗を計算する関数であると思って読むこと。

(4.1.7) 「以下、自作関数 power() の定義」以下を、順に説明してゆく。

(4.1.8) 多くの関数は、cos(0) が 1 であるように、値をもっている。 その値のことを「返り値(かえりち)」と言う。 「int power」の int は、 power() の返り値が int 型であることを示している。 (数学のことばづかいを借りて言えば「整数に値をとる関数」ということ。)

(4.1.9) 関数は power(x, y) のようにして使うが、 この xy を「引数(ひきすう)」と言う。 ここでは x, yint 型と決めたので 「関数 power()int 型の引数を二つとる」などと言う。 x, y でもいいのだが、 引数の意味がわかりやすいように、 最初の引数を base(「底」の意味)、 あとの引数を n と呼ぶことにした。 「int power」 のあとの小カッコの中には引数の型と名前を、 間をカンマで区切って書く。 この例では第一引数は int 型で名前は base, 第二引数も int 型で名前は n, である。

(4.1.10) その次の行の「int i, p;」は、 関数 power() の中だけで使う変数の宣言である。 いままで main() の中で宣言していたのと全く同じスタイルである。

(4.1.11) p = 1; とその次の for ループは(本質的には) 前と全く同じである。 basen 乗を計算し、 最後の「return p;」でその値を返す。 return のうしろに書いた式の値がこの関数の値になる、 という約束である。

(4.1.12) 上へ戻って、「プロトタイプ宣言」の行。 ここには、power() の定義の一行目と同じものを書く。 ただし、最後の中カッコ開き「{」を取り除き、 代わりにセミコロン「;」をつける。 これはコンパイラに 「これから出てくる power() はこういう関数 (int 型の引数を二つとって int 型の値を返す関数) です」と教える働きを持つ。

(4.1.13) 「power(x, y)」などとしてその値を計算させることを 「関数 power() を呼び出す」とも言う。

(4.1.14) プログラムは main() 関数 ――main() も実は関数なのだった―― の最初から実行されるが、 関数を呼び出すところにくると main() の実行はいったん止まり、 呼び出された関数がその最初から実行される。 その関数から return で戻れば、 呼び出したところの次から、main() の続きが実行される。 自作関数から別の自作関数を呼び出す場合も同様。

(4.1.15) つまり、まず、 x が 3, y が 5 で power(x, y) を呼び出す。 呼び出された power() 側では base が 3, n が 5 で実行が始まる。 次に、 x, y の値はそのままで power(y, x) を呼び出す。 呼び出された power() 側では base が 5, n が 3 で実行が始まる。

(4.1.16) さらに細かく説明しよう。。

#include <stdio.h>      /* これは説明のためのものです。コンパイルできません */

int main() {
    ...                              3  5        3  5
  printf("%d の %d 乗は %d です.\n", x, y, power(x, y));
    ...                                    ~~~~~~~~~~~
}                                              | ↑
                 +−−−ここへとぶ−−−−−−+ | 
                 ↓                               +−243 が出力される−+
               3        5                                               |
int power(int base, int n) {    /* 以下、自作関数 power() の定義 */     |
  int i, p;                                                             |
                                                                        |
  p = 1;                                                                |
  for (i = 0; i < n; i++) {                                             |
    p = p * base;                                                       |
  }                                                                     |
  return p;                                                             |
}       243 が返る−−−−−−−−−−−−−−−−−−−−−−−−−−−+

x が 3, y が 5 で main() 内の power(x,y) にさしかかったとする。 すると、power() へとび、 base が 3, n が 5 で動き始める。 power() の終わりにある return p; にさしかかったとき、 p の値は 243 である。よって、 main() 内の power(x, y) が 243 に置き換わって出力される。

(4.1.17) main() の中で定義した変数と関数 power() の中で定義した変数は、 名前が同じであっても、全く無関係である。 たとえば、仮に main() の中に i という名前の変数があったとして、関数 power() の中で i の値が変わっても、 main() 側の i は変わらない。

次のプログラムで、「1」とコメントをつけた行の i と、 「3」とコメントをつけた行の i とは無関係である。 「1」とコメントをつけた行の n と、 「2」とコメントをつけた行の n とも無関係である。

#include <stdio.h>

int power(int base, int n);

int main() {
  int i, n;                     /* 1 */

  n = 10;
  for (i = 0; i < n; i++) {
    printf("2 の %d 乗は %d です.\n", i, power(2, i));
  }
}

int power(int base, int n) {    /* 2 */
  int i, p;                     /* 3 */

  p = 1;
  for (i = 0; i < n; i++) {
    p = p * base;
  }
  return p;
}

(4.1.18) main() の中で使う変数は main() の中カッコの中で定義する。 その他の自作関数の中で使う変数はその中カッコの中で定義する。 これらは名前が同じであっても別物である。

(4.1.19) ※ return は、必要なら一つの関数の中に複数個おいてもよい。

  if (...) {
    return p;
  } else {
    return -p;
  }

また、return f(x)*y; とか、return 0; などと書いてもよい。

(4.1.20) ※ 二乗や三乗の場合は、関数を呼び出すより x*x, x*x*x のほうが速い。 また、(2.8.2) に書きもらしたが、C 言語には xy を返す double pow(double x, double y) という関数がある。

(4.1.21) 自作関数の書き方を、もう一度、まとめてみよう。 上の例では、power(x, y) と呼び出されたときの x, y の値を、 自作関数の側では base, n として受け取る。 これら base, n と、ほかにその自作関数の中で宣言した変数 (上の例では ip)を使って計算し、 答えを return すればよい。 その際、呼び出されるときに power(x, y), power(y, x) であることは一切考えなくてよい。

(4.1.22) なお、C 言語では、関数が受け取った base, n の値は、 関数の中で必要なら変えてしまっても構わない、という規則になっている。 (これはたぶんこの授業では使わない。)

(4.1.23) 次は、不適切なプログラムである。gcc ではエラーになる。

int power(int base, int n) {    /* 以下、関数 power() の定義 */
  int i, n, p;
  ...
}

受け取った n の値を、 この関数内で宣言した n が隠してしまう。 たとえば power(3, 5) として呼び出されても、 この関数の中の n は 5 とは無関係であるため、 受け取った 5 が使えない。

(4.1.24) 正しく作られた関数は、使いまわしがきく。 すなわち、そこだけコピーして、ほかのプログラムで利用可能である。

§4.2 練習問題

(4.2.1) 階乗を計算する関数 int factorial(int n) を書け。 main() では 0 から適当な自然数までの階乗を計算させてみればよいだろう。

※ 単に「関数 factorial()」でなく 「関数 int factorial(int n)」と呼ぶことで、 引数の数や型、返り値の型をも伝えることができる。

※ 「関数を作る」というのは、 「テキストファイルとして作成して終わり」ではなく、 テストのための main() も書き、コンパイルして実行し、 思った通りに動くことを確認することである。

前に (2.1.11) で書いたプログラムが参考になる。

次に、double factorial(int n) に改造せよ。 返り値を double 型に変えるだけだが、 n! を正しく計算できる n の範囲が、int 型のときよりも広くなる。

int main()int はそのままである。 double main() としてはいけない。

(4.2.2) double 型変数 x と自然数 n に対し、 xn 乗を返す関数 double power(double x, int n) を書け。 (4.1.5) を改変すればよい。

§4.3 発展問題

これらは、上の練習問題よりは難しい。時間に余裕のある人向けである。

(4.3.1) 0 以上の整数 n および 0 以上 n 以下の整数 r に対し、 n 個のものから r 個のものを取り出す方法の総数 nCr の値を返す関数 int comb(int n, int r) を書け。 テスト用の main() は各自で工夫せよ。 下のように、パスカルの三角形を書くのもよかろう。

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1

階乗を返す関数を使ってもよいが、 すぐに桁あふれしてしまうのが欠点である。 15C3 = 15C12 = 15 * 14 * 13 / (3 * 2 * 1) を利用してみたらどうか?

(4.3.2) 整数 x, y に対し、 それらの最大公約数を返す関数 int gcd(int x, int y) を書け。 x, y の動く範囲 ―― 数学ならば定義域 ―― は各自で考えて決めよ。 また、最小公倍数を返す関数 int lcm(int x, int y) を書け。

(4.3.3) 自然数 x に対し、 その最小の素因数を返す関数 int prim(int x) を書け。 その関数を利用して、素因数分解をおこなうプログラムを書け。 すなわち、たとえば 24 を入力すると 「24 = 2 * 2 * 2 * 3」と出力するプログラムを書け。 ((2.1.10) 参照。なお、 このプログラムは、自作関数を使わなくても書ける。)

(4.3.4) C 言語にもともとある数学関数、sin()exp() を、自分で書いてみよ。 関数名は mysin(), myexp() などとするともともとある関数と重複しない(と思う)。 正しく書けたかどうかは、もともとある sin() などと比べてみればよい。 マクローリン展開はどこで打ち切るか?


岩瀬順一