5.関数
構造化プログラミングにおいては、「関数」をうまく使うことが、とても重要です。
5.1 関数のしくみ
5.1.1 関数とは
あるまとまりのある機能を実行するための処理単位です。C言語のプログラムは関数の集まりだといえます。これまで書いてきたプログラムはすべてmain関数を記述したものです。C言語ではこのmain関数を頂点に、処理系が用意したライブラリ関数と、プログラマが作ったユーザ関数をリンクして、実行形式が作成されます。
5.1.2 関数化のメリット
特定の機能を関数という処理単位にまとめると、わかりやすいプログラムになります。また、プログラムのテストや修正も関数単位で行えるので、開発の効率も上がります。複数のプログラマによる分業も可能になります。さらに、関数を別のプログラムで活用することもできます。
関数は、一つ一つを独立した機能単位にまとめ、なるべく他の関数との関連性は断つべきです。ある関数を修正すると、他の関数にも影響が及ぶようなつくりにはすべきではありません。
5.1.3 関数の使い方の基本
以下の例は、二つの整数の和を計算する単純な関数をつかったプログラムです。
<例5−1>
#include <stdio.h>
int summation(int a, int b); /* 関数summationのプロトタイプ宣言 */
int main(void) {
int
n, m;
int
result;
/*
キーボードから2つの整数値を入力 */
printf("Input
two numbers (separate by space): ");
scanf("%d
%d", &n, &m);
result=
summation(n, m); /*
関数summationの呼び出し */
/*
結果の表示 */
printf("Sum
of %d and %d is %d\n", n, m, result);
return
0;
}
/* 関数summationの定義
*/
/* 2つの整数値の和を計算する */
int summation(int a, int b) {
int
sum;
sum
= a+b;
return
sum;
}
[解説] プロトタイプ宣言
mainのなかの
result=summation(n, m);
というところで、summationというユーザー関数を呼び出しています。呼び出す際に足す2つの変数を「引数」(nとm)として関数にわたし、関数が返してきた「返却値」をresultに代入しています。
一方summation関数側では、mainで呼び出された時に渡された値を「仮引数」(aとb)をして受け取って、その足し算の結果(sum)の値を
return sum;
でmainに返しています。
このように関数側に必要な値を送るのには引数を、逆に関数から呼び出し元に値を送るには返却値を使うのが基本です。
【課題5−1】(プログラムを提出)
<例5−1>の関数を、足し算だけでなく5つの演算子(+, -, *, /, %)の計算ができる関数に拡張する。(参考:【課題2−2】)
ただし、5つのバラバラの関数を作るのではなく、一つの関数にまとめること(二つの整数値のほかに、演算の種類も引数で指定する)。
《プログラム例》
5.2 関数との値の受け渡し
5.2.1 関数に配列を渡す
関数にある値を渡すには引数を使いますが、配列を引数にすることで、多くの値をまとめて関数に渡すことができます。
<例5−2>
#include <stdio.h>
int summation(int *dt, int n); /*
関数summationのプロトタイプ宣言 */
int main(void) {
int
data[10]={1,2,3,4,5,6,7,8,9,10};
int
result;
result
= summation(data, 10); /* 関数summationの呼び出し */
printf("sum
of data = %d\n", result); /* 結果の表示 */
return
0;
}
/* 関数summationの定義
*/
/* dtの中のn個の要素の値の和を計算する */
int summation(int *dt, int n) {
int
i, sum=0;
for
(i = 0; i < n; i++) {
sum
+= *dt;
dt++;
}
return
sum;
}
[解説] 配列の仮引数
この例では、mainの側から
result = summation(data, 10);
と、関数summationを呼び出しています。1番目の引数としてint型の配列(data)を、2番目の引数としてその要素数(10)を渡しています。
dataと書いた場合、それは「配列dataの先頭アドレス」の値を示します。つまり、関数側に「このアドレスから10個のint型の値が並んでいるので、それを使ってくれ」といっているわけです。
dataはint型の配列(の先頭アドレス)ですので、それを受け取る関数側はint型のポインタ(アドレスを格納する変数)を仮引数(int *dt)として受ける必要があります。mainから呼び出された時点で、ポインタ変数dtにはdataの先頭アドレスが入ることになります。
5.2.2 関数から値を返す
関数から値を返す時には、return文を使って返却値として返すのが基本ですが、これだと一つの値しか返せません。複数の値を呼び出し元に返したい場合はどうすればよいでしょうか?
<例5−3>
#include <stdio.h>
/* 関数calculateのプロトタイプ宣言 */
void calculate(int a, int b, int
*sum, int *sub, int *mul, int *div);
int main(void) {
int
n, m; /*
数値入力用変数 */
int
wa, sa, seki, shou; /*
計算結果格納用変数 */
/*
キーボードから2つの整数値を入力 */
printf("Input
two numbers (separate by space): ");
scanf("%d
%d", &n, &m);
/*
関数calculateを呼び出した後、計算結果を表示 */
calculate(n,
m, &wa, &sa, &seki, &shou);
printf("%d
+ %d = %d\n", n, m, wa);
printf("%d
- %d = %d\n", n, m, sa);
printf("%d
* %d = %d\n", n, m, seki);
printf("%d
/ %d = %d\n", n, m, shou);
return
0;
}
/* 関数のcalculateの定義
*/
/* 2つの整数値の和、差、積、商を計算する */
void calculate(int a, int b, int
*sum, int *sub, int *mul, int *div)
{
*sum
= a+b;
*sub
= a-b;
*mul
= a*b;
*div
= a/b;
}
[解説] void型
[解説] 値渡しとアドレス渡し
この例のmainでは
calculate(n, m, &wa, &sa,
&seki, &shou);
というように、6個の引数を渡して関数を呼び出しています。始めの2つの引数は計算に使う値ですが、残りの4つは、&がついていることから分かるとおり、それぞれint型変数のアドレスを渡しています。ちなみに、前者を「値渡し」、後者を「アドレス渡し」と呼んでいます。値渡しとアドレス渡しについては、[解説]を参照してください。
これまでにさんざん使ってきたscanf関数は、この手の関数の代表例です。
配列を使って、関数から多くの値をまとめて返すということも、よく用いられる手法です。
<例5−4>
#include <stdio.h>
#define DATA_SIZE 10
/* 関数double_arrayのプロトタイプ宣言 */
void double_array(const int *src,
int *dst, int n);
int main(void) {
int
data[DATA_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; /* データ配列 */
int
result[DATA_SIZE]; /*
計算結果格納用配列 */
int
i;
double_array(data,
result, DATA_SIZE); /*
関数double_arrayの呼び出し */
/*
結果の表示 */
for
(i = 0; i < DATA_SIZE; i++) {
printf("%d\n",
result[i]);
}
return
0;
}
/* 関数double_arrayの定義 */
/* srcの各要素の2倍の値をdstに書き込む */
void double_array(const int *src,
int *dst, int n) {
int
i;
for
(i = 0; i < n; i++) {
dst[i]
= src[i] * 2;
}
}
[解説] const修飾子
この例では、mainからデータの入った配列dataと、計算結果を入れるための配列resultを引数として、関数double_arrayを呼び出しています。配列要素の数(DATA_SIZE)もint型として渡しています。
<例5−2>でもみたように、関数側が配列(の先頭アドレス)を受け取る際、通常はポインタを仮引数として用います。実際には、仮引数dstの中にはmain内のresultのアドレスが入るわけですから、関数側でその配列の中身を参照することができるだけでなく、その中身を書き換えることもできるわけです。これも一種のアドレス渡しです。
関数double_arrayの中では、dstが指す配列(すなわちresult)の各要素に、srcが指す配列(すなわちdata)を2倍した値を代入しています。
【課題5−2】(プログラムを提出)
<例5−1>や<例5−4>を参考に、「二つのint型配列の要素同士の和を求める」という関数を使ったプログラムをつくる。
例えば、使うデータ配列をdata1, data2、結果を収める配列をresultとすれば
result[0]
= data1[0] + data2[0]
result[1]
= data1[1] + data2[1]
...
【余裕がある人は...】
【課題5−1】を参考に、2つ配列に対して5種類の演算(+, -, *, / , %)を行うことができる関数に拡張する。
《プログラム例》
多くのデータをまとめて返す方法には、関数側で配列を作ってそこに値を格納し、その配列のアドレスを返却値として返す、という方法もあります。これには「メモリ領域の動的確保」が必要になります(後述)。