要約
このレッスンは、コースの第 5 単元、関数を扱う単元の始まりである。単元が何のためにあるかを説明する。問題を名前のついた部分に分解し、それぞれにインタフェースと契約を与えること、そしてある種の問題を、それ自身のより小さな事例に帰着させて解くこと、すなわち再帰を教えることである。単元の四つのレッスンと、それらを結ぶ糸を紹介する。単元が前提とすること、とりわけ第 4 単元のループとその不変条件を振り返る。単元が解決する問題を述べる。同じ計算をコピーして繰り返し、コピーの際の不注意のせいで、コンパイラが何も言わないまま誤った答えを出すプログラムの問題である。単元が各部分の仕様を定める形式として、契約(事前条件と事後条件)を前もって紹介する。そして、この先の単元と「C++ プログラミング II」のコースで、単元がどこへつながるかを示す。
学習目標
- 単元の内容の順序を説明し、各レッスンが次のレッスンに何をもたらすかを説明する。
- 第 4 単元のアルゴリズム(ループのトレース、その不変条件と変量)を思い出し、必要なら復習する。
- 例を通して、契約の考え方を前もってつかむ。プログラムの一部分がそれを使う側に何を求め、その代わりに何を保証するかである。
- 単元を終えたときに何ができるようになっているべきかを知り、それをこの先の単元で学ぶことと区別する。
導入テスト
単元に取りかかる前に、出発点を測っておくとよい。このテストは自己チェックのためのもので、採点はしない。目的はただ学習の方向づけにある。最初の五問は、単元が前提とすることを確かめる。ループのトレース、その不変条件、仕様の事前条件、初期化での値のコピー、そして第 4 単元のプログラムの中から、それ自身の入力と出力をもつ計算を見つけることである。続く五問は、レッスンの順に、単元の中心となる考え方を扱う。コールスタック、実引数の渡し方、計算と表示の分離、階乗の再帰的定義、そして自分自身を呼び出す関数の実行である。まだ正解できなくてもかまわないし、そのうちのいくつかでは「まだわからない」が誠実な答えである。正解であれ不正解であれ、どの答えもその要点を説明し、それを扱うレッスンを示すからである。同じ技能は、単元の最後のまとめのレッスンで再び測る。
単元を始める前に、自分の出発点を測りましょう。短い自己評価です。採点はされず、結果はあなたのブラウザーにだけ保存されます。
問 1前の単元から
int n{305}; int c{1}; while (n >= 10) { n = n / 10; ++c; } std::println("{}", c);は何を出力するか。周回を数え、cが 1 から始まることを忘れている。一周多く回っている。 のとき、条件n >= 10はすでに偽である。カウンタを、最初の周回の後のnの値と取り違えている。問 2前の単元からループ
int total{0}; int k{1}; while (k <= n) { total += k; ++k; }()で、条件を調べるたびに成り立つ関係はどれか。周回の途中、足した後で増やす前には成り立つが、条件を調べるときには成り立たない。終わったときにだけ成り立つ。それは事後条件であって、不変条件ではない。それはループの条件であり、ちょうど抜けるときに偽になる。不変条件はそのときにも成り立つ。問 3前の単元から除算の仕様は「整数 と が与えられたとき、 と を求めよ。ただし かつ 」と述べる。その事前条件は何か。
事前条件と事後条件を取り違えている。それは結果が満たすことであって、データに求められることではない。出力についての条件であって、入力についての条件ではない。のときは、 であって を満たすものは存在しない。問題は解をもたず、事前条件はそれを除外している。事前条件は、問題が解をもつためにデータに求められることであり、事後条件は結果が満たすことである。単元はこの組を各関数の契約に変える。事前条件は呼び出す側を縛り、事後条件は関数を縛る。復習: 問題からアルゴリズムへ
問 4前の単元から
int a{3}; int b{a}; b = 7;の後、aの値はいくつか。bとaがいつまでも結びついたままだと思っている。初期化がコピーするのは値であって、区画ではない。コピーを初期化されていない変数と取り違えている。aは 3 を受け取り、それを変えるものは何もなかった。問 5前の単元からレッスン 4.4 の、100 より小さい素数を出力するプログラムには、候補を順にたどるループがあり、その中に候補の約数を試す別のループがある。それ自身の入力と出力をもち、名前をつける価値のある計算はどの部分か。
プログラム全体の入力と出力を、内側の計算の入力と出力と取り違えている。それは一つの文であって、それ自身の意味をもつ計算ではない。誰かが使いたくなるような入力と結果をもたない。データを仕事と取り違えている。変数は計算の結果を保持するが、計算そのものではない。問 6プログラムは
mainから始まる。mainは関数gを呼び出し、gは別の関数hを呼び出す。hの実行中、進行中の呼び出しはどれか。呼び出す側は呼び出した時点で消えると思っている。実際には、止まったところから続けるために待っている。gはhを呼び出した時点で終わったと思っている。終わるのは、hが制御を返したときである。誠実な答えである。レッスン 5.1 で、スタックの図を使って説明する。問 7関数
int f(int x) { x = x + 1; return x; }があるとき、int a{3}; int b{f(a)}; std::println("{} {}", a, b);は何を出力するか。関数が実引数を変更すると思っている。既定では、関数が受け取るのはコピーである。xの変更が戻り値にさえ及ばないと思っている。仮引数は変更できないと思っている。constと宣言されていない限り、変更できる。仮引数xは、実引数のコピーで初期化されるfの変数である。関数はコピーを変えて 4 を返し、aは 3 のままである。これが値渡しであり、C++ の既定の渡し方である。復習: 引数とスコープ
問 8あるプログラムは日付を読み込み、その曜日を出力しなければならない。計算と表示を切り離す分解はどれか。
二つの仕事を混ぜている。計算した結果を再利用することも、期待される値と比べることもできない。仕事は分けているが、隠れた作用によって両者をつないでいる。呼び出しを読む人には、データがどこから来るのかが見えない。誠実な答えである。レッスン 5.3 で、どの関数を書くかの決め方を教える。こうすれば、計算は画面を見ずに数でテストでき、結果を別の形で出力する別のプログラムにも役立てられる。レッスン 5.3 はこの基準を「一つの関数に一つの仕事」と呼ぶ。復習: 関数による設計
問 9次の定義のうち、 を定義するものはどれか(すべての自然数 について)。
基底がなければ、定義は決して止まらない。 は を参照し、それが果てしなく続く。を正しく定義する( のとき)が、 を定義しないままにしている。その値は 1 である。をそれ自身によって定義している。定義は循環しており、問題を小さくしない。問 10
int f(int n) { if (n == 0) { return 0; } return n + f(n - 1); }があるとき、std::println("{}", f(3));は何を出力するか。呼び出しが一回だけ実行されると思っているか、実引数を結果と取り違えている。基底のreturn 0が呼び出しの連鎖全体の結果だと思っている。それは最後の呼び出しの結果にすぎない。関数の名前はそのヘッダの時点から既知であり、自分の本体の中で使うことができる。f(3)はf(2)を待ち、それはf(1)を待ち、それはf(0)を待つ。最後の呼び出しは 0 を返し、戻りながら 1、2、3 が足される。この関数は を計算する。二問目のループと同じ和である。復習: 再帰
コンテンツを読み込んでいます…