計算機実習 I
第五回 (2014 年 5 月 8 日)
関数の作り方と使い方
http://www.sw.it.aoyama.ac.jp/2014/CP1/lecture5.html
Martin J. Dürst
© 2005-14 Martin
J. Dürst 青山学院大学
今日の予定
- ミニテスト
- 前回の演習問題について
- 関数の概要
- 宣言と定義
- 関数の抽出、リファクタリング
- 関数の自己呼び出し、再帰 (recursion)
- 演習問題について
ミニテスト
- 授業開始までにログイン済み
- 授業開始まで教科書、資料、筆箱、財布などを鞄に入れ、鞄を椅子の下に
- テスト終了後その場で待つ
前回の演習問題について
- 未提出 (またはエラー): 04A1/2/3: 0人、04B1: 5人、04B2:
12人、04C1: 40人 (
partial
: 27;
完成: 23)
- 04C1 ではコンパイルエラーが多数
- 新しい内容はこれからも増える一方 →
予習、復習を一層強化
- 金曜日の午後、研究室 (O-527/529)で質問可能
- 宿題でも Q&A フォーラムをもっと利用 (土曜 24:00
まで; 発展問題は日曜日まで)、題名に注意
04C1 の解答例
文字処理の注意点
if (c == 97)
⇒ if (c == 'a')
putchar('&'); putchar('a'); putchar('m');
⇒
printf("&am");
関数の大切さ
(関数: function)
- プログラムを小分け、構造化
- 同等な処理や類似な処理を一回だけ記述
- 「何を」と「どうやって」の分離
- 「どうやって」から「何」への抽象化
プログラム言語の関数と数学の関数
- 数学的理論では「関数」さえあれば「計算」は何でもできる
- 数学の関数と C の関数は少し違う:
- 数学: 結果は普遍、副作用 (side effects) なし
- C: 副作用あり (入出力、参照渡しなど)
- 関数型言語 (functional programming language):
副作用なしの関数のプログラム言語 (例: Haskell)
- 関数が多くて短いプログラムの方がいい
(オブジェクト思考言語や関数型言語では 10
行の関数は長い)
関数関係の概念
- 関数名 (function name)
- 引数 (ひきすう、argument, parameter)
- 仮引数 (formal parameter、関数で定義、変数と同様)
- 実引数 (actual parameter、呼び出し時に渡される値)
- 戻り値 (return value、返り値とも)
関数の考え方の基本
関数の作る方と使う方を全く別のこととして考える!
- 使う方では関数の名前、引数の数と型、戻り値の型だけ考える
- 作る方では関数の中身だけ考える
- グローバル変数は禁止
- 副作用の関数は極限避ける
- 入出力は
main
関数で
(大きいプログラムでは main
関数に近く、入力や出力専用の関数で、計算の関数と別)
宣言と定義
関数 (とグローバル変数) の場合、区別される
- 宣言 (declaration)
- 物の存在、名前、型を知らせる
- 例:
long fibonacci(int i);
- 複数のファイルにわたるプログラムの場合に
.h
ファイル内
- 定義 (definition)
- 実体の存在、メモリの確保、初期化、操作の記述
- 定義は宣言の役割も果たせる
- 例:
long fibonacci(int i) { ... }
- 複数のファイルにわたるプログラムの場合に
.c
ファイル内
(定義は宣言にもなるので、教科書みたいに関数の定義の直前に宣言するのは不必要:
int main(void);
int main(void)
{...
)
関数の抽出の手順
- (空の) 新関数を作成 (意図に合った名前)
- 抽出したいコードを新関数にコピー
- 抽出したコード内のローカル変数の使用をチェック
→新関数のローカル変数または引数
- 抽出したコードでのローカル変数の変更をチェック
→一個の場合、新関数の戻り値
→複数の場合、関数の抽出を断念
- コンパイル (新関数の文法チェック)
- 元の場所で抽出されたコードを新関数の呼出しに変更
- 新関数だけ使用された変数を元の場所で削除
- コンパイル、テスト
(Refactoring: Improving the Design of Existing Code, Martin Fowler et
al., Addison-Wesley, 1999 の「メソッドの抽出より」)
レファクタリング
(refactoring)
- 更なる機能の追加の準備や
デサインの向上のため、
プログラムの機能を変更せずに
プログラムを書き直す
- 簡単にできる小さなプログラムの変更
- 例:
- 一時変数の導入 (または削除)
- 関数の名前の変更
- 関数の抽出 (又はインライン化)
- オブジェクト指向プログラミングでよく使われるが、C
でも利用可能
- テストとの組み合わせで力を発揮
関数の自己呼び出し
すごく単純な例: 1 から n までの合計:
int sumN (int n)
{
if (n <= 0)
return 0;
else
return n + sumN(n-1);
}
数学的根拠: ∑ni=0
i = n +
∑n-1i=0 i
(n>0 の場合);
∑0i=0 i = 0
sumN
の実行例
sumN
A(n
A=3)
n
A > 0
sumN
B(n
B=2)
n
B > 0
sumN
C(n
C=1)
n
C > 0
sumN
D(n
D=0)
n
D ≦ 0
return
0
return
1 + 0
return
2 + 1
return
3 + 3
→ 結果が 6
再帰的関数
- 「自己呼び出し」の関数は「再帰的関数」 (recursive
function)
- 原理: 再帰 (recursion)
- 自己呼び出しは間接的でも可
- 再帰は数学的帰納法 (mathematical induction) と深い関係
- 問題の定義によって、再帰的な関数が書きやすい
再帰的関数のもう一例
ある数 i を n 進数で出力:
void printNary (int i, int n) {
static char letters[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
if (i < n)
putchar (letters[i]);
else {
printNary (i/n, n);
putchar (letters[i%n]);
}
}
printNary
の実行例
printNary
A(iA=83, nA=6)
- iA ≧ nA
printNary
B(iB=13,
nB=6)
- iB ≧ nB
printNary
C(iC=2,
nC=6)
- iC < nC
putchar
('2')
putchar
('1')
putchar
('5')
→ 結果が 215
再帰的関数の要点
- 問題の分担、簡単化が可能
- 引数とローカル変数は呼び出しごと別に存在
- 「同じ関数」のではなく「同じような関数」で考えた方がよい
(「同じ人」) ではなく、「同じ仕事をこなせる人」)
再帰のパターン (1)
- 再帰しない基底
- 必ず前もってチェック
- そうしないと暴走
(スタック・オーバーフロー、stack overflow;
環境によって segmentation fault として報告)
- 再帰そのもの
再帰のパターン (2)
- 再帰、処理 (
sum
, printNary
,...)
- 処理、再帰 (末尾再帰/tail recursion)
- 処理、再帰、処理 (05C2)
- 再帰、再帰、処理 (Fibonacci 関数)
- 再帰、処理、再帰 (ハノイの塔)
プログラム開発のコツ
(advice for program development)
- 大きいプログラムを小さい部分に分ける
- 開発は小さい部分で:
- 全体をおおざっぱに分けるだけ (top-down)
- 一部の小さい機能から (bottom-up)
- プログラムを常にインデントされ、読みやすい状態に保つ
- プログラムを常にコンパイルできる状態に保つ
- まめにコンパイル・実行・テスト
演習問題の概要
- 05A1: 簡単なプログラムから階乗関数を抽出
- 05B1: 組み合わせの表の計算
- 05B2: Josephus 関数
- 05B3: Fibonacci 関数 (数列) の再帰的プログラム
- 05C1: 学生番号分析プログラムを関数で書き直す
- 05C2 (発展問題、部分点無し):
再帰を使って入力を逆順にする
(05C1 以外は全部非常に短い)
問題再利用の目的
- 実世界でプログラムの更新が多い
- リファクタリングとテストの練習
- 時間の短縮 (短い時間で長い問題に挑戦可能)
- 自分のプログラムの読み直し (何週間後で再読)
- 複数のプログラミングスタイルの比較
次回の準備
- 今日の復習
- 残った演習問題を宿題として完成させる
- 教科書の第 10 章 (構造体, pp. 242-279) を読む (pp. 272-273
(
typedef
))