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

本日の課題の要約

(1)関数の作成についての復習
(2)関数への配列の受け渡し
(3)関数原型宣言(関数プロトタイプ宣言)
(4)静的変数(Static変数)
(5)変数の有効範囲

===実習課題1:前回の復習(制限時間:5分)===

forループを使用して、1から100までの整数の和を計算するプログラムを作成して下さい。

===実習課題2:前回の復習(制限時間:5分)===

以下のプログラムは1回目の演習課題Cで扱ったBMIを計算するプログラムです。
printf("BMIは%.1lfです。\n",BMI);の部分をprintf("BMIは%.1lfです。\n",BMI(height, weight)); という形で計算できるようにBMIという名前の関数を作成してプログラムを書き換えて下さい。

#include <stdio.h>
int main(void)
{
    double height, weight, BMI;// 身長, 体重, BMI
    printf("\n");
    printf("身長は何cmですか:"); scanf("%lf",&height);
    height /= 100.0;
    printf("体重は何kgですか:"); scanf("%lf",&weight);
    BMI = weight / (height * height);
    printf("BMIは%.1lfです。\n",BMI);
    return(0);
}

===実習課題3:演習課題Aのヒント(制限時間:5分)===

以下のプログラムは、1 + 1/2 + 1/3 + ... 計算を項の大きさが1.0E-7以下となるまで行うものです。このプログラムのwhileループをforループで書き換えて下さい。

#include <stdio.h>
int main(void)
{
    double sum, an;
    int n; //初項1はn=0と考えている
    sum = an = 1.0;
    n = 1;
    while (an > 1.0E-7){
        an = 1.0 / (1.0+n);
        sum += an;
        n++;
    }
    printf("1 + 1/2 + 1/3 +... = %f\n", sum);
    return (0);
}

===実習課題4:演習課題Bのヒント(制限時間:5分)===

以下のプログラムはdouble型の配列v1[3], v2[3]の要素をキーボードから入力してその和を出力するプログラムです。
result[0] = v1[0] + v2[0]; result[1] = v1[1] + v2[1]; result[2] = v1[2] + v2[2];の部分をasum(v1, v2, result);と置き換えてプログラムが同じように機能するように書き換えて下さい。

#include <stdio.h>
int main(void)
{
    double v1[3], v2[3], result[3];
    printf("v1_x: "); scanf("%lf", &v1[0]);
    printf("v1_y: "); scanf("%lf", &v1[1]);
    printf("v1_z: "); scanf("%lf", &v1[2]);
    printf("v2_x: "); scanf("%lf", &v2[0]);
    printf("v2_y: "); scanf("%lf", &v2[1]);
    printf("v2_z: "); scanf("%lf", &v2[2]);
    result[0] = v1[0] + v2[0];
    result[1] = v1[1] + v2[1];
    result[2] = v1[2] + v2[2];
    printf("result[]= (%.1f, %.1f, %.1f)\n", result[0],result[1],result[2]);
    return (0);
}


(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  値を返さない関数の例 (p171, 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.173, 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.175 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.179, 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);
}

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

「1」変数の有効範囲(スコープ;scope)

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

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

#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変数)について  

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

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

#include  <stdio.h>

int	 fx = 0;				/* プログラム起動時に1度だけ初期化され、プログラムが終了するまで存在する */

void func(void)
{
	static int	sx = 0;		/* プログラム起動時に1度だけ初期化されプログラムが終了するまで存在するが、関数の外部からはアクセスできない */
	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もあります。ファイル有効範囲を持つ変数は、比較的小さなプログラムではどこからでもアクセスできて便利ですが、大きなプログラムではそのことが逆に分かりにくさの原因となります。

====== 演習問題 6A (p6A.c) ==============

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

で計算できる。を計算する関数
double J_1(double x); を作成せよ。ただし、級数の和は上のべき級数展開の式を下の式を満たす項が現れる手前まで計算する。

ここで、10のマイナス7乗はプログラム中では1.0E-7と記述する。この関数を用いて、出力例のようにキーボードからxの値を入力してそのベッセル関数の値を計算するプログラムを作成せよ。指数表現による出力にするには、これまで%fとしていた部分を%Eとすること。
<出力例(キーボードから3.8317を入力して計算した結果出力が0.000002>
1次のBessel関数を計算します。
x = 3.8317
J_1(3.8317) = 2.404559E-06
----------------------
(ヒント)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-7 || an > 1.0E-7の間計算して、その合計を計算する。

この関数は円形開口からの光の回折を計算する場合に重要であり、x = 3.8317で0となる。このゼロ点は収差の無い円形レンズの空間分解能を決める。
合格の条件:3.8317を入力したとき小数点以下5桁目までが0になること。

====== 演習問題 6B (p6B.c) ==============

3つのdouble型の配列v1[3], v2[3], result[3]を引数として受け取り、v1とv2を3次元ベクトルとした外積を計算して結果を配列resultに格納して返す関数
void vproduct( const double v1[3], const double v2[3], double result[3]) { /* ...... */ }
を作成せよ。配列の添え字0, 1, 2がそれぞれx, y, z座標を表す。外積の計算は、
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];
である。配列を引数としているため、関数内でresult[ ]の値をセットすれば呼び出し元の配列の値もその値となる。この関数を用いて下記出力例に従ってキーボードから2点の座標を入力してそれらの点と原点とでできる平行四辺形の面積を出力するプログラムを作成せよ。2つのベクトルの外積の大きさ(長さ)はそれらのベクトルでできる平行四辺形の面積である。 長さを計算するには、ベクトルのそれぞれの要素の2乗和を計算してその平方根を計算する。平方根の計算には、数学関数のdouble sqrt(double)を用いる。ここでは、上記関数を呼び出して計算した外積の結果をresult[3]とし、面積を表す変数をareaとすると、
area = sqrt( result[0]*result[0] + result[1]*result[1] + result[2]*result[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
面積は 2.00です。  

====== 演習問題 6C (p6C.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("\n<<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( )を用いる。また、関数stddev( )の中で関数mean( )を用いても良いが、その場合上記のように関数mean( )を先に記述する必要がある。

========