自作関数の考え方と設計について
自作関数の考え方について、階乗するだけの簡単なプログラムを通して紹介します。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
文とそこで扱う変数が対象になります。
階乗は正の整数のみ扱いかつ大きな値になるので、関数の戻り値の型をunsigned long int
型にします。少し読みづらいので、typedef
でulong
という名前の型を定義しましたが、別にそのままでも構いません。
#include <stdio.h>
#include <stdlib.h>
typedef unsigned long int ulong;
static ulong getFactorial(int value);
int main(void)
{
int n = 5;
printf("%d! is %ld\n", n, getFactorial(n));
return 0;
}
static ulong getFactorial(int value)
{
ulong 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>
typedef unsigned long int ulong;
static ulong 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 ulong getFactorial(int value)
{
ulong 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>
typedef unsigned long int ulong;
static ulong 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;
// 引数が2つ(実行ファイル含む)でなければ終了
if (argc != 2) {
usage(argv[0]);
return 1;
}
// 引数に正の整数以外が含まれていたなら終了
if (isPositiveInteger(argv[1]) == 1) {
usage(argv[0]);
return 1;
}
n = atoi(argv[1]); // atoi()は失敗すると0を返す
printf("%ld\n", getFactorial(n));
return 0;
}
static ulong getFactorial(int value)
{
ulong res = 1;
// 0!は空積で1と定義される
if (value == 0) {
return 1;
}
for (int i = value; i > 0; i--) {
ulong tmp = res; // 桁あふれの逆算用
res *= i;
// 逆算して桁あふれを判定
// isOverflow()という関数にしてもよい
if (i != (res / tmp)) {
fprintf(stderr, "Error: return value is too large\n");
exit(EXIT_FAILURE);
}
}
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/null
)でエラー出力を捨てることができるからです。
$ ./a.out 21
Error: return value is too large
$ for i in {1..10000}; do ./a.out $i 2> /dev/null; 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の場合は等号を表示するようにします。さらに、#ifdef DEBUG
をつけ、コンパイル時に表示/非表示を選択できるようにしました。
#include <stdio.h>
#include <stdlib.h>
typedef unsigned long int ulong;
static ulong 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;
if (argc != 2) {
usage(argv[0]);
return 1;
}
if (isPositiveInteger(argv[1]) == 1) {
usage(argv[0]);
return 1;
}
n = atoi(argv[1]);
printf("%ld\n", getFactorial(n));
return 0;
}
static ulong getFactorial(int value)
{
ulong res = 1;
if (value == 0) {
#ifdef DEBUG
showFactExpre(value);
#endif
return 1;
}
for (int i = value; i > 0; i--) {
ulong tmp = res;
res *= i;
#ifdef DEBUG
showFactExpre(value);
#endif
if (i != (res / tmp)) {
fprintf(stderr, "Error: return value is too large\n");
exit(EXIT_FAILURE);
}
}
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 -DDEBUG test.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*$
想定外の表示がされました。
またエラー処理の始まりです。しかし、独自関数を作成してきたことで、コードの風通しがよく、どの関数がどんな仕事をしているのかが明晰になっていると思います。どこをどう修正すればよいのか自ずと見えてくるはずです。
なお、解決方法ですが、malloc()
で確保したメモリに式を文字列として格納していき、成功した場合にまとめて出力する方法が考えられます。もしかしたら、もっと簡単な方法があるかも知れません。
パイプラインに対応
パイプラインにも対応させてみましょう(C99で書いてあります)。main()
には、switch
文だけを置いてargc
(引数の数)で処理を分別します。パイプラインについては、「パイプラインから値を受け取る」を参照ください。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
enum {
_STDIN = 1,
_ARGS = 2,
MAX = 64,
};
typedef unsigned long long ull;
static ull getFactorial(int value);
static bool isPositiveInteger(const char *p);
static bool isOverflow(ull x, ull 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;
}
printf("%llu\n", getFactorial(n));
return 0;
}
static ull getFactorial(int value)
{
if (value == 0) {
return 1;
}
ull res = 1;
for (int i = value; i > 0; i--) {
ull tmp = res;
res *= i;
if (!isOverflow(res, tmp, i)) {
fprintf(stderr, "Error: return value is too large\n");
exit(EXIT_FAILURE);
}
}
return res;
}
static bool isPositiveInteger(const char *p)
{
while (*p != '\0') {
if (*p < '0' || *p > '9') {
return false;
}
++p;
}
return true;
}
static bool isOverflow(ull x, ull y, int i)
{
if (i != (x / y)) {
return false;
}
return true;
}
$ echo 3 + 2 | bc | ./a.out
120