要約
このレッスンは、コースの第 8 単元、すなわち最後の単元の始まりであり、この単元はより大きなプログラムを扱う。単元の目的を説明する。もはや一つのファイルにも頭の中にも収まらないプログラムを編成し、検証し、デバッグすることである。単元の三つのレッスンと、それらを結ぶ糸を紹介する。その糸は、コースの最初のレッスンで開かれた輪(仕様を定める、作る、検証する)を閉じるものである。単元が前提とすること、すなわちコース全体を振り返る。単元が解決する問題を述べる。一つのファイルからなるプログラムが、テストに使ったデータでは動くのに、仕様の境界にある誤りについては黙っているという問題である。テストすることと証明することの違いを前もって示す。そして、最終プロジェクトと「C++ プログラミング II」のコースで、単元がどこへつながるかを示す。
学習目標
- 単元の内容の順序を説明し、各レッスンが次のレッスンに何をもたらすかを説明する。
- 例を一つ挙げて、関数の宣言と定義の違い、関数を規定する契約、そして複数のファイルを一つのプログラムに変える段階(コンパイルとリンク)を思い出す。
- 例を一つ挙げて、プログラムをいくつかのケースでテストすることと、許容されるすべての入力に対して正しいと証明することの違いを前もって知る。
- 単元とコースを終えたときに何ができるようになっているべきかを知り、それを「C++ プログラミング II」で学ぶことと区別する。
導入テスト
単元に取りかかる前に、出発点を測っておくとよい。このテストは採点しない。学習の方向づけのためのものである。最初の七問は、単元が前提とすること(宣言と定義、事前条件、第 6 単元のある関数の契約、リンカのメッセージ、二分探索のケース、トレース表、ベクターの範囲外の読み出し)を確かめる。続く三問は、レッスンの順に、単元の中心となる考え方を扱い、そこでは「まだわからない」が誠実な答えである。どの答えも要点を説明し、それを扱うレッスンを示す。同じ技能は、まとめのレッスンで再び測る。
単元を始める前に、自分の出発点を測りましょう。短い自己評価です。採点はされず、結果はあなたのブラウザーにだけ保存されます。
問 1前の単元から
mainの前に書かれたint gcd(int a, int b);という行は何をするか。宣言と定義を混同している。本体がなければ実行するものは何もないし、どの関数も書かれているから実行されるのではなく、呼び出されたときに実行される。宣言と呼び出しを混同している。呼び出しには引数の型は書かない。コンパイラが呼び出しを確かめるにはシグネチャで足りることを知らない。本体がないことは、誰もそれを書かなければ、コンパイラではなくリンカが見つける。宣言があれば、コンパイラは呼び出しを一つずつ確かめられる。本体をもつ定義は、もっと下にあっても別のファイルにあってもよい。レッスン複数ファイルと自作ライブラリ全体がこの分離に基づいている。ヘッダが宣言し、実装ファイルが定義する。復習: 関数
問 2前の単元から関数
toBase(n, base)の事前条件は「n >= 0かつ2 <= base <= 10」である。それを保証しなければならないのは誰か。事前条件を入力の検証と混同している。検証はプログラムの境界の仕事であり、個々の関数の仕事ではない。コメントが確かめられると思っている。コンパイラはコメントを捨てる。契約が義務を分担させることを見ていない。契約がなければ、何かがうまくいかないとき、誰が間違えたのか誰にもわからない。問 3前の単元からレッスン 6.4 の関数
binarySearch(values, target)は、targetの位置の一つを返し、見つからなければvalues.size()を返す。その事前条件は何か。ベクターが空なら、ループは一周もせず、関数は 0、つまりvalues.size()を返す。この場合は事後条件で扱われている。関数は値がないことを知らせる。値があることを要求すれば、関数は役に立たなくなる。重複があれば、関数はその値の位置の一つを返すが、それは事後条件が許している。各周回は中央の要素と比べて区間の半分を捨てるが、それが正しいのはベクターがソートされている場合だけである。ソートされていないデータでは、関数は何の警告もなく誤った答えを返す。事前条件は契約に書き、呼び出す側がそこで目にする。復習: 探索とソート
問 4前の単元からあるプログラムはエラーなしにコンパイルできるが、実行ファイルをビルドすると
undefined reference to `tetrahedral(int)'が出る。失敗したのはどの段階か。ファイルと行を示すコンパイラのメッセージと、オブジェクトファイルと名前を示すリンカのメッセージを区別していない。プログラムはまだ存在するに至っていない。実行ファイルがなければ実行もない。#includeがなければ、コンパイル時にwas not declared in this scopeが出るのであって、このメッセージではない。コンパイラは宣言を見たので、呼び出しをすべて受け入れた。リンカはすべてのオブジェクトファイルとライブラリで定義を探したが、見つからなかった。レッスン複数ファイルと自作ライブラリがそのよくある原因を説明する。コンパイルされなかったファイルや、別の型で写された宣言である。復習: プログラム・コンパイラ・C++
問 5前の単元から次のケースの組のうち、二分探索を最もよく試すのはどれか。
量と網羅を混同している。値がない場合も境界も決して試さないが、誤りはふつうそこにある。すでに通ったケースは、新しいことを見つける見込みがいちばん小さいケースである。それは無限にあるか、型の範囲内でも天文学的な数になる。テストには選択が欠かせない。問 6前の単元から
int a{0}; int b{1}; for (int i{0}; i < 4; ++i) { const int t{a + b}; a = b; b = t; }の後で、aとbの値はいくつか。一周多く数えている。i < 4では本体は四回実行され、五回ではない。一周少なく数えている。逆向きの 1 ずれのエラーである。tがaの変わる前に計算した和を保持していることを忘れている。最後の周回ではaはbの 3 を受け取るが、bが受け取るのはその和の 5 であって、aの新しい値ではない。トレース表を一周ずつたどると となる。四周で、iは 0 から 3 までである。デバッガがプログラムを書く人に代わって埋めるのがこの表であり、レッスンプログラムのデバッグは、ブレークポイントごとの状態を判断するのにそれを使う。復習: ループ
問 7前の単元から
std::vector<int> v{1, 2}; std::println("{}", v[2]);は、コースのオプションでは停止するのに、サニタイザなしで-O2だけを付けてコンパイルすると、任意の数を書き出すことがあるのはなぜか。最適化が誤りを直すと思っている。最適化は確認を取り除き、プログラムが規格に違反しないと仮定するだけである。効果を逆にとらえている。プログラムを止めるのは、最適化なしのときのライブラリの確認である。添字の 1 ずれのエラーである。要素が二つのベクターの添字は 0 と 1 である。問 8インクルードガードをもつヘッダが、
inlineなしでdouble scale(double x)を(宣言するだけでなく)定義し、main.cppとreport.cppがそのヘッダをインクルードしている。プログラムをビルドすると何が起こるか。ガードがファイルのあいだで働くと思っている。ガードが働くのは、各翻訳単位の内部である。コンパイルするファイルとプログラム全体を混同している。各ファイルの内部では定義は一つしかない。誠実な答えである。レッスン 8.1 で、リンカの実際のメッセージとともにそれを示す。ガードは一つのファイルにヘッダが二度入るのを防ぐが、各.cppは別々にコンパイルされ、それぞれが定義のコピーを受け取る。リンカは二つの定義を見つける。レッスン複数ファイルと自作ライブラリの単一定義規則である。復習: 複数ファイルと自作ライブラリ
問 9ある関数が、念入りに選んだ五百個の自動テストを通る。何が言えるか。
数の多さと証明を混同している。どのケースも触れなかった境界の誤りは、内側のケースがいくつあっても生き残る。ケースはありうる誤りの無作為な標本ではなく、通った割合はプログラムが正しい確率を測らない。誠実な答えである。レッスン 8.2 はこの問いから始まる。問 10ループの中の
total += k;の行にブレークポイントがあり、プログラムがそこで三度目に止まる。強調表示された行は何を示しているか。停止を、すでに進めた一歩と混同している。プログラムが止まる場所(それはデバッグする人が選ぶ)を、故障の原因と混同している。誠実な答えである。レッスン 8.3 でブレークポイントとステップ実行を扱う。デバッガは印の付いた行の前で止まる。表示されるtotalの値は、それより前の二周の結果である。これを取り違えると、トレース表が一行ずれる。レッスンプログラムのデバッグがこれを掘り下げる。復習: プログラムのデバッグ
コンテンツを読み込んでいます…