2003年度後期 IT教育基礎論演習B

第4回: 手続き型プログラミング(2)
プログラムの部品化と再利用

概要と目標

■ 概要

手続き型プログラミング言語における プログラムの部品化と再利用方法を理解し、 プログラムの効率的な開発方法を習得する。

■ 目標

目次


演習内容

■ 関数

C言語では、複数の式からなる手続き、 すなわちブロックに対して名前をつけ関数として定義することで、 プログラムを部品化して利用することができる。 このとき、関数に引数を与えることで、その手続きを汎用的に利用することができ、 また、関数の戻り値により、関数を実行した結果を得ることができる。

□ 関数の定義と呼び出し

新しく関数を定義するには、関数(の戻り値)の型、 関数名、関数の引数の型と名前の列、およびその関数で実行する手続きを 以下の形式で記述する。 このとき、関数の戻り値は、return文で指定することができる。

<type> <function>(<type-1> <variable-1>,<type-2> <variable-2>, ...) {
  <procedures>
      ...
  return <value>
}

例えば、2つの整数を引数に取り、その和を返す関数"add()"は、 以下のように定義することができる。

int add(int a, int b) {
  int c;

  c = a + b;
  return c;
}

定義された関数を利用するには、以下のように、 その関数の名前と引数を与えればよい。

result = add(1, 2);

ただし、関数を使用するには、プログラムの中で関数を使用する場所より前で その関数を定義するか、もしくは、以下の形式で記述される 関数原型宣言(プロトタイプ宣言)を行う必要がある。

<type> <function>(<type-1>, <type-2>, ...);

例えば、2つの整数の和を求める関数"add()"を定義し、 利用したプログラムは、以下の2種類の形式で記述することができる。

関数を先に定義したプログラム: add-1.c
/* add-1.c */

#include <stdio.h>

int add(int a, int b) {
  int c;

  c = a + b;
  return c;
}

int main(int argc, char **argv)
{
  int i=1, j=2;
  int result;

  result = add(i, j);
  printf("adding %d to %d makes %d.\n", i, j, result);
}

プロトタイプ宣言を使用したプログラム: add-2.c
/* add-2.c */

#include <stdio.h>

int add(int, int);

int main(int argc, char **argv)
{
  int i=1, j=2;
  int result;

  result = add(i, j);
  printf("adding %d to %d makes %d.\n", i, j, result);
}

int add(int a, int b) {
  int c;

  c = a + b;
  return c;
}

なお、戻り値を必要としない関数を定義したい場合、 戻り値の型を定義する代わりに"void"を指定し、 return文も記述しなくてよい。

□ 値渡しと参照渡し

関数の中では、引数として与えられた値を代入する変数のほかに、 関数の中で新たな変数を宣言し、利用することができる。 これらの変数の有効範囲は、原則的にはその関数の中となる。 従って、引数を格納する変数や、関数内で宣言された変数の値を変更しても、 その関数外へは影響しない。

例えば、以下のプログラムのように同じ名前の変数名を使用していても、 関数に引数として渡されるのは変数の値であり、 関数を呼び出した側の変数の値は変更されていないことがわかる。 このような関数の呼び出し方法を値渡しによる呼び出し(call by value)と呼ぶ。

関数への値渡しの例: call-by-value.c
/* call-by-value.c */

#include <stdio.h>

void function(int a) {
  a++; /* add a point to a */

  printf("Value of a in the function: %d\n", a);
}

int main(int argc, char **argv) {
  int a=0;

  printf("Initial value of a: %d\n", a);
  function(a);
  printf("Value of a after calling the function: %d\n", a);
}

call-by-value の実行結果
$ ./call-by-value
Initial value of a: 0
Value of a in the function: 1
Value of a after calling the function: 0

これに対し、 用意した配列に値を代入するための手続きを関数として定義したい場合など、 何らかの変数の中身を関数内で変更したい場合もある。 その場合には、関数の引数として値を渡すのではなく、 変数そのものを関数に渡すことができると便利である。 このような関数の呼び出し方法を参照渡しによる呼び出し(call by reference)と呼ぶ。

C言語では、参照渡しによる関数の呼び出しを行うことはできない。 そのかわり、ポインタを使用して同様の呼び出しを行うことができる。 関数の引数として変数の値が格納されている領域へのポインタを渡すことで、 呼び出された関数の中で変数の値を変更することができる。 例えば、用意された整数型変数に標準入力より入力された数値を 格納する関数を利用したプログラムは、 以下のように記述できる。

C言語における参照渡しの実現例: call-by-reference.c
/* call-by-reference.c */

#include <stdio.h>

void read_number(int *pointer) {
  printf("Input number: ");
  scanf("%d", pointer);
}

int main(int argc, char **argv) {
  int number;

  read_number(&number);
  printf("Inputed number is %d\n", number);
}

このプログラムを実行すると、以下のように、 main()関数内で宣言された変数であるnumberの値を 関数read_number()の中で変更できていることがわかる。

call-by-reference の実行例
$ ./call-by-reference
Input number: 3
Inputed number is 3

参照渡しを利用することにより、 変数への代入のための手続きを関数として定義できる。 例えば、main()関数の中で用意された構造体の配列の各要素に、 標準入力から入力された値を格納するプログラムを 以下のように記述することができる。

変数への代入手続きのための関数定義例: nominal-list-input.c
/* norminal-list-input.c */

#include <stdio.h>

struct _person {
  char name[256];
  int age;
};

int input(struct _person *person) {
  fflush(stdin); /* gets() による入力の前に標準入力のバッファをクリア */

  printf("Name: ");
  if ( gets(person->name) == NULL ) { /* 1行入力 */
    return 0; /* ^D(C-d)キーを押すと終了 */
  }    
  printf("Age: ");
  if ( scanf("%d", &person->age) == EOF ) { /* 整数の入力 */
    return 0; /* ^D(C-d)キーを押すと終了 */
  }
  return 1;
}

int main(int argc, char **argv) {
  struct _person list[256];
  int status;
  int num = 0, i;

  while( num < 256 && input(&list[num]) ) { /* 入力数が256未満かつ入力がある間は繰り返す */
    num++;
  }

  printf("\nLists are...\n"); /* 入力結果を標準出力に出力 */
  for( i = 0; i < num; i++ ) {
    printf("%s (%d)\n", list[i].name, list[i].age);
  }
}

nominal-list-input の実行例
$ ./nominal-list-input
ame: Masayuki Torai
Age: 34
Name: Takoshi Yotsuashi
Age: 33
Name: Yuji Tameyama
Age: 33
Name: Yasuto Izumikawa
Age: 32
Name: Yuichi Ogawa
Age: 29
Name: ^D
Lists are...
Masayuki Torai (34)
Takoshi Yotsuashi (33)
Yuji Tameyama (33)
Yasuto Izumikawa (32)
Yuichi Ogawa (29)

□ 関数の再帰呼び出し

関数の定義の中で、その関数自身を利用することを関数の再帰呼び出しと呼ぶ。 例えば、1からnまでの総和を求める関数sum(n)は、 関数の再帰呼び出しを利用して以下のように帰納的に定義することができる。

sum(n)関数の帰納的な定義
sum(n) =
n
Σ i
i = 1
= {
n - 1
n + Σ i
i = 1
= n + sum(n-1)   (n > 1)
1   (n = 1)
 

これをC言語の関数として定義する場合、以下のように記述することができる。

関数の再帰呼び出しを利用したsum()関数の帰納的定義
int sum(int n) {
  if ( n > 1 )
    return n + sum(n - 1);
  else
    return 1;
}

ただし、この様に関数を定義したほうが見た目に判りやすい場合もあるが、 関数の再帰呼び出しはメモリ領域を多く消費するうえ、 実行速度も遅くなることが多いため、あまり推奨されない。 プログラムが判りづらくならない限り、繰り返し構造を利用すべきである (注: 再帰呼び出しを利用しないと定義できない関数もある)。

main()関数

C言語によるプログラムにおいて、特別な関数としてmain()がある。 main()関数は、1つのプログラムに必ず1つ存在し、 そのプログラムが実行された際に先ずはじめに呼び出される関数であり、 プログラムの実行と終了におけるOSとの橋渡しをする役目を持つ。

main()関数は、 プログラムを実行する際に指定された引数をプログラム中で参照するために、 整数型および文字配列の配列の2つの引数をとり、 またプログラムの終了状態をOSに返すために、整数型の戻り値を返す。 従って、一般にmain()関数は以下のように定義される。

int main(int argc, char **argv) {
        ...
     <関数の本体>
        ...
  return <戻り値>;
}

一般に、プログラムの実行時にそのプログラムに渡される引数の個数は そのプログラムにより異なり、 プログラムによっては与えられる引数の個数が可変の場合もある。 このように個数が不定の引数を扱うために、main()関数では、 第1引数としてプログラムの実行時に与えられた引数の個数をとる。 ただし、実行時に指定されたプログラムへのパスも引数の1つとして数えられるため、 第1引数の値は、引数として与えられた個数より1大きい値となる。 従って、パス以外の引数が1つも与えられてない場合は1、 n 個の引数が与えられている場合はn +1となる。

また、第2引数には、引数として与えられた個々の文字列を格納した 文字配列へのポインタを要素とする配列へのポインタをとる。 ただし、第2引数として与えられる配列の0番の要素には 実行時に指定されたプログラムへのパスが格納されるため、 引数として与えられた文字列は1番目以降に格納される。 従って、n 番目の引数である文字配列は、 argv[n ] のように参照することができる。

例えば、main()関数の引数の使用例として、 与えられた引数を標準出力に出力するプログラムは 以下のように記述できる。

プログラム実行時の引数を出力するプログラム: check-main-args.c
/* check-main-args.c */

#include <stdio.h>

int main(int argc, char **argv) {
  int i;

  printf("Number of args is %d.\n", argc);
  for( i = 0; i < argc; i++ ) {
      printf(" argv[%d]: %s\n", i, argv[i]);
  }
  return 0;
}

このプログラムの実行結果は以下のようになる。

check-main-argsの実行結果
$ ./check-main-args first second third ...
Number of args is 5.
 argv[0]: ./check-main-args
 argv[1]: first
 argv[2]: second
 argv[3]: third
 argv[4]: ...

■ マクロ定義

C言語では、マクロ定義により数値や式、手続きに名前をつけ、 プログラムの中で利用することができる。 マクロは、プログラムのコンパイル時に、 コンパイラのプリプロセッサによりもとの定義内容に展開されるため、 関数定義によるプログラムの部品化と違い、 厳密な意味での部品化と再利用とは異なるが、 関数を定義し、呼び出すことに比べ実行速度を上げることができる、 プログラムの記述性、視認性を向上できるといった利点がある。

□ マクロの定義と利用

新たにマクロ定義を行う場合、以下のように記述する。

#define <マクロ名> <定義内容>

例えば、円周率3.14に"PI"という名前をつけて利用したい場合、 以下のように記述する。

#define PI 3.14

これにより、以下のプログラムのように、 プログラム内で自由にPIを利用することができる。

マクロ定義の利用例(1): circle-and-spherea.c
/* circle-and-spherea.c */

#include <stdio.h>

#define PI 3.14159265358979323846

int main(int argc, char **argv) {
  float r;

  printf("Radius? ");
  scanf("%f", &r);

  printf("Circumference of circle is %f\n", 2 * PI * r);
  printf("Size of circle is %f\n",          PI * r * r);
  printf("Surface are of spherea is %f\n",  4 * PI * r * r);
  printf("Volume of spherea is %f\n",       4 * PI * r * r * r / 3);
}

circle-and-sphereaの実行結果
./circle-and-spherea
Radius? 2
Circumference of circle is 12.566371
Size of circle is 12.566371
Surface are of spherea is 50.265482
Volume of spherea is 33.510322

同様に、引数をとる演算式や手続きに対してマクロを定義することもできる。 例えば、与えられた引数の2乗や3乗を計算する演算式を以下のように 定義することができる。

#define square(x) ((x) * (x))
#define cube(x)   ((x) * (x) * (x))

このマクロ定義して、プログラムcircle-and-spherea.cを 以下のように書き改めることができる。

マクロ定義の利用例(1): circle-and-spherea.c
/* circle-and-spherea.c */

#include <stdio.h>

#define PI        3.14159265358979323846
#define square(x) ((x) * (x))
#define cube(x)   ((x) * (x) * (x))

int main(int argc, char **argv) {
  float r;

  printf("Radius? ");
  scanf("%f", &r);

  printf("Circumference of circle is %f\n", 2 * PI * r);
  printf("Size of circle is %f\n",          PI * square(r));
  printf("Surface are of spherea is %f\n",  4 * PI * square(r));
  printf("Volume of spherea is %f\n",       4 * PI * cube(r) / 3);
}

このようにマクロ定義を利用することにより、 プログラムの開発効率を高められるだけでなく、 記述されたプログラムの視認性、可読性も高めることができる。

□ マクロの展開

マクロは、プログラムをコンパイルする際に、 コンパイルに先立ち、プリプロセッサによりもとの定義内容に展開される。 このため、引数をとるマクロなどを定義する場合には、 展開後の形を想定するなどの注意が必要となる。

例えば、与えられた数値の2乗を計算するマクロとして以下のように定義すると、 一見よさそうに見えるが、実際には意図しない結果をえることになる。

#define square(x) x * x         /* 不適切なマクロ定義 */

このマクロ定義を利用して、(2 + 3)の2乗の2倍を計算しようとし、 プログラム中に以下のように記述したとする。

answer = square(2 + 3) * 2;

本来、この演算結果は、(2 + 3) * (2 + 3) * 2 = 50となるべきところであるが、 これをもとの定義内容に展開すると、以下のようになり、 結果的に、2 + 3 * 2 + 3 * 2 = 8といった値となってしまう。

answer = 2 + 3 * 2 + 3 * 2;

このような誤りを防ぐには、 引数として与えら得た部分の演算順序を明確にするために()を適切につけ、 以下のように定義する必要がある。

#define square(x) ((x) * (x))   /* 正しいマクロ定義 */

この定義により、上記の記述を展開すると以下のようになり、 正しい結果を得られることがわかる。

answer = ((2 + 3) * (2 + 3)) * 2;

レポート課題

■ レポート課題1

(1)

2つの実数を引数とする四則演算のための関数、 "add()"、"sub()"、 "mul()"、"div()"をそれぞれ定義し、 標準入力から入力された2つの値の四則演算の結果を 標準出力に出力するプログラムを作成せよ。

(2)

一般に、複素数x の実部および虚部は、 それぞれRe (x )、Im (y )と示される。 これに基づき、構造体により定義される複素数の実部および虚部を求める マクロとしてRe(x)およびIm(x)を定義せよ。

次に、このマクロを利用して2つの複素数の和、差、積の演算結果を求める関数、 "complex-add()"、"complex-sub()"、 "complex-mul()"をそれぞれ定義し、 s =3+2i , t =1-4i において s (t-s)の計算結果を求めるプログラムを作成せよ。

ただし、2つの複素数x y の積は以下のように計算できる。

x *y = (Re(x ) * Re(y ) - Im(x ) * Im(y )) + (Re(x ) * Im(y ) + (Im(x ) * Re(y ))i

■ レポート課題2

(1)

関数の再帰呼び出しを利用してnの階乗: n! を求める関数fact()を定義し、 5!の値を求めよ。

(2)

フィボナッチ数列 fibn は、 以下のように帰納的に定義される数列である。

fibn = { 0 (n = 0)
1 (n = 1)
fib n - 1 + fib n - 2 (n > 1)

フィボナッチ数列の定義に基づき、 n 番目のフィボナッチ数列の値を求める関数fib()を定義し、 fib 5の値を求めよ。

■ レポート課題3

"nominal-list-input.c"を参考にして 学籍番号(文字列)、氏名(文字列)、性別(整数: 0ならば女性、1ならば男性とする)、 年齢(整数)からなる構造体を定義し、 入力された学生名簿の一覧を各学生ごとに学籍番号、氏名、 性別(maleもしくはfemale)、年齢の順で出力するプログラムを作成せよ。 プログラムの実行結果には、10名以上を入力した結果を示せ。

■ レポート課題4

以下のように、コマンドラインからの引数がない場合には "Hello, somebody!"、 引数が1つの場合には"Hello, <引数1>!"、 引数が2つ以上の場合には"Hello, everybody!"と 標準出力に出力するプログラムを作成せよ。

$ ./hello-to
Hello, somebody!
$ ./hello-to takoshi
Hello, takoshi!
$ ./hello-to takoshi izumin
Hello, everybody!

参考書籍

  1. B. W. カーニハン, D. M. リッチー 著, 石田 晴久 訳: 「プログラミング言語C 第2版 ANSI規格準拠」, 共立出版(株), ISBN4-320-02692-6, ¥2,800-
  2. 平林 雅英 著:「ANSI C/C++辞典」, ISBN4-320-02797-3, ¥6,500-

Last modified: Tue Oct 28 18:25:11 JST 2003