05 月 23 日(火)1-2h

関数の定義と呼び出し

解読しやす易く無駄の無いソースコードを書くために,関数を活用しよう.


3C 原則と関数化

3C 原則(correct, clear, compact)は, 「高品質」な実務文章(レポート,論文,ニュース,等)を作成するための秘訣 として知られている.

同類語として,5W1H(when, where, who, what, why, how)も有名. ただし,こちらは記述すべき「最低限の内容」だ. 記述の「効率的な方法」が 3C.

作文法における具体的な注意事項については,たとえば, ここ によくまとめられている.

ところで, 「3C原則 作文」でググると, なぜか本サイトがトップに踊り出て困惑... 自作自演じゃありませんよ...

この原則は作文だけでなく,プログラミングにも通じる:

解読し易く無駄の無いソースコードを書くには, 処理の意味的なブロック(カタマリ)や 類型的なパターンを見つけ出して, 別の場所に括り出しておけばよい. この「ひとまとまり」の処理手続は, C言語では,関数と呼ばれている. これは,これまでに学習した KTurtle での learn や, Scheme での define とか lambda, 等とほぼ同じものだ.

プログラムの関数化は,数学の因数分解のようなものでもある. たとえば,式 a x + b x を (a + b) x に変形するように, 共通項(x)とその他(a,b)とを分類整理すればよい.

文章であれば,たとえば, 「今日の朝,A君は,寝坊してしまったので, 朝食をとらずに登校しました. 今日の朝,Bさんは,服を決めるのに時間をかけすぎてしまい, 朝食をとらずに登校しました.」 これだと,クドいし,小学生レベルの作文だよね?

3C を意識すると, 「今朝,寝坊したA君と服選びに手間取ったBさんは, どちらも食事抜きで登校した.」 このように,洗練された大人の文章に変換できた.


関数の定義例(1)棒グラフ

最初の例として, 3つの整数 a, b, c を棒グラフ表示するプログラムを考えよう.

まず,これまで通りの方法でソースコードを作成すると, List 1 のようになるだろう.

List 1. 棒グラフ表示 bar1.c
main()
{
	int  a = 10;
	int  b = 20;
	int  c = 15;
	int  i;

	// a の棒グラフ
	for (i = 0; i < a; i++) {
		printf("a");
	}
	printf("\n");

	// b の棒グラフ
	for (i = 0; i < b; i++) {
		printf("b");
	}
	printf("\n");

	// c の棒グラフ
	for (i = 0; i < c; i++) {
		printf("c");
	}
	printf("\n");
}
データ入力の処理は面倒くさいので,今回はとりあえず, データ値を決め打ちとしておきました.

このソースでは, ほとんど同じ処理を3回も書いており, コードが無駄に長くなってしまっている.

では, 共通部分を関数化してみよう. List 1 を元にして,List 2 のように改造しよう.

関数化について理解するために,List 2 のコピペは禁止. List 1 のコードを別名保存し,再編集すること.
List 2. 棒グラフ表示(関数化版)bar2.c

/* 棒グラフを描く関数 */
bar(int value, char symbol)
{
	int  i;

	for (i = 0; i < value; i++) {
		printf("%c", symbol);
	}
	printf("\n");
}

/* メイン関数 */
main()
{
	int  a = 10;
	int  b = 20;
	int  c = 15;

	bar(a, 'a');		// a の棒グラフ
	bar(b, 'b');		// b の 〃
	bar(c, 'c');		// c の 〃
}

関数化によって,実行結果は変わらないが, ソースコードは短く・読みやすくなった.

ただし残念ながら,まだ「正しいプログラム」ではなく, 3C は完成していない. 正しいコードの書き方については,後日説明予定.
補足

なお,反復と配列も使えば, 次のように,さらにコンパクト化することも可能:

...

main()
{
	int	value[3] = {10, 20, 15};
	char	name[3] = {'a', 'b', 'c'};
	int	i;

	for (i = 0; i < 3; i++) {
		bar(value[i], name[i]);
	}
}

bar( ) の呼び出しを1個にまとめられた.


ライブラリ関数とユーザ関数

データ入出力や数学関数など, 多くのプログラムで共通に利用されるような基本的な処理手続きは, ライブラリ関数として,既に用意されている. これまでに利用してきた printf( )scanf( ) は, ライブラリ関数の例だ.

ちなみに,ifwhilefor などは関数ではない. これらは「制御構造」というものだ. 「末尾に ( ) をつける奴が関数」というのは誤解. 見かけにダマされて,短絡的に考えるな!!

一方,ソースファイルの中に新たに定義する関数は, ユーザ関数と呼ばれている. なお,これまでのCのソースではすべて, 先頭に main( ) が記述されていたが, これは特別なユーザ関数の定義だ. プログラムを起動すると,このメイン関数が最初に呼び出されることになっている. また,メイン関数の中から, 他の関数(ユーザ関数やライブラリ関数)が呼び出されることになる.

実用的なCのプログラミング作法では, 複数の小さな関数を部品のように組み合わせることによって, ひとつの大きなプログラムを構成することになる.

実は,これまでの授業でも,複数の関数を組み合わせていた. 定義したのは1個のユーザ関数 main( ) だけだったが, 複数のライブラリ関数 printf( )scanf( ),等を 利用していた. これからは,複数個のユーザ関数を定義・利用して行くことになる.

関数の定義例(2)べき乗

次の例として, 整数のべき乗 z = xy を求めるためのユーザ関数 power( ) を定義してみよう.

まず,List 3 は, 関数を定義しない場合(メイン関数だけを定義する場合)のソースコードだ.

List 3. べき乗の計算 power1.c
main()
{
	int x, y;
	int z, i;

	printf("x, y > ");			// 入力部
	scanf("%d %d", &x, &y);	

	z = 1;					// 計算部
	for (i = 0; i < y; i++) {
		z *= x;
	}

	printf("%d^%d = %d\n", x, y, z);	// 出力部
}

なお,計算部にある z *= x は, z = z*x と同じことだ. (z*x の計算結果を z の新しい値としている.) この計算を y 回繰り返すと, z の値が xy 乗になる.

これまでも使って来ましたが... 式 z = z*x なんて,数学的には明らかに間違いだ. (z = 1 の場合は数学的にも正しいが...) 左辺と右辺の値が明らかに違うのに,等しいとは? しかし,C言語的には,これは等式ではなく代入式だ. イコール記号は右辺から左辺への代入を意味する. ダマされるな!!

さて,List 3 には,List 1 のような無駄な重複は見られないのだが,気にせずに, List 3 の計算部(べき乗の計算の処理手続き)を関数化してみよう. List 3 の内容を元にして,List 4 のように書き換えよう. (教科書 pp.30-31 にも同様なプログラムが載っている.)

List 4 のコピペは禁止. 関数定義の練習のため,List 3 から再編集しよう.
List 4. べき乗の計算(関数化版)power2.c
/* x の y 乗 を計算する関数 */
int power(int x, int y)
{
	int z, i;

	z = 1;
	for (i = 0; i < y; i++) {
		z *= x;
	}
	return (z);
}

/* メイン関数 */
main()
{
	int x, y, z;

	printf("x, y > ");			// 入力部
	scanf("%d %d", &x, &y);

	z = power(x, y);			// 計算部 
	printf("%d^%d = %d\n", x, y, z);	// 出力部
		// 計算部と出力部を1つにまとめて...
		// printf("%d^%d = %d\n", x, y, power(x, y)); としても OK.
}
関数 power( ) 内の変数 x, y, z と, 関数 main( ) 内の変数 x,y, z とは, 名前は同じだが実体は別物. 変数名を変えても,まったく構わない.

List 3 と比べると List 4 では, ソースコードが長くなっているように感じるかもしれない. しかし,メイン関数だけに注目すると, べき乗の具体的な処理の記述がなくなっている分,かなり短かくなり, 「プログラム全体の見通しがよくなっている」と思うハズだ. (メイン関数の部分だけをちょっと眺めるだけで, プログラムの全体像を把握できるようになった.)

なお,記号 /**/ で囲まれている部分は, コメント(説明文)である.(これまでにも使っていた.) これは,プログラムの動作とは何も関係ないので, 書かなくても実行できる. しかし,後々のメンテナンスのことを考えると, 関数についての最低限の説明については, 必ず書いておくべき.

記号 // から行の末尾までの部分もコメントだが, これは実は,C言語ではなくC++言語のコメント文. この授業では,短いコメントについては,// を利用する. こちらについては,授業用のコメント(学生向けの説明)なので, 必ずしも書かなくてよい.

また,他のプログラムの開発中に,べき乗関数が欲しくなった場合, List 4 の power() 関数のコードを変更なしにそのまま流用できる. これも関数化のメリットだ.

関数分割について,最初のうちは,面倒に思えるかもしれないが, 後で楽するために必要な初期投資だと考えよう.

引数と戻り値

関数は,引数と戻り値を持つ. 引数(ひきすう)は関数の外部から関数の内部へ引き渡される値であり, 戻り値(リターン値,返り値)は関数の計算結果の値である. また,関数の処理内容の実行のことを関数呼び出しと言う.

List 4 の関数 power(x, y) の例では, xy が引数, return (z)z が戻り値である.

なお,呼び出し側と呼び出され側で引数を区別する場合には, 実引数(じつひきすう)と 仮引数(かりひきすう)という言葉が使われる. 関数の呼び出し時に,実引数の値が仮引数に代入される. したがって,仮引数は変数でなければならない. 実引数は定数であっても変数であっても構わない. また,当然,実引数と仮引数の型は同じでなければならない.

次のコードに規則をまとめておく:

double func(int x, double y)	// 仮引数は必ず変数
{
	...
	return (...);		// (...) は戻り値(関数と同じ型に)
}

main()
{
	int    x = 1;
	double z;
	z = func(12, func(x, 3.14));	// 実引数は何でも(定数でも変数でも関数でも)よいが,仮引数と型を一致させる
	...
}

関数は通常,定数や変数と同じように, intdouble のような型を持ち, そして関数の型は,戻り値の型によって決まる. 一方,main( ) のように戻り値や型をもたない関数も存在する. このような場合,return や型宣言を省略してよい. また,戻り値が無いことを表わすためのデータ型として, void 型も用意されている.

実は,型を省略して定義した関数は,void ではなく, コンパイラによって自動的に int 型とみなされる. main( ) も実は int 型. しかし,今のところ,細かいことは気にしないことにしよう. 後日説明予定. 初期投資を怠ると,後で首が回らなくなる.

なお,場合によっては, 戻り値をもつ関数のことを 「関数」, 戻り値をもたない関数のことを 「サブルーチン」 と言って区別することもある.


関数化の利点

関数化(サブルーチン化)には,次のようなメリットがある:

要するに,プログラムの開発効率を向上 (よいプログラムを素早く作成・改良)することが目的である.

しかし,関数化の方法の正解は1通りだけとは限らない.複数の方法があるハズだ. 読みやすくコンパクトなソースコードを作成するためには, 「処理手続きのどの部分をどのように関数化すれば効果的か?」を よく考えることが重要だ. 「闇雲な関数化は逆効果」になりかねない.

経験やセンスも必要. たくさんコードを書き,それらを何度も書き直し,経験を積もう. そして,他人の書いたコード(専門書やオープンソースソフトウェア)も参考にしよう.

ただし,鵜呑みにしてはダメだ. 世の中には,ひどいコードも多数公開されてしまっている. 本科目の課題レポートでは, 他人のコードの丸写しは大幅な減点対象となる. シラバス を確認せよ. 参考にした他人のコードについては,その良し悪しを考察するとともに, 独自の改善策を考え出すこと.


練習問題

  1. 次のソースコードにおいて, 関数 sqr( ) の実引数,仮引数,および戻り値はそれぞれ何か?
  2. L01	double sqr(double x)
    L02	{
    L03		return (x*x);
    L04	}
    L05
    L06	main()
    L07	{
    L08		printf("%f\n", sqr(2.0));
    L09	}
    L10	// ← 左端の Lxx は行番号
    
    ヒント:回答方法としては「○引数:何行目の何」みたいに3個. プログラム実行時のではなく, ソースコード中での表記そのままに書くこと.
  3. List 4 を元にして, 非負整数 n の階乗 n!を計算する関数 int factorial(int n) を定義せよ.
  4. ヒント:
    • n! = 1×2×3×…×n
    • 1!= 1
    • 0!= 1
    アドバイス:階乗の定義では,2J scheme でおなじみの「再帰」ではなく, 今回は「反復」を使おう. C言語での再帰については後日解説予定.
  5. 以前の課題のプログラムについて,適切に関数化し書き換えよ.

余裕のある人は,等差級数 Σk ak, 等比級数 Σk ak 等などにも取り組んでみよう.


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