アドプロ 2018.05.21

関数

ある程度大きなプログラムを作っているとき, 複数の同じような処理を別々の場所に記述したい場合がよくある. また,たくさんのプログラムを作るようになると, 他のプログラムで使ったのと同じような処理が再度必要になる場合もよくある. こんな場合に,効率よくプログラミングするには,どうすれば良いか?

...作業に移る前に,いつもの準備をお忘れなく,


関数によるソースコードの効率化

まず,非効率なソースコードの記述例を挙げておく.

ソースファイル polygons-ng.c

#include "kame3d.h"

int main()
{
	int  i;

	Init("Kame3D Polygons");

	Goto(-3.0, -2.5);
	SetColor(1);
	for (i = 0; i < 3; i++) {	// 三角形(triangle)
		Move(2.5);
		Turn(120.0);
	}

	Goto( 1.0, -2.5);
	SetColor(2);
	for (i = 0; i < 4; i++) {	// 四角形(square)
		Move(2.5);
		Turn(90.0);
	}

	Goto( 1.0,  1.0);
	SetColor(3);
	for (i = 0; i < 5; i++) {	// 五角形(pentagon)
		Move(2.0);
		Turn(72.0);
	}

	Goto(-3.0,  1.0);
	SetColor(4);
	for (i = 0; i < 6; i++) {	// 六角形(hexagon)
		Move(2.0);
		Turn(60.0);
	}

	Goto(0.0, 0.0);

	Play();
	return (0);
}

複数の異なる多角形を描いているのだが, ソースコード的には, ほとんど同じ処理(for ループ)を何度も書いている.

これらは,「まったく同じ処理」ではないが, 「同じパターンの処理」である. 「ちょっと違う部分」を含んではいるが, ほとんど同じだ.

かつて「 違いがわかる男 」はプロの代名詞であったが, 実は,本当のプロなら「違い(ちがい,相違点)」だけでなく, 「類い(たぐい,共通点)」もわからなければならない.

C言語では,次のように, 同じパターンの処理を関数として, ひとまとめに記述できる.

これまでの本授業では,プログラムを 1 個の関数 main() だけで構成していた. これからは,プログラムを複数個の関数の集合体として構成することになる.

上のコードを元にして, 次のソースファイル polygons-func.c を作成しよう:

#include "kame3d.h"

// 以下のコードの色分けについて:違い要素類い要素

// 多角形を描く関数(n:頂点の個数,d:辺の長さ)
void Polygon(int n, double d)
{
	int    i;

	for (i = 0; i < n; i++) {
		Move(d);
		Turn(360.0/n);
	}
}

// メイン関数
int main()
{
	Init("Kame3D Polygons");

	Goto(-3.0, -2.5);
	SetColor(1);
	Polygon(3, 2.5);	// 三角形

	Goto( 1.0, -2.5);
	SetColor(2);
	Polygon(4, 2.5);	// 四角形

	Goto( 1.0,  1.0);
	SetColor(3);
	Polygon(5, 2.0);	// 五角形

	Goto(-3.0,  1.0);
	SetColor(4);
	Polygon(6, 2.0);	// 六角形

	Goto(0.0, 0.0);

	Play();
	return (0);
}

ここで,用語を説明しておく:

もちろん,int main( ) { ... } もまた関数定義である. main 関数は,プログラムの本体であり, プログラムを実行すると最初に自動的に呼び出されることになっている.

関数化前後でプログラムの実行結果については,まったく変わらない. しかし,関数化によって,ソースコード全体が短かくなった. (この例では,実際には,ほとんど変わっていないが... より大規模なプログラムの場合, その可能性については想像できるハズだ.) 特に,main 関数がコンパクトになり, プログラム全体の見通しが良くなった.

補足

優秀な諸君のことなので,次のような疑問を持つかもしれない. 「Goto(...)SetColor(...) についても, Polygon 関数の定義に入れてしまえば良いのでは? その方が短かくなるし.」 確かに,その通りである.

しかし,このプログラムの場合では, それは「やりすぎ」かもしれない. 例えば,引数が多くなり,関数呼び出しの記述方法が憶えづらくなる. それに,多角形を連続的に描画したい場合や色を変えたくない場合には, かえって面倒なことになる. 「過ぎたるは及ばざるがごとし.

結局,どの範囲までを関数化すれば良いかを考えるとき, 「唯一の正解」というものは存在しない. 「さまざまな解」があり,どの方法を選ぶかによって, プログラマのセンスが問われることになる. プログラムの設計は,エンジニアリングであると同時に, アートやサービスでもある. ユーザ(顧客)や仲間(同僚)や自分(三日後は他人同然)への 「おもてなしの心」が重要.


関数の戻り値

呼び出した側から呼び出された側へ引き渡されるデータが引数である. その逆に,呼び出された側から呼び出した側へデータを返すこともできる. それが戻り値である.

前回in-circle.c を元に改造して, 戻り値ありの関数の定義例を示す:

...

// 中心からの距離の自乗
double Dist2()
{
	double x, y;

	x = PosX();
	y = PosY();
	return (x*x + y*y);
}

int main()
{
	...
	for (i = 0; i < 100; i++) {
		if (Dist2() > r*r) {	// if (x*x + y*y > r*r) と同じ
			...
		} else {
			...
		}
		...
	}
	...
}

return (...) の括弧内の計算結果が戻り値であり, double Dist2()double が戻り値の型(関数の型)である.

引数は複数個あってもよいが, 戻り値は1個だけ,あるいはゼロ個である. 戻り値が無い関数では, 関数の型は void である. (上記の Polygon( ) の定義を見よ.)

なお,この例では,改造前と比べて,ソースコード全体が長くなってしまっている. しかし,その代わり,メイン関数が短くなったので, このプログラムの処理内容についての全体的な見通しは良くなったハズだ.


乱数

通常のプログラムでは,規則的・固定的な動作しかしないが, 乱数を利用すれば,偶発的な変化を生み出せるようになる. kame3d ライブラリでは,乱数関数 int Rand(int n) が定義されている. この関数を呼び出すと,0 以上 n 未満の整数が返される.

関数を呼び出すたびに異なる値が返される. なお,このライブラリの関数 Rand() は「再現性のない乱数」であり, プログラムを実行するたびに異なる結果となる. 一方,他のライブラリには「再現性のある乱数」もある.

Rand() の使用例:(in-circle.c の改造)

	Turn(Rand(3600)/10.0);	// 0.1(deg) 刻みのランダムな角度で回転
	...

サイコロで場合分けの例:

	n = Rand(3);	// 1/3 の確率で場合分け
	if (n == 0) {
		...
	} else if (n == 1) {
		...
	} else {
		...
	}
	...

なお,この例では,Rand(3600)/10 としてはダメだ. 実数 10.0 で割ること. C言語では整数同士の計算結果は整数になるので, Rand(3600)/10 では, Rand(360) と同じこと(0.1 刻みではなく 1 刻みの角度) を無駄に長く書いたことになってしまう.

応用:負の乱数値も使いたい場合,引き算すればよい. 例:n = Rand(7)-3; とすれば, n には範囲 -3 〜 +3 の乱数値が代入される.

もちろん,単純に,様々な多角形を描きたいだけなら:(polygons-func.c の再改造)

for (...) {
	...
	Polygon(Rand(...)..., ...);
	...
}

このように,if による場合分けは不要.


(上級者向け)関数の間接呼び出し

様々な図形を様々な大きさで描きたい場合, 次のようなコードが思いつくだろう:

void Triangle(double size) { ... }

void Square(double size) { ... }

void Star(double size) { ... }

int main()
{
	int	type;
	double	size;
	...
	Goto(Rand(100)*0.1 - 5.0, Rand(100)*0.1 - 5.0);
	type = Rand(3);
	size = Rand(10)*0.1;
	if (type == 0) {
		Triangle(size);
	} else if (type == 1) {
		Square(size);
	} else if (type == 2) {
		Star(size);
	}
	...
}

しかし,これだと if 文の単調な羅列があり,非効率だ. 次のように,関数へのポインタの配列を使うと, 効率良く記述できる:

void Triangle(double size) { ... }

void Square(double size) { ... }

void Star(double size) { ... }

int main()
{
	void (*shape[])(double size) = { Triangle, Square, Star };
		// ↑ 相違部分(関数名)だけを羅列し,配列に登録しておく.
		// shape[0](size) で Triangle(size),
		// shape[1](size) で Square(size),
		// shape[2](size) で Star(size) を呼び出せるようにした.
	...
		// ↓ 実際に,どれかの関数を呼び出す.
	shape[Rand(3)](Rand(10)*0.1);
	...
}

ただし,この技を使う場合, 対象とする関数のプロトタイプ(関数の戻り値や引数の個数・型)を 統一しておく必要がある.


本日の課題

関数の定義と呼び出しを効果的に利用して, 複雑な図案を効率良く描くような タートルグラフィックスプログラムを自由に作成せよ.

例えば,複数の種類の図形関数を定義しておき, それらをランダムに,さまざまな場所・大きさ・色で描くとか.

担当教員へレポートを送信せよ:


(c) 2018, yanagawa@kushiro-ct.ac.jp