作曲・指導・C言語・Linux

金沢音楽制作

金沢音楽制作では、楽曲・楽譜の制作と、作曲や写譜などレッスンを行っています。

自作関数の考え方と設計について

自作関数の考え方について、階乗するだけの簡単なプログラムを通して紹介します。(独自関数についてのみ書くつもりでしたが、どんどん話が大きくなってしまいました。)

プログラムをmain()関数だけで書いてしまうと、冗長で可読性が低くなります。そこで、ある一連の処理をまとめた自作関数を作成して、コードを読みやすくします。そうすることで、エラー処理や改良がしやすくなります。

C言語(C89+gccの拡張機能)で書いてありますが、どの言語でも考え方は同じです。

出力するだけなら簡単

階乗を標準出力するだけなら簡単です。ここでは、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型にします。少し読みづらいので、typedefulongという名前の型を定義しましたが、別にそのままでも構いません。

#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()関数です。他方は、コマンドライン引数が正の整数だけで構成されているかをチェックするisNumber()関数です。

さて、isNumber()関数の引数にconst指定子がついていますが、static指定子と同じく後から付けるのは大変なので、基本的につけておきます。

#include <stdio.h>
#include <stdlib.h>

typedef unsigned long int ulong;

static ulong getFactorial(int value);
static int isNumber(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 (isNumber(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;
  ulong tmp;  // 桁あふれの比較用

  // 0!は空積で1と定義される
  if (value == 0) {
    return 1;
  }

  for (res = 1, tmp = 1; value > 0; value--) {
    res *= value;

    // 桁あふれを起こすと終了
    // isOverflow(int)という関数を考えてもよい
    if (res < tmp) {
      fprintf(stderr, "Error: return value is too large\n");
      exit(EXIT_FAILURE);
    }
    tmp = res;
  }

  return res;
}

static int isNumber(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 isNumber(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 (isNumber(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;
  ulong tmp;

  if (value == 0) {

#ifdef DEBUG
showFactExpre(value);
#endif

    return 1;
  }

  for (res = 1, tmp = 1; value > 0; value--) {
    res *= value;

#ifdef DEBUG
showFactExpre(value);
#endif

    if (res < tmp) {
      fprintf(stderr, "Error: return value is too large\n");
      exit(EXIT_FAILURE);
    }
    tmp = res;
  }

  return res;
}

static int isNumber(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()で確保したメモリに式を文字列として格納していき、成功した場合にまとめて出力する方法が考えられます。もしかしたら、もっと簡単な方法があるかも知れません。

更新情報