関数1(基本)

プログラムを複数個の関数に分割すれば, ソースコードを短く,分り易く記述できる.

教科書の該当範囲:第10章,第13.1節,第13.5節

基礎知識

関数

関数は,プログラムの部品であり, 関数を組み合わせてプログラム全体を作り上げる.

関数の分類:

なお,メイン関数 main() は特別なユーザ関数であり, プログラムを実行すると,この関数が最初に自動的に呼び出される.

関数 printf() が部品なら, 制御構造 for とか数式 ++ とか型 int とかは何? 部品を作るためのより根本的な部品...材料や工具や接合具のようなもの.

部品の切り分けと組み合せをうまく設計しておけば, 大規模なプログラムの開発でも,効率的に進められるようになる.

ユーザ関数の定義と呼び出し

記述方法:

// ユーザ関数の定義
戻り値の型 関数名(型1 仮引数1, 型2 仮引数2, ..., 型N 仮引数N)
{
	...
	return (戻り値);
}

// メイン関数の定義
int main(void)
{
	...
	変数 = 関数名(実引数1, 実引数2, ..., 実引数N);	// 関数の呼出
	...
}

具体例:

// 実数 a, b の最大値を返す関数...関数名と仮引数(変数名)は自由に設定
double max(double a, double b)
{
	if (a > b) return (a);	// a の方が大なら a を返し,呼出元へ戻る
	return (b);		// b を返し,呼出元へ戻る
}

// メイン関数...プログラムはここからスタート
int main(void)
{
	double	m;
	...
	m = max(1.23, 4.56);	// max(1.23, 4.56) を呼び出すと...
		// max() 側で a = 1.23,b = 4.56 となる.
		// max() が戻り値 4.56 を返して来る.
		// その結果,main() 側で m  = 4.56 となる.
	...
}
教科書の練習問題10.1 と同じような関数です.

ところで,変数では,使う前に型宣言が必要だった. 関数でも同様に,呼び出し前に プロトタイプ宣言(戻り値と仮引数の型宣言)が必要となる. このため基本的には,ユーザ関数の定義はメイン関数の前に記述すること. または場合によっては,関数の定義を呼び出しのメインの後に書きたければ, プロトタイプ宣言をメインの前に追加すること:

double max(double a, double b);		// 関数の宣言...型と名前だけ記述,末尾にセミコロン

int main(void)
{
	...
	... max(...);	// 何型?あー宣言されてたー double だねー
	...
}

double max(double a, double b)		// 関数の定義...処理内容も記述
{
	if (a > b) return (a);
	return (b);
}

特例措置:

ライブラリ関数の利用

数学ライブラリ関数 sqrt() の利用例 sqrt.c

#include <math.h>	// ヘッダファイル math.h の取込...sqrt() の宣言

int main(void)
{
	return ((int)sqrt(100.0));	// 平方根関数 sqrt() の呼出
}

// sqrt() の定義は他のファイル(ライブラリ)内に書かれている
もちろん,計算結果を return ではなく,printf() してもよいですが... その場合 #include <stdio.h> も必要となり, ややこしくなるので省略してますよ.
$  cc  sqrt.c  -lm  -o  sqrt	# ライブラリファイル libm の連結...sqrt() の定義

$  ./sqrt
$  echo  $?		# main() の戻り値を表示
10			# √100 = 10

なお, 超基本的な関数(printf()scanf() 等)の ライブラリファイル libc については, cc ... -lc ... としなくても自動的にリンクされる.

コンパイルの仕組み

コンパイルコマンド cc は, 内部的には,次の3手順で動作する:

  1. プリプロセッサの実行(前処理...ソースの置換)
  2. コンパイラの実行(機械語への変換)の前に, ソースファイルの #include 行が ヘッダファイルの内容に置き換えられる. sqrt.c の置換結果:

    // math.h 内のプロトタイプ宣言
    ...	
    extern double sqrt(double x);
    extern double sin(double x);
    extern double cos(double x);
    ...	// extern は「関数の定義は他のファイル内にあるよ」という意味
    
    int main(void)
    {
    	return ((int)sqrt(100.0));
    }
    
  3. コンパイラの実行(ソースの翻訳 → オブジェクトの生成)
  4. ソースファイル(C言語)を変換(翻訳)し, オブジェクトファイル(機械語)を生成する.

  5. リンカの実行(オブジェクトとライブラリの連結 → プログラムの生成)
  6. 生成されたオブジェクトファイル(ユーザ関数 main() 等の定義)と 既存のライブラリファイル(ライブラリ関数 sqrt() 等の定義)とを 連結(リンク)し,実行可能なプログラムファイルを生成する.

Linux の場合, ヘッダファイル *.h はディレクトリ /usr/include/ 等に, ライブラリファイル lib*.* はディレクトリ /lib/ 等に, それぞれ保管されている. ヘッダファイルの内容は,ソースと同様にC言語で書かれている. 一方,ライブラリファイルの内容は,ソース(C言語)ではなく, コンパイル済みのオブジェクト(機械語)となっている.


ユーザ関数の例(1)無駄の削減

最初の例として, 3つの非負整数 a,b,c を棒グラフ表示するプログラムを考えよう. まず,これまで通りの方法でソースコードを作成すると, 次のようになるだろう:

$  vim  bar.c
#include <stdio.h>

int main(void)
{
	int	a = 10;
	int	b = 20;
	int	c = 15;
	int	i;

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

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

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

	return (0);
}
$  cc  bar.c  -o  bar
$  ./bar
**********
********************
***************
整数値を決め打ちとしておいたが, もちろん,実行時に入力するようにしてもよい.

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

共通部分をユーザ関数として定義し,1箇所にまとめよう:

#include <stdio.h>

/*
棒グラフを描く関数
引数 n:棒の長さ,非負整数
戻り値:なし
*/
void bar(int n)		// 戻り値なしの場合,型を void とする
{
	int	i;

	for (i = 0; i < n; i++) {
		printf("*");
	}
	printf("\n");

//	return;		// 戻り値なしの場合,return 自体を省略してもよい
}

int main(void)		// 引数なしも void とする
{
	int	a = 10;
	int	b = 20;
	int	c = 15;
	int	i;

	bar(a);	// a の棒グラフ
	bar(b);	// b の棒グラフ
	bar(c);	// c の棒グラフ

	return (0);
}

なお,「/**/」は複数行に渡るコメント, 一方,「//」は行末まで一行だけのコメント. ユーザ関数には使い方のヒントとなるようなコメントを必ず記述しておこう. 特に,引数と戻り値の意味については,記述は面倒だが,後で楽をするために重要.

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

もちろん,変数 a,b,c を配列化し,関数 bar() の呼出を反復化すれば, さらにコンパクトにも記述できますね.

ユーザ関数の例(2)意味の明確化

次の例として, 非負整数の累乗 z = xy を求めるプログラムを作成しよう. まず,ユーザ関数を定義しない(メイン関数だけを定義する)場合のソース power.c

#include <stdio.h>

int main(void)
{
	int x, y, z;
	int i;

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

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

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

	return (0);
}

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

さて,このソースには例(1)のような無駄な重複は見られないが, まあ,計算部を関数化してみよう:

教科書の練習問題10.4 と同じです.
#include <stdio.h>

/*
累乗を計算する関数
x:整数
y:非負整数
戻り値:x^y(xのy乗)
*/
int power(int x, int y)
{
	int	i;
	int	z = 1;

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

int main(void)
{
	int x, y, z;
	int i;

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

	// 計算部
	z = power(x, y);

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

	return (0);
}
さらに,計算部と出力部を1つにまとめて, 「printf(..., x, y, power(x, y));」とすれば, メイン内の変数 z を削減できる.

この変更で,ソースコードが長くなってしまったように感じるかもしれない. しかし,メイン関数だけに注目すると,計算部がかなり短かくなり, 「プログラム全体の見通しがよくなった」とも思うハズだ. メイン関数の部分だけをちょっと眺めるだけで, プログラムの全体像を把握できるようになった.


本日の課題

  1. power.c を元にして, 非負整数 n の階乗 n!を計算する関数 int factorial(int n) を定義せよ. なお,テスト用メイン関数も定義し, ソースファイルを factorial.c とすること.
  2. ヒント:
    • n! = 1×2×3×…×n
    • 1!= 1
    • 0!= 1
    アドバイス:階乗の定義では,2J scheme でおなじみの「再帰」ではなく, 今回は「反復」を使おう. C言語での再帰については後日解説予定.
  3. 数学ライブラリ関数(sqrt()sin()cos(),等)を利用して, 何らかの数学関数のグラフを描くプログラム graph.c を作成せよ.
  4. 実行例:(sin カーブ)

    $ ./graph
         *
            *
              *
            *
         *
      *  
    *   
      *
         *
    

提出:


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