関数1(基本)

プログラムを複数個の関数に分割すれば, 全体像を分り易く,また短く記述できる.

外見的な関数の書き方や使い方だけでなく, 内面的なコンパイルの仕組みについても理解しよう.

教科書の該当範囲:第4章(第4.1〜4.2節),第10章,第11.1節

基礎知識

関数

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

関数の分類:

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

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

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

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

記述方法:

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

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

具体例:(書き方1)

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

	// else return (b); と書きたいかもしれないが...
	// return (a) の後に次の文 return (b) に連接することは絶対にないので...
	// else は冗長・不要.Keep It Short and Simple!!
}

// メイン関数...プログラムはここからスタート
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 となる.
	...
}
教科書の List 4_2 と類似したプログラムです. そちらでは,if文の代わりに条件演算子「?」を利用.

ところで,変数では,使う前に型宣言が必要だった. 関数でも同様に,呼び出し前に プロトタイプ宣言(戻り値と仮引数の型宣言)が必要となる. このため基本的には,ユーザ関数の定義はメイン関数の前に記述すること.

または場合によっては, ユーザ関数(呼び出され側)の定義を メイン関数(呼び出し側)の後に書いても良い. ただし,次の具体例のように, プロトタイプ宣言をメインの前に記述しなけばならない.

具体例:(書き方2)

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

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

double max(double a, double b)	// 関数の定義...処理内容も記述
{
	if (a > b) return (a);
	return (b);
}
これだと,double max(...) とかを2回も書くことになり,面倒.

特例措置:

ライブラリ関数の利用

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

#include <math.h>	// ヘッダファイル math.h の取込...sqrt() の宣言
	// sqrt() のプロトタイプ宣言がヘッダファイル math.h 内に書かれている.
	// それをソースファイルに取り込んで利用する.
	// ソース内で宣言し直す必要はない.

int main(void)
{
	return ((int)sqrt(100.0));	// 平方根関数 sqrt() の呼出
		// sqrt() の定義は他のファイル(ライブラリ)内に書かれている.
		// それをコンパイル時にリンク(連結)して利用する.
}
もちろん,計算結果を return ではなく,printf() してもよいですが... その場合 #include <stdio.h> も必要となり, ややこしくなるので省略してますよ.
$ cc sqrt.c -lm -o sqrt	# ライブラリ libm の連結
	# ライブラリ関数の定義は,誰かが過去にコンパイル済みなので...
	# 再コンパイルではなく,リンク(連結)だけで使える

$ ./sqrt

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

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

コンパイルの仕組み

コンパイルコマンド cc は, 内部的には,次の3手順を実行する:

  1. プリプロセッサ(preprocessor):コンパイルの前処理(preprocess)...ソースの置換

    コンパイラの実行(機械語への変換)の前に, ソースコードの #include 行が ヘッダファイルの内容に置き換えられる. sqrt.c の置換結果:(抜粋)

    #include <math.h>      // math.h の内容に置換↓
    // math.h 内のプロトタイプ宣言
    ... 
    extern double sqrt(double x);
    extern double sin(double x);
    extern double cos(double x);
    ...        // extern は「関数の定義は外部(external,他のファイル内)に書いてあるよ」という意味
    
    int main(void)
    {
            return ((int)sqrt(100.0));
    }
    
    ちなみに,前回紹介したマクロの展開についても プリプロセッサが担当していますよ.
  2. コンパイラ(compiler):ソースの翻訳(compile)→ オブジェクトの生成

    置換されたソースコード(C言語)を変換(翻訳)し, オブジェクトコード(機械語)を生成する.

  3. リンカ(linker):オブジェクトとライブラリの連結(link)→ プログラムの生成

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

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

実験室の環境では, sqrt() のプロトタイプ宣言は, /usr/include/math.h から更にインクルードされている /usr/include/x86_64-linux-gnu/bits/mathcalls.h にありました.

プリプロセッサの置換結果は, コマンド「cpp sqrt.c | less」で確かめられます.

また,ライブラリ libc.*libm.*/usr/lib64/ とか /usr/lib/x86_64-linux-gnu/ にあるでしょう.


本日の作業

タスク1:ユーザ関数の例(1)無駄の削減

関数定義が必要となる簡単な例として, 3つの非負整数 a,b,c を棒グラフ表示するプログラムを考えよう.

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

「実習の準備」等については,もう省略でいいよね. 各自,適切に準備しよう.

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箇所にまとめよう. bar.c:改善例

#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:ユーザ関数の例(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);
}
前回までの知識では,こんな書き方でも OK でした. しかし,今後は「残念」となるかもしれません.

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

$ cc power.c -o power
$ ./power
x, y > 2 8
2^8 = 256

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

#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);
}

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

さらに短く,計算部と出力部とを一体化してみよう:

...

int main(void)
{
	int	x, y, z;
	// 入力部
	...
	// 計算・出力部
	printf("%d^%d = %d\n", x, y, power(x, y));
	...
}
関数値 power(x, y) を表示するだけなら, 変数 z は,いらない子でした.

タスク3:整数の符号判定

(本日の課題1です.)

入力された整数 x の正/負に応じて 符号値 +1,-1,または 0 を出力するプログラム sign.c を作成せよ. ただし,符号関数 int sign(int x) を定義・利用すること.

実行例:

$ ./sign
整数 > 123
+1		# 「+」は表示しなくても,まあOK
整数 > 0
0
整数 > -45
-1
...
[Ctrl]+[C]
$
ヒント:sign() の記述方法については, 「基礎知識」にあった max() を参考に,コンパクトに記述しよう.
正数を符号付きで表示するには,変換指定子 "%+d" とか.

タスク4:反復による割り算(関数版)

(本日の課題2です.)

以前作成した反復による割り算のプログラム div.c を適切に関数分割せよ. つまり,割り算を計算する関数を作成せよ.

以前は商と同時に剰余も計算していた. しかし今回,剰余の方は計算しなくて(返さなくて)よい. C言語では,関数の戻り値は1個だけに限られているので.
ヒント:記述方法については, power.c を参考にしよう.

タスク5:数学関数グラフの描画

(本日の課題3です.)

数学ライブラリ関数(sqrt()sin()cos(),等) を利用して, 何らかの数学関数 y = f(x) のグラフを描くプログラム graph.c を作成せよ. ただし,グラフの1点を描く関数 void plot(int y) を定義・利用すること.

実行例:(sin カーブ)

$ ./graph
     *
        *
          *
        *
     *
  *  
*   
  *
     *
ヒント:plot() の定義については, bar() を参考にしよう. その他の詳細な仕様については,自由に設定してよい.
アドバイス:数学関数などのライブラリ関数を利用する場合, ソースコードにはインクルードを,コンパイルにはリンクを, それぞれ追加する必要がある.「基礎知識」を参照せよ.
注意:大抵の数学関数の戻り値は実数. 一方,端末画面上のグラフの点の座標は整数. ...という訳で,グラフを美しく表示するには, 適切な四則演算による数値の調整も必要.

本日の課題

レポートを提出せよ.

質問 Q1〜Q5 に回答し,電子メールで提出せよ.