要約
このレッスンは、C++ によるプログラミングのはじめの一歩にあてられた、コースの第一単元の始まりである。まず単元の目的を説明する。すなわち、プログラムを書いたことのない人を、ふだんの言葉で述べられた問題から、コンパイルされ、実行され、データを読み、結果を書くプログラムまで導き、その途中でそれぞれの道具が何をしたのかを分かるようにすることである。さらに、単元の六つのレッスンと、日本語で書いたアルゴリズムからエラーの分類までそれらを貫く筋道を示し、前提とするわずかな知識、すなわち学校の算数、書かれた指示に従う習慣、ファイルとフォルダの扱いを明らかにする。また、単元が解決する問題、つまり手でできる計算を、機械が間違えず疲れもせずに実行する手続きに変えるという問題を定式化し、最後に、単元がどこへ通じるか、すなわちこの先の単元と「C++ プログラミング II」コースへの道を示す。
学習目標
- 単元の内容の順序を述べ、各レッスンがそのあとのレッスンに何をもたらすかを説明する。
- コースが前提とする予備知識(整数と小数の計算、商と余りの計算、書かれた指示に従うこと、決まったフォルダにファイルを保存すること)を確認し、必要なら復習する。
- 単元がたどり方を教える道筋を、例を用いて説明する。すなわち、問題文から仕様へ、仕様からアルゴリズムへ、アルゴリズムからコンパイルされ実行されるプログラムへという道筋である。
- 単元の終わりにできるようになるはずのことを確認し、それをこの先の単元で学ぶことと区別する。
導入テスト
単元に取りかかる前に、出発点を測っておくとよい。このテストは自己チェックのためのもので、採点はしない。目的はただ学習の方向づけにある。これはコースの第一単元なので、前の単元に関する問いはない。その代わりに、最初の三問は、コースが前提とする知識を確かめる。整数の割り算の商と余り、手続きの手順の順序、そしてファイルの名前・拡張子・フォルダである。続く七問は、単元の六つのレッスンの中心となる考え方を順に扱う。プログラムを書いたことのない人が正解できなくても当然であり、それで構わない。正解であれ不正解であれ、どの答えもその要点を説明し、それを扱うレッスンを示すからである。同じ技能は、単元の最後のまとめのレッスンで再び測る。
単元を始める前に、自分の出発点を測りましょう。短い自己評価です。採点はされず、結果はあなたのブラウザーにだけ保存されます。
問 1前の単元から47 枚の紙を 6 人に、同じ数の整数枚ずつ配り、一巡分に足りない紙は取っておく。一人あたり何枚もらい、何枚取っておくか。
小数の割り算であって、整数の割り算ではない。紙は切り分けないし、問いは何枚残るかを尋ねている。商を切り上げると、ない紙が必要になる。被除数と除数が正のとき、整数の割り算の余りが負になることはない。余りを商の小数部分と混同している。余りは枚数で数えるのであって、一巡の何分の一かで数えるのではない。これは整数の割り算である。商は配り切れる巡の最大数であり、余りは残る分で、つねに除数より小さい。コースは最初のプログラムからこれを使い、C++ にはそのための演算子がある。復習: 問題からアルゴリズムへ
問 2前の単元からファイルを電子メールに添付して送るには四つの手順がある。A、宛先のアドレスを書く。B、「送信」を押す。C、新しいメッセージを開く。D、ファイルを添付する。適切な順序はどれか。
列挙の順序は実行の順序ではない。この順では、メッセージは存在する前に送られてしまう。結果は順序に左右される。送信後に添付したものは届かない。宛先も添付もないままメッセージを送ってしまう。そのあとの手順ではもう変えられない。手順には、ほかの手順に依存するもの(開いていないメッセージには添付できず、送ったあとのメッセージは書き足せない)と、依存しないものがある。この依存関係を見抜くことが、アルゴリズムを書くときにまず求められることである。復習: 問題からアルゴリズムへ
問 3前の単元からあるファイルが
C:\Users\ana\programas\suma.cppに保存されている。その名前は何か、拡張子は何か、どのフォルダにあるか。ファイルを含むフォルダをファイルそのものと混同している。名前はパスの最後の要素である。拡張子は名前の一部である。既定で拡張子を隠すシステムもあるが、コースでは表示するように求めている。suma.cppとsuma.exeはまったく別のファイルだからである。C:はパスが始まるドライブであって、拡張子ではない。パスは、ルートからファイルまでのフォルダを順に並べたものである。拡張子は名前の最後の点のあとに続く部分で、中身の種類を示す。.cppは C++ のソースコードである。これをコンパイルするには、ターミナルがそのフォルダにいるか、完全なパスを受け取る必要がある。復習: コンパイル・実行・設定
問 4あるレシピにこう書いてある。「生地が乾いていたら、水を大さじ一杯加えてこねる。乾いていなくなるまで繰り返す」。生地が乾いていなくなるには、ちょうど大さじ三杯が必要である。生地が乾いているかどうかを何回尋ねるか。
答えが「いいえ」になる問いを数えていない。この問いも行われる。それがなければ、水を加えるのをやめるべきだと誰にも分からない。手続きの文面とその実行を混同している。条件が書かれているのは一度だが、判定は周回ごとに行われる。数えているのは大さじと大さじのあいだの間隔であって、問いではない。行動する前に尋ねる繰り返しではつねに、条件は本体よりも一回多く判定される。最後の問いこそが、抜け出すことを可能にする問いである。数えながら手続きを手で追えば、このことは曖昧さなく分かる。復習: 問題からアルゴリズムへ
問 5以下の指示のうち、誰でも、あるいは機械でも、まったく同じやり方で従えるものはどれか。
実行する人の好みに左右される。二人が従えば違うやり方になり、機械には好みがない。何回なのかも、うまくいくとは何かも言っていない。終了条件が確かめられない。数が定まっていない。「大きな」は量ではなく、除数がゼロになるかもしれない。どの演算を、どのデータで行い、結果がどこに残るかを述べている。実行する人の判断に左右されるものは何もない。この明確性は、アルゴリズムに課される三つの要件の一つである。復習: 問題からアルゴリズムへ
問 6あるプログラムはエラーも警告もなくコンパイルされ、多くのデータで正しく実行されるが、ある特定のデータで「division by zero」というメッセージを出して停止する。エラーはどの時点で生じたか。
コンパイラは文面を翻訳するのであって、実行はしない。ユーザーが入力するデータも知らない。リンカはプログラムの部品を組み立てる。計算はしないし、データも見ない。別のデータでは同じ実行ファイルが正しく動く。欠陥は、プログラムがそのデータに対して行うことにあり、道具にあるのではない。プログラムの文面だけに依存することは、コンパイルやリンクのときに見つかる。データに依存することは、実行時にしか見つからない。だから、コンパイルできるプログラムだからといって、それだけでエラーがないわけではない。復習: プログラム・コンパイラ・C++
問 7Visual Studio Code とその C++ 拡張機能をインストールしたが、ターミナルで
g++ --versionを求めると、g++を認識できないという答えが返ってくる。最もありそうなことは何か。拡張機能はコンパイラを含んでおらず、ターミナルが何を見つけるかにも関わらない。コマンドg++ --versionはプログラムを一つもコンパイルしない。バージョンを尋ねるだけであり、ターミナルは尋ねる相手さえ見つけられなかった。エディタは関係ない。欠けているのはコンパイラであり、どのエディタでも同じである。エディタはコンパイルしない。別のコンパイラを呼び出すのであり、そのコンパイラはインストールされ、ターミナルから見つけられなければならない。C++ 拡張機能はコードを書くのを助けるが、コンパイラは含んでいない。問 8コンパイラが
warning:で始まるメッセージを出し、それでも実行ファイルを作る。これは何を意味するか。それならエラー(error:)であり、実行ファイルはできない。試したデータで動いても、ほかのデータについては何も保証しない。無視された警告は、しばしば明日のエラーである。実行ファイルは正常である。疑わしいのは、指摘された箇所でプログラムが行うことである。エラーは実行ファイルを得ることを妨げる。警告はそれを許すが、疑わしいコードを指摘する。多くの警告は本物の欠陥を暴くので、コースは警告が一つも出ないでコンパイルされるプログラムを求めている。復習: コンパイル・実行・設定
問 9C++ のプログラムの実行はどこから始まるか。
最初の行はふつう#includeディレクティブであり、これはコンパイルの前に処理され、実行はされない。ファイル内の順序はエントリポイントを決めない。決めるのはmainという名前である。エントリポイントの名前は規格が定めている。コンパイルのコマンドで選ぶものではない。mainはエントリポイントである。システムはプログラムを起動するときにこれを呼び出し、これが終わるとプログラムも終わる。mainのないプログラムはコンパイルはできるが、リンクはできない。復習: プログラムの解剖
問 10あるプログラムが
int age{};を宣言し、年齢を読み取るためにstd::cin >> age;を実行し、ユーザーがabcと入力する。何が起こるか。停止しない。読み取りがうまくいったかのように続行する。だからこの欠陥は危険なのである。intの変数は整数しか格納しない。文字は数に変換されず、格納もされない。読み取りはやり直さない。一度失敗して、ストリームをエラー状態のままにする。整数の読み取りは、文字に出会うと失敗する。規格は、変数が 0 を受け取ること、そしてエラーが処理されるまで以後の読み取りは何もしないことを定めている。プログラムが読み取りを確かめなければ、何の兆候もなく誤った結論を書き出す。復習: 入力・出力・エラー
単元の内容
この単元には六つのレッスンがあり、問題から動くプログラムまでの道を順にたどる。どのレッスンもその前のレッスンを前提とし、最初のレッスンは言語について何も前提としない。
- 問題からアルゴリズムへ。アルゴリズムはプログラムに先立つので、コースはコードなしで始まる。このレッスンでは、問題をその入力と出力と両者の関係(事前条件と事後条件)によって仕様として定めること、アルゴリズムを番号付きの手順で曖昧さなく記述すること、トレース表を使ってそれを手で追うこと、その中に順次・分岐・反復を見分けること、そして周回ごとに減少する量によって繰り返しが停止することを示すことを学ぶ。
- プログラム・コンパイラ・C++。機械のために書かれたアルゴリズムはプログラムであり、C++ のプログラムは書かれたままの形では実行されない。プリプロセッサ、コンパイラ、リンカがそれを実行ファイルに変換する。このレッスンでは、この三つの段階をたどり、コンパイル時に見つかることと実行時にしか現れないことを区別し、コンパイル方式とインタプリタ方式を比較し、言語の規格を時代順に位置づける。コースが使う C++23 まで、そして各コンパイラで実装が進行中の C++26 までである。
- Windows・macOS・Linux での環境構築。システムごとに一つずつ、三つの完全なガイドで、C++23 に対応したコンパイラと、エディタ Visual Studio Code とその C++ 拡張機能をインストールし、動作確認用のプログラムですべてが動くことを確かめ、よくある問題を解決する。ターミナルがコンパイラを見つけられない、バージョンが古い、規格を指定していない、といった問題である。
- コンパイル・実行・設定。編集・保存・コンパイル・実行・読むという作業の流れを、ターミナルとエディタの両方から、このコースのコマンドを使って行う。このレッスンでは、実行ファイルの生成を妨げるエラーと、疑わしいコードを指摘する警告を区別し、プログラムの実行中にそれを見張るサニタイザ(メモリエラーを検出する AddressSanitizer と未定義動作を検出する UndefinedBehaviorSanitizer)を有効にし、出力に日本語の文字の代わりに奇妙な記号が表示される理由を説明する。
- プログラムの解剖。最小のプログラムを部品ごとに見る。インクルード指令とヘッダ、関数
mainと終了コード、名前空間std、文、ブロック、コメントである。このレッスンでは、コンパイラのメッセージを最初のものから読んで構造のエラーを直す方法を学び、コースのスタイルガイドを定める。 - 入力・出力・エラー。プログラムは外部とやり取りする。
std::printとstd::printlnで、幅と小数の桁数のための書式指定を使って出力し、std::cinで読み取る。このレッスンでは、入力が期待した形をしていないときに何が起こるか、読み取りをどう確かめるかを示し、エラーをそれが現れる時点で分類して単元を締めくくる。コンパイルエラー、リンクエラー、実行時エラー、論理エラーである。
これらをつなぐ糸は、プログラムそのものである。一つ目のレッスンはそれを構想し、二つ目はそれが何に変換されるかを説明し、三つ目はそれを変換する道具をインストールし、四つ目はその道具の使い方と、道具が告げることに耳を傾ける方法を教え、五つ目はそれを解剖し、六つ目はそれを使う人と対話させる。そうであれば、単元の終わりに学生は C++ についてまだごくわずかしか知らないが、C++ でどう仕事をするかは知っている。そして、それこそがコースの残りの部分が前提とすることである。
前提とする知識
このコースは、プログラミングの経験を何も前提としない。その代わりに三つの技能を前提とする。導入テストで何か不確かな点が見つかったなら、今のうちに復習しておくとよい。
第一は学校の算数である。整数と小数の計算をし、演算の順序(かっこがなければ、掛け算と割り算を足し算と引き算より先に行う)を守り、そして何よりも整数の割り算の商と余りを求めることである。この最後のものは見かけ以上に重要である。47 枚の紙を 6 人で分けると、一人 7 枚で 5 枚余る。これを と書き、余り は除数 より小さい。C++ には商のための演算子と余りのための演算子があり、整数を使う計算の大部分(秒を時間に換算する、数を各桁に分ける、数が偶数かどうかを判定する)はこの二つに帰着する。整数の割り算をじっくり学びたい人は、「代数と幾何 I:数・関数・平面幾何」コースで、その証明とともに見つけられる。ここでは計算できれば十分である。
第二は、書かれた指示に文字どおり従い、ある指示だけでは何をすればよいか分からないときにそれに気づく習慣である。これは知識ではなく、態度の問題である。「塩を好みで加える」と読んで、この指示は実行する人によって変わると気づくことである。単元の最初のレッスンは、この態度を方法に変える。
第三は、コンピュータの基本的な操作である。フォルダを作り、そこに選んだ名前でファイルを保存し、名前の拡張子(.cpp、.txt、.exe)を見分け、各ファイルがどのフォルダにあるかを知っていることである。拡張子を表示するように、今からシステムを設定しておくとよい。Windows のように、既定で拡張子を隠すシステムもあるからである。ソースコードの suma.cpp と実行ファイルの suma.exe はまったく別のファイルであり、両者を取り違えると、ばかげたエラーのもとになる。単元ではターミナルの使い方も教えるので、あらかじめ知っている必要はない。ボタンを押す代わりにコマンドを打ち込む気があれば十分である。最後に、プログラムをインストールするには、自分のコンピュータでそれができなければならない。それができない人のために、インストールのレッスンはオンラインの代替手段を示している。
英語は前提としない。コンパイラのメッセージは英語だが、コースではそれをフィールドごとに読む方法を教えるので、そこで大事なこと(ファイル、行、欠陥の種別)は、英語に通じていなくても分かる。また、学校で学ぶ範囲を超える数学も前提としない。例に使うのは割り算、平均、九九の表、日付、時刻である。
単元が解決する問題
手でできる計算は、まだプログラムではない。両者のあいだの距離こそ、この単元が埋め方を教えるものであり、簡単な問題がその距離を測らせてくれる。
ストップウォッチが各作業の所要時間を秒数で記録する。たとえば 7384 である。これを、分と秒をつねに二桁で表す、時・分・秒のふつうの形で書きたい。2:03:04 である。手で計算すると、割り算の連鎖になる。 から 2 時間と 184 秒が得られ、 から 3 分と 4 秒が得られる。所要時間が一つなら、一分で済む。しかし記録にある千件の所要時間となると、手計算は長く、誤りやすい。この仕事は機械に実行させるのに向いている。
しかし、機械は上の説明を受け取ることができない。機械が受け取るのはプログラミング言語で書かれたテキストであり、そのテキストを書くには、手計算では暗黙のままだった三つの問題を先に解決しておかなければならない。第一は仕様である。何が入り(負でない整数の秒数)、何が出て(三つの整数、つまり時、分、秒)、どんな関係がそれらを結ぶか。 を総秒数とすると、 であり、 かつ である。第二はアルゴリズムである。すなわち、許される任意の入力に対してその出力を生む、正確な手順である。
- の値を、 を で割った商にする。
- の値を、 で割った商(割られる数は を で割った余り)にする。
- の値を、 を で割った余りにする。
- 、コロン、二桁の 、コロン、二桁の を書く。
第三は、言語が求める正確な形でアルゴリズムを C++ で書き、適切な道具でそれを実行ファイルに翻訳することである。以下のプログラムがそれを行う。そこには、まだ説明していないことが多く含まれている。変数(レッスン 2.1)、整数の割り算の商と余りを与える演算子 / と %(レッスン 3.1)、std::cin による読み取りと書式指定(レッスン 1.6)である。このプログラムの書式指定 {:02} は、数を二桁の位置に左をゼロで埋めて書くもので、そのレッスンが教える幅の一種であり、レッスン 3.1 でも使う。今のところは、その中にアルゴリズムの四つの手順を見分けられれば十分である。
#include <iostream>#include <print>int main(){ int total{}; // 変数はレッスン 2.1 で説明する std::print("所要時間(秒):"); std::cin >> total; const int hours{total / 3600}; const int minutes{total % 3600 / 60}; const int seconds{total % 60}; std::println("{} s = {}:{:02}:{:02}", total, hours, minutes, seconds);}7384所要時間(秒):7384 s = 2:03:049 行目から 11 行目が手順 1 から 3 にあたり、12 行目が手順 4 にあたる。結果が手計算と一致するのは偶然ではなく、どのプログラムも受けるべき確認である。まず、答えの分かっているデータで試してみることである。
プログラムは問題を解決するが、単元はそれを疑うことも教える。ユーザーが数の代わりに abc と入力すると、読み取りは失敗し、total はゼロのままで、プログラムは何の警報もなく、所要時間は 0:00:00 だと書く。本当らしく見える誤った結論である。負の所要時間を入力した場合は、仕様が何が起こるべきかを定めておらず、結果は意味をなさない。total / 360 を total / 3600 の代わりに書くと、プログラムはコンパイルされ、実行され、誤った結果を出すが、どの道具もそれを警告しない。これらの欠陥はそれぞれ別の種類のエラーに属し、別の時点で見つかり、別のやり方で探される。単元の最後のレッスンがそれらを分類する。ここから、単元が解決する問題は、このプログラムを書くことだけではなく、それがなぜ動くのか、どんなデータで動かなくなるのか、そしてどの道具がそれぞれの欠陥を警告するのか、あるいはしないのかを知ることでもある、ということが導かれる。
したがって、単元を終えた学生は、この種のプログラムを書き、コンパイルし、実行できるようになる。少しの入力データ、順に並んだいくつかの計算、書式をつけて書かれたいくつかの結果からなるプログラムである。C++ で分岐したり文を繰り返したりすること(日本語で書いたアルゴリズムではできるが)や、各データの型を根拠をもって選ぶことは、まだできない。それはこの先の単元がもたらすものである。
この先へ
この単元はコースの入り口である。ここで説明なしに使うものは、のちに以下の順序で説明される。第 2 単元「データと型」は、変数とそれがメモリにどう格納されるか、整数の限界(そして正しい計算がなぜオーバーフローしうるのか)、小数をもつ数の丸め、型のあいだの変換を説明する。第 3 単元「式と演算子」は、負の数の商と余りを含む C++ の算術、条件の論理、そして式において規格が未定義のまま残すものを説明する。第 4 単元「制御フロー」は、最初のレッスンが日本語で書く分岐と反復を C++ に移し、ループが正しく、かつ停止することを示す方法を教える。第 5 単元はプログラムを関数に分解し、第 6 単元は複合データ(配列、ベクター、表、レコード)を、探索とソートのアルゴリズムおよびそのコストとともに導入し、第 7 単元はテキスト、頑健な入力の読み取り、ファイルを扱い、第 8 単元は複数ファイルのプログラム、プログラムの体系的なテスト、デバッガを扱う。
このコースのあとは、「C++ プログラミング II」が、このコースが意図して外に残したものを引き継ぐ。ポインタと動的メモリ、メソッドをもつクラス、自作のテンプレート、無名関数、例外である。これらの主題はどれも、このコースの問題を解くのには必要なく、それらを後回しにすることで、どの言語でも通用することに集中できる。仕様を定め、アルゴリズムを設計し、それを明快に書き、正しいことを確かめることである。この単元のまとめのレッスンは、まさにこの区別、すなわち C++ に固有のものと、ほかのどの言語にも持っていけるものとの区別を取り上げる。