自作関数の考え方と設計について
自作関数の考え方について、階乗するだけの簡単なプログラムを通して紹介します。C言語(C89+gccの拡張機能)で書いてありますが、どの言語でも考え方は同じです。
プログラムをmain()
関数だけで書いてしまうと、冗長で可読性が低くなります。そこで、ある一連の処理をまとめた自作関数を作成して、コードを読みやすくします。そうすることで、エラー処理や改良がしやすくなります。
出力するだけなら簡単
階乗を標準出力するだけなら簡単です。ここでは、for
文でi
の値を累積代入演算子(*=
)を使って計算しています。
#include <stdio.h>
int main(void)
{
int n = 5;
int res = 1;
int i;
// 階乗の計算部
for (i = n; i > 0; i--) {
res *= i;
}
printf("%d! is %d\n", n, res);
return 0;
}
$ ./a.out
5! is 120
使い捨てのコードなら、これでも全く問題ありません。
処理を関数化する
それでは、関数を作ってみましょう。main()
関数から、階乗に関わる処理を分離するだけです。今回の場合なら、for
文とそこで扱う変数が対象になります。
階乗は大きな値を扱うので、関数の戻り値の型をlong
型にします。
#include <stdio.h>
#include <stdlib.h>
static long getFactorial(int value);
int main(void)
{
int n = 5;
printf("%d! is %ld\n", n, getFactorial(n));
return 0;
}
static long getFactorial(int value)
{
long res;
for (res = 1; value > 0; value--) {
res *= value;
}
return res;
}
関数名は、動詞+名詞にすることが一般的なので、get(取得)にfactorial(階乗)を足してgetFactorial
としました。
関数の前にstatic
指定子がついています。static
指定子がついた関数は、同じファイルからしか呼び出せないのでセキュアなコードになります。static
指定子を後からつけようとすると、その関数の影響範囲が分からないため、非常に大変な作業になります。逆に外す場合は、コンパイラがエラーとして教えてくれます。
さて、このプログラムですが、少し使いづらいと思います。というのも、階乗にかける値を変数n
に直値(リテラル)として代入しているからです。これだと、値を変える度にコンパイルし直す必要がでてきます。そこで、値を自分で指定できるようにしましょう。
値はコマンドライン引数で
値を直値ではなく、コマンドライン引数で受け取れるようにします。fgets()
といった標準入力よりも、便利な場合が多いです。後述しますが、大量の処理をスクリプトで簡単に行えます。
#include <stdio.h>
#include <stdlib.h>
static long getFactorial(int value);
int main(int argc, char *argv[])
{
int n = atoi(argv[1]);
printf("%d! is %ld\n", n, getFactorial(n));
return 0;
}
static long getFactorial(int value)
{
long res;
for (res = 1; value > 0; value--) {
res *= value;
}
return res;
}
$ ./a.out 5
5! is 120
コマンドライン引数なら、シェルのfor
文を使って連続して計算することができます。
$ for i in {1..5}; do ./a.out $i ;done
1! is 1
2! is 2
3! is 6
4! is 24
5! is 120
結果の値のみを出力すると、結果を別の処理に再利用できます。下掲のコードだと、3の階乗の結果をさらに階乗にかけて2で割っています。コマンドライン引数が標準入力よりも便利なことが分かったと思います。
$ echo $(./a.out $(./a.out 3)) / 2 | bc
360
ここまでで「動く」プログラムは完成しました。しかし、まだ油断はできません。というのも、入力によっては想定外の動作をするからです。それを防ぐためには、次項で紹介するエラー処理を施す必要があります。
終わりなきエラー処理
想定外の入力の対策をします。これをエラー処理といいます。エラー処理は果てしなく存在します。たとえば、main()
関数に渡される引数の数は適正か、その引数が数字かどうか、数字の場合は正の整数であるか、などを確認します。また、getFactorial()
関数であれば、0を受けとった場合(空積)の処理も必要になります。
ここで、2つの関数を追加します。1つは、プログラムの使い方を標準エラー出力する、usage()
関数です。他方は、コマンドライン引数が正の整数だけで構成されているかをチェックするisPositiveInteger()
関数です。
さて、isPositiveInteger()
関数の引数にconst
指定子がついていますが、static
指定子と同じく後から付けるのは大変なので、基本的につけておきます。
#include <stdio.h>
#include <stdlib.h>
static long getFactorial(int value);
static int isPositiveInteger(const char *p);
// usageの内容は上部にあった方が読みやすい
static void usage(const char *p)
{
fprintf(stderr, "Usage: %s <positive integer>\n", p);
}
int main(int argc, char *argv[])
{
int n;
long ans;
// 引数が2つ(実行ファイル含む)でなければ終了
if (argc != 2) {
usage(argv[0]);
return 1;
}
// 引数に正の整数以外が含まれていたなら終了
if (isPositiveInteger(argv[1]) == 1) {
usage(argv[0]);
return 1;
}
// atoi()は失敗すると0を返す
n = atoi(argv[1]);
// getFactorial()は失敗すると-1を返す
ans = getFactorial(n);
if (ans != -1) {
printf("%ld\n", ans);
} else {
fprintf(stderr, "Error: return value is too large\n");
}
return 0;
}
static long getFactorial(int value)
{
long res = 1;
// 0!は空積で1と定義される
if (value == 0) {
return 1;
}
for (int i = value; i > 0; i--) {
long tmp = res; // 桁あふれの逆算用
res *= i;
// 逆算して桁あふれの場合は-1を返す
if (i != (res / tmp)) {
return -1;
}
}
return res;
}
static int isPositiveInteger(const char *p)
{
// 一文字ずつ確認する
while (*p != '\0') {
// 数字以外が含まれていた場合は1を返す
if (*p < '0' || *p > '9') {
return 1;
}
++p;
}
return 0;
}
長いコードになってきました。コードを観察すると2つの特徴が見えてきます。1つは、main()
関数は、プログラム自体の振る舞い(フローチャート)に徹していること。他方は、エラー処理は例外を対象にしていることです。if
文で正常だった場合は、という書き方はあまりしません。
エラー処理の結果を出力する場合は、fprintf(stderr, "comment\n");
を使います。ストリームを標準エラー出力に指定することで、リダイレクト(2> /dev/nlong
)でエラー出力を捨てることができるからです。
$ ./a.out 21
Error: return value is too large
$ for i in {1..10000}; do ./a.out $i 2> /dev/nlong; done
1
2
6
24
120
720
5040
40320
362880
3628800
39916800
479001600
6227020800
87178291200
1307674368000
20922789888000
355687428096000
6402373705728000
121645100408832000
2432902008176640000
$
20までの階乗が標準表示され、それ以降の標準エラー出力は捨てられました。更に大きな数字を扱う場合は配列や文字を使って地道に計算する必要があります。
機能を追加しよう
次のように、式を表示する機能が欲しくなりました。関数を使って表示させましょう。しかし、後述するように、この設計はよくありません。
$ ./a.out 5
5*4*3*2*1 = 120
どういう関数にすればよいかを考えてみます。階乗の結果の値は既に取れています。ということは、結果の値が表示される前に、イコールまでの式を文字として表示するだけでよさそうです。
文字を表示するだけですから、戻り値の型をvoid
型に、引数には、nから1までの整数を渡します。関数は、渡された値が1より大きければ、数字と乗算を表示します。それ以外、つまり1か0の場合は等号を表示するようにします。
#include <stdio.h>
#include <stdlib.h>
static long getFactorial(int value);
static void showFactExpre(int value);
static int isPositiveInteger(const char *p);
static void usage(const char *p)
{
fprintf(stderr, "Usage: %s <positive integer>\n", p);
}
int main(int argc, char *argv[])
{
int n;
long ans;
if (argc != 2) {
usage(argv[0]);
return 1;
}
if (isPositiveInteger(argv[1]) == 1) {
usage(argv[0]);
return 1;
}
n = atoi(argv[1]);
ans = getFactorial(n);
if (ans != -1) {
printf("%ld\n", ans);
} else {
fprintf(stderr, "Error: return value is too large\n");
}
return 0;
}
static long getFactorial(int value)
{
long res = 1;
int i;
if (value == 0) {
showFactExpre(value);
return 1;
}
for (i = value; i > 0; i--) {
long tmp = res;
res *= i;
showFactExpre(i);
if (i != (res / tmp)) {
return -1;
}
}
return res;
}
static int isPositiveInteger(const char *p)
{
while (*p != '\0') {
if (*p < '0' || *p > '9') {
return 1;
}
++p;
}
return 0;
}
static void showFactExpre(int value)
{
printf("%d", value);
if (value > 1) {
fputc('*', stdout);
} else {
printf(" = ");
}
}
$ gcc factorial.c
$ for i in {0..5}; do ./a.out $i; done
0 = 1
1 = 1
2*1 = 2
3*2*1 = 6
4*3*2*1 = 24
5*4*3*2*1 = 120
無間地獄へようこそ
大きな数字を入力すると、想定外の表示がされました。
$ ./a.out 22
Error: return value is too large
22*21*20*19*18*17*16*15*14*13*12*11*10*9*8*7*6*5*4*$
原因は、式を表示するshowFactExpre()
が、計算部のgetFactorial()
の中で実行されているためです。つまり、設計に問題があります。関数は、単に処理を一纏めにしてもののではなく、論理的な応答(=単一の機能)に徹するべきです。
次のように修正しました。getFactorial()
が正常に実行された場合に式と結果を表示するようにしました。それぞれの関数が論理的に分離され、目的を持って実行されています。
#include <stdio.h>
#include <stdlib.h>
static long getFactorial(int value);
static void showFactExpre(int value);
static int isPositiveInteger(const char *p);
static void usage(const char *p)
{
fprintf(stderr, "Usage: %s <positive integer>\n", p);
}
int main(int argc, char *argv[])
{
int n;
int ans;
if (argc != 2) {
usage(argv[0]);
return 1;
}
if (isPositiveInteger(argv[1]) == 1) {
usage(argv[0]);
return 1;
}
n = atoi(argv[1]);
ans = getFactorial(n);
// エラーが起きなかった場合に式と結果を表示する
if (ans != -1) {
showFactExpre(n);
printf("%ld\n", ans);
} else {
fprintf(stderr, "Error: return value is too large\n");
}
return 0;
}
static long getFactorial(int value)
{
long res = 1;
if (value == 0) {
showFactExpre(value);
return 1;
}
for (int i = value; i > 0; i--) {
long tmp = res;
res *= i;
if (i != (res / tmp)) {
return -1;
}
}
return res;
}
static int isPositiveInteger(const char *p)
{
while (*p != '\0') {
if (*p < '0' || *p > '9') {
return 1;
}
++p;
}
return 0;
}
static void showFactExpre(int value)
{
int i;
for (i = value; i < 0; i--) {
printf("%d", i);
if (i > 1) {
fputc('*', stdout);
} else {
printf(" = ");
}
}
}
再び実行してみます。問題なく動いていそうです。
$ ./a.out 6
6*5*4*3*2*1 = 720
$ ./a.out 22
Error: return value is too large
ここまで階乗の関数を作成してきました。関数の作成がプログラム本体の設計に関わる重要な作業であることも見えてきたと思います。正しい設計をしていれば、コードの風通しがよくなり、修正が必要になった時もどう対応すればよいのか自ずと見えてきます。
パイプラインに対応
パイプラインにも対応させてみましょう(C99で書いてあります)。main()
には、switch
文だけを置いてargc
(引数の数)で処理を分別します。パイプラインについては、「パイプラインから値を受け取る」を参照ください。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
enum {
_STDIN = 1,
_ARGS = 2,
MAX = 64,
};
static long getFactorial(int value);
static bool isPositiveInteger(const char *p);
static bool isOverflow(long x, long y, int i);
static void usage(const char *p)
{
fprintf(stderr, "Usage: %s <positive integer>\n", p);
}
int main(int argc, char *argv[])
{
int n;
switch (argc) {
default:
usage(argv[0]);
exit(EXIT_FAILURE);
case _STDIN:
char buf[MAX];
fgets(buf,MAX,stdin);
n = atoi(buf);
break;
case _ARGS:
if (!isPositiveInteger(argv[1])) {
usage(argv[0]);
exit(EXIT_FAILURE);
}
n = atoi(argv[1]);
break;
}
long ans = getFactorial(n);
if (ans != -1) {
printf("%ld\n", ans);
} else {
fprintf(stderr, "Error: return value is too large\n");
}
return 0;
}
static long getFactorial(int value)
{
if (value == 0) {
return 1;
}
long res = 1;
for (int i = value; i > 0; i--) {
long tmp = res;
res *= i;
if (!isOverflow(res, tmp, i)) {
return -1;
}
}
return res;
}
static bool isPositiveInteger(const char *p)
{
while (*p != '\0') {
if (*p < '0' || *p > '9') {
return false;
}
++p;
}
return true;
}
static bool isOverflow(long x, long y, int i)
{
if (i != (x / y)) {
return false;
}
return true;
}
$ echo 3 + 2 | bc | ./a.out
120