応用理工学情報処理 5回目

4回目提出課題についてのコメント

(1)C言語では、main関数を含めて関数の内部で関数を定義することはできません。そのようなプログラムを書くとコンパイルエラーとなります。

(2)whileやforループでカウンターとして用いている変数の値を{ }ブロックの途中で変更すると理解しにくいプログラムとなりやすいので注意が必要です。forループは以下のようなwhileループのカウンターに関係する文を1か所にまとめたものに相当します。

i = 0;
while (i < 100) {
    .......
	i++;
}
このループブロックの末尾のi++;をブロック末尾ではなく途中に移動すると、プログラムが非常に読みにくくなります。whileループと比較してforループが使われる機会が多い理由は、カウンターとなる値がどこで変更されるかが明白で読みやすいプログラムとなるためだと思います。

今回は、前回に引き続いてテキスト第6章、関数について学習します。今日の主なテーマは関数への配列の受け渡しと変数の有効範囲、静的変数です。

(a) 関数(2) 値を返さない関数, 配列の受け渡し 

「1」関数内部の変数の独立性

次のプログラムは、2つの引数をとって前者の値を後者の値の回数だけ掛け合わせた値を返す関数を使っています。
/*
	べき乗を求める
*/

#include  <stdio.h>

/*--- dxのno乗を返す ---*/
double power(double dx, int no)
{
	int	 i;
	double	tmp = 1.0;

	for (i = 1; i <= no; i++)
		tmp *= dx;
	return (tmp);
}

int main(void)
{
	int		n;
	double	x;

	printf("実数を入力してください:");   scanf("%lf", &x);
	printf("整数を入力してください:");   scanf("%d",  &n);

	printf("%.2fの%d乗は%.2fです。\n", x, n, power(x, n));

	return (0);
}

この関数呼び出しpower(x, n)で与えた実引数x, nは関数実行に際して値が関数側の変数にコピーされて実行されるため、関数内部で仮引数dx, noの値を書き換えても呼び出し元の変数x, nの値は全く変化しません。このように値がコピーされる変数の受け渡し方法を、値渡し(pass by value)と呼びます。そのため、関数内部で仮引数を使って以下のようにpower( )関数を書くことも出来ます。(p.141, List6-6)

/*--- dxのno乗を返す ---*/
double power(double dx, int no)
{
	double	tmp = 1.0;

	while (no-- > 0)
		tmp *= dx;
	return (tmp);
}

「2」 値を返さない場合の返却値型=>void  値を返さない関数の例 (p142, List6-7) void put_stars(int no); no個の*を表示

/*
	直角三角形(左下が直角)を表示(関数版)
*/

#include  <stdio.h>

/*--- *をno個連続表示 ---*/
void put_stars(int no)
{
	while (no-- > 0)
		putchar('*');
}

int main(void)
{
	int	 i, ln;

	printf("何段ですか:");
	scanf("%d", &ln);

	for (i = 1; i <= ln; i++) {
		put_stars(i);
		putchar('\n');
	}

	return (0);
}

「3」 仮引数を受け取らない場合=>void  関数の例(p.144, List6-9) int scan_uint( void )

/*
	読み込んだ非負の整数値を逆順に表示
*/

#include  <stdio.h>

/*--- 非負の整数を読み込んで返す ---*/
int scan_uint(void)
{
	int	 tmp;

	do {
		printf("非負の整数を入力してください:");
		scanf("%d", &tmp);
		if (tmp < 0)
			puts("\a負の数を入力しないでください。");
	} while (tmp < 0);
	return (tmp);
}

/*--- 非負の整数を逆転した値を返す ---*/
int rev_int(int num)
{
	int	 tmp = 0;

	if (num > 0) {
		do {
			tmp = tmp * 10 + num % 10;
			num /= 10;
		} while (num > 0);
	}
	return (tmp);
}

int main(void)
{
	int	 nx = scan_uint(); //関数の返却値で変数を初期化することも可能

	printf("反転した値は%dです。\n", rev_int(nx));

	return (0);
}

「4」関数原型宣言 (プロトタイプ宣言)

これまで、main( )関数で使用する関数はmain( )関数の前で定義していました。この順番を逆にすると、コンパイラは「プロトタイプ宣言のない関数呼び出し」といったエラーまたは警告を表示します。コンパイラは関数を使っている部分に遭遇すると、その引数の数及び型、またその関数からの返り値が正しいかどうかをチェックするため、関数は使用する前に定義されていなければならないためです。しかし、関数をmain( )プログラムの後や別のファイルで定義したいことは良くあることです。この場合に、main( )関数の前にコンパイラにその関数の引数の数及び型、またその関数からの返り値の情報を与えるために用いるのが関数原型宣言 (プロトタイプ宣言)です。以下のテキストp.146 List 6-10で確認してください。関数原型宣言では、返り値の型、関数名(仮引数リスト); を書きます。 最後のセミコロンを忘れないようにしましょう。

/*
	最高点を求める
*/

#include  <stdio.h>

#define  NUMBER		5

int	 tensu[NUMBER];

/* int	 top(void);		 【関数プロトタイプ宣言】 */

int main(void)
{
/*	extern int  tensu[];*/
	int	 i;

	printf("%d人の点数を入力してください。\n", NUMBER);
	for (i = 0; i < NUMBER; i++) {
		printf("%d:", i + 1);
		scanf("%d", &tensu[i]);
	}
	printf("最高点=%d\n", top());
	return (0);
}

/*--- 配列tensuの最大値を返す【関数定義】 ---*/
int top(void)
{
/*	extern int  tensu[]; */
	int	 i;
	int  max = tensu[0];
	for (i = 1; i < NUMBER; i++)
		if (tensu[i] > max)
			max = tensu[i];
	return (max);
}

「5」関数への配列の受け渡し: 1次元配列の場合 

関数の引数として与えた変数の値は、関数呼び出しにおいて関数内部の変数にコピーされてから処理されるため、関数外部のその変数の値を変化させることはありません。これは関数の独立性を高める非常に重要な機能ですが、非常に大きな配列を関数の引数として与えるような場合には問題となります。そこで、このような場合には配列データの存在する場所を関数の引数として与えます。この関数に引数として与えられた配列の場所(アドレス)に関するデータは関数実行に際して関数内部の変数にコピーしてから実行されるため、関数内で変化させることは出来ません。しかし、この場所のデータを介して実質的に関数外部の配列データを呼ばれた関数内から参照して値を変化させることが出来ます。配列データの存在する場所(アドレス)は配列から[ ]を取った配列名のみの部分で表します。以下のテキストp.150, List 6-11で配列の取り扱いを学びましょう。

/*
	英語の点数と数学の点数の最高点を求める
*/

#include  <stdio.h>

#define  NUMBER		5

/*--- 要素数noの配列vcの最大値を返す ---*/
int max_of(int vc[], int no)
{
	int	 i;
	int  max = vc[0];
	for (i = 1; i < no; i++)
		if (vc[i] > max)
			max = vc[i];
	return (max);
}

int main(void)
{
	int	 i;
	int	 eng[NUMBER];		/* 数学の点数 */
	int	 mat[NUMBER];		/* 英語の点数 */
	int	 max_e, max_m;		/* 最高点 */

	printf("%d人の点数を入力してください。\n", NUMBER);
	for (i = 0; i < NUMBER; i++) {
		printf("[%d] 英語:", i + 1);  scanf("%d", &eng[i]);
		printf( "    数学:");		   scanf("%d", &mat[i]);
	}
	max_e = max_of(eng, NUMBER);	/* 英語の最高点 */
	max_m = max_of(mat, NUMBER);	/* 数学の最高点 */

	printf("英語の最高点=%d\n", max_e);
	printf("数学の最高点=%d\n", max_m);

	return (0);
}

配列の仮引数の書き方と実引数の与え方を確実に理解して下さい。このプログラムはmaxof関数で、 

max_of(int vc[ ], int no)となっています。int vc[NUMBER]となっていないことに注意してください。int vc[NUMBER]と書いてもエラーは起こりませんが、配列で関数に渡されるのは配列の場所(最初のデータのアドレス)だけで、配列の要素数についての情報は全く渡されません。そのため、2番目の引数でその配列の要素数を受け取る必要があるのです。

また、main( )関数内でのmax_of関数の呼び出し max_e = max_of(eng, NUMBER);での配列の実引数への与え方に注意してください。配列eng[ ]の[ ]を取ったeng は配列の最初の要素の場所(アドレス)を表すということを覚えておいて下さい。

ここの int max_of(int vc[ ], int no) 関数では配列の要素の値を書き換えていませんが、関数内部で配列vc[ ]の値を書き換えることも可能です。関数内部でvc[1]=10; とするとmain( )関数の配列eng[1], mat[1]の 値がmax_of(eng, NUMBER) 及び max_of(mat, NUMBER) と呼ばれたときに 10 に変化します。間違って関数外部の配列の値を書き換えることが無いようにしたい場合は、関数定義の仮引数宣言にconst という型修飾子を付けます。つまり、int max_of(const int vc[ ], int no) とします。このプログラムの場合には、誤って試験の点数を書き換えてしまっては困るため、const を付けた方が良い例と言えます。

「6」 関数への配列の受け渡し、多次元配列の場合

配列を、int ma[2][3];と宣言した後、プログラム中で x = ma[1][1];という文でma[1][1]のデータにアクセスする場合には、コンピュータはmaで表される最初のデータのアドレスをもとにそこから (int型のサイズ)×(×1+1)バイト離れた位置のデータを読みに行きます。つまり、配列のそれぞれの要素にアクセスするためにはint ma[2][3];におけるそれぞれの次元サイズ[2]と[3]のうち[2]は必要無いけれども[3]という値は絶対に必要となります。そこで、多次元配列を関数に引数として渡す場合にはこの情報も渡しておかなければ関数内部で配列の要素にアクセスすることが出来ません。そのため、仮引数に指定する配列の要素数は最初の(一番左側の)要素数は省略できますが、それ以外の要素数は省略できません。以下のプログラムの関数の仮引数宣言で要素数を消してみて、どこでエラーが出るかを確認してください。

/*
	2行3列の行列を加算する(関数版)
*/

#include  <stdio.h>

/*--- 2行3列の行列maとmbの和をmcに格納する ---*/
void mat_add(const int ma[][3], const int mb[][3], int mc[][3]) // 3を消去するとエラーとなる
{
	int  i, j;

	for (i = 0; i < 2; i++)
		for (j = 0; j < 3; j++)
			mc[i][j] = ma[i][j] + mb[i][j];
}

int main(void)
{
	int  i, j;
	int	 ma[2][3] = { {1, 2, 3}, {4, 5, 6} };
	int	 mb[2][3] = { {6, 3, 4}, {5, 1, 2} };
	int	 mc[2][3] = { 0 };

	mat_add(ma, mb, mc);					/* maとmbの和をmcに格納 */

	for (i = 0; i < 2; i++) {
		for (j = 0; j < 3; j++)
			printf("%3d", mc[i][j]);
		putchar('\n');
	}

	return (0);
}

====== 演習問題 5A (p5a.c) ==============

3つの配列v1[3], v2[3], result[3]を引数として受け取り、v1とv2を3次元ベクトルとした外積を計算して結果を配列resultに格納して返す関数
void vproduct( const double v1[3], const double v2[3], double result[3]) { /* ...... */ }
を作成せよ。外積の計算は、
result[0] = v1[1]*v2[2] – v1[2]*v2[1];
result[1] = v1[2]*v2[0] – v1[0]*v2[2];
result[2] = v1[0]*v2[1] – v1[1]*v2[0];
である。その関数を用いてキーボードから2点の座標を入力してそれらの点と原点とでできる三角形の面積を以下の出力例の通りに出力するプログラムを作成せよ。2つのベクトルの外積の大きさ(長さ)はそれらのベクトルでできる平行四辺形の面積であり、三角形の面積はその1/2である。 長さを計算するには、ベクトルのそれぞれの要素の2乗和を計算してその平方根を計算する。平方根の計算には、数学関数のdouble sqrt(double)を用いる。ここでは、上記関数を呼び出して計算した外積の結果をresult[3]とし、面積を表す変数をareaとすると、
area = sqrt( result[0]*result[0] + result[1]*result[1] + result[1]*result[1])/2;
で面積が計算できる。数学関数を用いる場合には、#include <math.h>という行を#include <stdio.h> の行のすぐ下に挿入しておく。printf( )関数がstdio.hファイルに宣言されていたように、これら数学関数はmath.hファイルに宣言されている。

<出力例>
入力する2点と原点からなる三角形の面積を計算
<1点目>
X = 1
Y = 0
Z = 0
<2点目>
X = 0
Y = 2
Z = 0
面積は 1.00です。  
---------------------------------
ヒント:座標の入力は以下のプログラムで実行できる。
printf("入力する2点と原点からなる三角形の面積を計算\n");
printf("<1点目>\n");
printf("X = "); scanf("%lf", &v1[0]);
printf("Y = "); scanf("%lf", &v1[1]);
printf("Z = "); scanf("%lf", &v1[2]);
printf("<2点目>\n");
printf("X = "); scanf("%lf", &v2[0]);
printf("Y = "); scanf("%lf", &v2[1]);
printf("Z = "); scanf("%lf", &v2[2]);

OKの基準:指定の関数を作成して用いていること、三角形の面積が正しく計算出来ていること、正しくインデントされたコードを記述できていること。

====== 演習問題 5B (p5a.c) ==============

double型の配列とその要素数を受け取ってその平均、および標準偏差を計算する関数
double mean( const double data[ ], int n) { /* ........ */}
double stddev( const double data[ ], int n) { /* ...... */}
を作成せよ。それらの関数を下記プログラムのmain()関数の前に書き加えることにより、0から10までの100000個のdouble型の乱数の平均値及び標準偏差を表示するプログラムを作成せよ。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 100000

int main(void)
{
	double data[N];
	int i;
	
	srand(time(NULL)); /* 乱数列を現在時刻で初期化 */
	
	for (i=0; i < N; i++){
		data[i] = (double)rand( )*10/RAND_MAX;
	}
	printf("<rand関数を用いて作成した0から10までの%d個の乱数の平均と標準偏差を計算>\n", N);
	printf("-------------------------------------------------------------\n");
	printf("平均値:%f\n", mean( data, N));
	printf("標準偏差:%f\n", stddev( data, N));
	printf("-------------------------------------------------------------\n");
	return (0);
}

--------------------------------------------------------------------------------
(ヒント)n個のデータ di (i=0, 1, 2, ... , n-1)の標準偏差は、その平均値をmとすると以下の式で与えられる。

平方根の計算には、前問の説明のように数学関数sqrt( )を用いるには、ヘッダーファイルmath.hをincludeしておく必要がある。また、関数stddev( )の中で関数mean( )を用いても良いが、その場合上記のように関数mean( )を先に記述する必要がある。

OKの条件:指定通りの返却値、引数を持つ関数が作成出来ていること、プログラムを実行した結果が平均値5.0, 標準偏差2.9付近の値となっていること。

========

(b) 関数(3) 有効範囲と記憶域期間

「1」変数の有効範囲(スコープ;scope) テキストp.160

 プログラムで使用する変数は、宣言される場所によってその変数を使用できるソースコード内での範囲が異なります。次のプログラムでそのことを確認してください。

/*
	識別子の有効範囲を確認する
*/

#include  <stdio.h>

int	 x = 700;					/* [1]ファイル有効範囲 */

void print_x(void)
{
	printf("x = %d\n", x);		/* [A] */
}

int main(void)
{
	int	 i;
	int	 x = 800;				/* [2]ブロック有効範囲 */

	print_x();

	printf("x = %d\n", x);		/* [B] */

	for (i = 0; i < 5; i++) {
		int	 x = i * 100;		/* [3]ブロック有効範囲 */
		printf("x = %d\n", x);	/* [C] */
	}
	
	printf("x = %d\n", x);		/* [D] */

	return (0);
}

このプログラムでは、変数 x の宣言が[1]~[3]の3箇所出てきます。また、x の値を出力するprintf文が[A]~[D]の4箇所出てきます。重要な点は、内部のブロックで宣言された変数は、その外部で宣言された同じ名前の変数を覆い隠すということです。関数内部で宣言された変数が外部の同名の変数を覆い隠すこと、またその覆い隠した外部の変数に影響を与えないことは、関数をそれを用いるプログラムとは独立に作成するための非常に重要な機能です。

上のプログラムの実行結果は以下のようになります。それぞれの出力に対して上のプログラムの対応するprintf文と宣言の番号を示しました。

x = 700		[A] - [1]
x = 800		[B] - [2]
x = 0		[C] - [3]
x = 100		[C] - [3]
x = 200		[C] - [3]
x = 300		[C] - [3]
x = 400		[C] - [3]
x = 800		[D] - [2]

「2」静的変数について  関数内部で宣言された変数は関数が呼ばれるたびに初期化されますが、時には関数が呼ばれた回数などを関数内部で保持しておきたい場合があります。そのような場合に静的(static)な変数を使用します。以下のテキストp163, List 6-18 のプログラムで確認してください。

/*
	自動記憶域期間と静的記憶域期間
*/

#include  <stdio.h>

int	 fx = 0;				/* 静的記憶域期間+ファイル有効範囲 */

void func(void)
{
	static int	sx = 0;		/* 静的記憶域期間+ブロック有効範囲 */
	int			ax = 0;		/* 自動記憶域期間+ブロック有効範囲 */
				
	printf("%3d%3d%3d\n", ax++, sx++, fx++);
}

int main(void)
{
	int	 i;

	puts(" ax sx fx");
	puts("----------");
	for (i = 0; i < 10; i++)
		func();
	puts("----------");

	return (0);
}

このプログラムの関数func内の変数でsx はstatic と宣言されています。このため sx の値はprintf関数内部のインクリメント演算子によって呼ばれるたびに値が増加します。このプログラムでは、関数の外部で宣言されたファイル有効範囲を持つ変数fxもあります。ファイル有効範囲を持つ変数は、比較的小さなプログラムではどこからでもアクセスできて便利ですが、大きなプログラムではそのことが逆に分かりにくさの原因となります。

====== 演習問題 5C (p5c.c) ==============

1次の第1種ベッセル関数は、

で計算できる。引数としてdouble型のxの値を受け取り、このベッセル関数の値をdouble型で返す関数

double J_1( doubel x) { /* */}

を作成する。その関数を用いて、キーボードからxの値を入力しその値に対するこのJ_1( )関数の値を計算して下記出力例の通りに表示するプログラムを作成せよ。
ただし、計算は上のべき級数展開の式を用い、下の式を満たす項が現れる手前の項まで加算すること。

ここで、10のマイナス10乗はプログラム中では1.0E-10と記述する。また出力例のような指数表現による出力にするには、これまで%fとしていた部分を%eとすること。
出力例(イタリック太字の数字はキーボードから入力した値を示す)>
1次のBessel関数を計算します。
x = 3.83170597
J_1(3.83170597) = 8.365374E-11
----------------------
(ヒント)double x, an, sn; int n; として、anをべき級数展開のn項目の値、snを0項目からn項目までの和とする。
0項目(n: 0):an = x / 2.0; sn = an;
1項目(n: 1):an = -an * x * x / (4 *n * (n+1));(ただし右辺のanは0項目のanである。)
2項目(n: 2):an = -an * x * x / (4 *n * (n+1));(ただし右辺のanは1項目のanである)
 つまり、第n項の値はその前の第(n-1)項に、- x * x / (4 *n * (n+1))を掛ければ求まる。
これらを、an < -1.0E-10 || an > 1.0E-10の間計算して、その合計を計算する。

この関数はx = 3.83170597で0となる。このゼロ点は円形開口からのフラウンホーファー回折の最初の0点の位置を与える。

OKの基準:3.83170597を入力したときJ_1(3.83170597)が1.0E-10以下になること。

===