要約
このレッスンは、テキストとファイルを扱うコースの第七単元を開く。単元の目的を説明する。テキストを、その実際のエンコーディングを含めてデータとして扱うこと、そして不正な形式の入力に出会っても壊れずにファイルを読み書きするプログラムを書けるようにすることである。四つのレッスンと、それらを結ぶ糸を紹介する。単元が前提とすること、とりわけ機械にとって文字は数であること、そして第 5 単元と第 6 単元のベクター、レコード、関数を振り返る。単元が解決する問題を述べる。正しく見えるのに、黙ってデータを失い、最初の予想外の行で止まり、ASCII 以外の文字を含む名前を正しく測れないプログラムの問題である。一つの文字がいつも一バイトを占めるとは限らないことを予告する。そして、第 8 単元と「C++ プログラミング II」のコースで、単元がどこへ通じるかを示す。
学習目標
- 単元の内容の順序を説明し、各レッスンが次のレッスンに何をもたらすかを説明する。
- 文字を数値のコードとして表すこと(レッスン 2.4)を思い出し、コードを二進法と十六進法で書く。
- 例を通して、一つの文字がいつも一バイトとは限らず、したがってバイトを数えることは文字を数えることではないと前もってつかむ。
- 単元を終えたときに何ができるようになっているべきかを知り、それをこの先の単元と「C++ プログラミング II」で学ぶことと区別する。
導入テスト
単元に取りかかる前に、出発点を測っておくとよい。このテストは自己チェックのためのもので、採点はしない。目的はただ学習の方向づけにある。最初の六問は、単元が前提とすることを確かめる。文字のコード、数の二進法と十六進法での書き方、ビットマスク、要素を選び出すベクターの走査、読み取りが数の代わりに文字を受け取ったときに起こること、そしてレコードの定義である。続く四問は、レッスンの順に、単元の中心となる考え方を扱う。何も見つからない検索が返すもの、ASCII 以外の文字を含む単語のサイズ、二つの読み方を混ぜたときに現れる空行、そして開けなかったファイルである。この四問にまだ正解できなくてもかまわないし、そのうちのいくつかでは「まだわからない」が誠実な答えである。正解であれ不正解であれ、どの答えもその要点を説明し、それを扱うレッスンを示すからである。同じ技能は、単元の最後のまとめのレッスンで再び測る。
単元を始める前に、自分の出発点を測りましょう。短い自己評価です。採点はされず、結果はあなたのブラウザーにだけ保存されます。
問 1前の単元から
std::println("{:d} {:d}", 'A', 'a');は何を出力するか。小文字が大文字のすぐあとに続くと思っている。両者のあいだには 32 個のコードがあり、途中にいくつかの記号がある。{:d}がコードを求めることを見落としている。それがなければstd::printlnは文字を書く。順序を逆にしている。ASCII では大文字のコードのほうが小文字より小さい。charは数、つまり文字のコードを保持し、{:d}は文字ではなくその数を書くよう求める。ASCII では大文字は 65 から、小文字は 97 から始まる。差は 32、つまりビット一つである。レッスン テキストは ASCII ではない は、ここから出発して、ASCII にない文字がどうなるかを説明する。復習: 文字・真偽値・型変換・定数
問 2前の単元から233 は二進法と十六進法でどう書くか。
下位四ビットのまとめ方を誤っている。1001は 9 であって 1 ではない。ビットを逆向きに、最下位から最上位へと読んでいる。十六進の表記と十進の表記を混同している。。右から四つずつまとめると、ビット1110と1001は 14 と 9、つまりeと9である。これは「é」のコードであり、単元では、今日のテキストファイルではこのコードが一バイトのe9としてではなく、二バイトとして保存されることを示す。復習: 整数とその表現
問 3前の単元から
0xB1 & 0xC0の値はいくつか。&と|を混同している。ビットごとの OR は上位ビットを選び出すのではなく立ててしまう。マスクは何も変えないと思っている。マスクは下位六ビットを消す。&と^、つまり XOR を混同している。0xB1は10110001、0xC0は11000000である。ビットごとの AND は0xB1の上位二ビットだけを残し、それは10である。このマスクこそ、レッスン テキストは ASCII ではない が文字を続けるバイトを見分けるのに使うものである。復習: ビットと数学ライブラリ
問 4前の単元から
std::vector<int> v{4, 7, 10, 3}; std::vector<int> w{}; for (const int x : v) { if (x % 2 == 0) { w.push_back(x); } } std::println("{}", w);は何を出力するか。条件を逆にしている。x % 2 == 0は偶数に対して真である。push_backがベクター全体を写すと思っているか、選び出しがvに適用されてvが出力されると思っている。push_backが先頭に追加すると思っている。追加されるのは末尾である。ループはvを走査し、偶数だけを現れた順にwへ写す。vは変わらない。単元はテキストにも同じ型を使う。文字列のバイトを走査していくつかを残す、あるいはファイルの行を走査して正しい行を残す、というようにである。復習: ベクター
問 5前の単元から
int n{-1}; std::cin >> n;のあと、ユーザーがabcと入力する。nの値はいくつで、そのあとの読み取りはどうなるか。失敗した読み取りは跡を残さないと思っている。跡は変数とストリームの状態に残る。C++ が不正な入力を知らせると思っている。プログラムは読み取りがうまくいったかのように続く。整数の読み取りと文字の読み取りを混同している。問 6前の単元から次の定義のうち、題名と年を持つレコードを正しく宣言しているのはどれか。
閉じ波括弧のあとのセミコロンを忘れている。レコードを書く人が最初にする誤りであり、コンパイラはそれを正確に指摘する。リストの要素のようにフィールドをカンマで区切っている。各フィールドは一つの宣言であり、セミコロンで終わる。型の異なる、名前のついたフィールドを持つレコードを、要素がすべて同じ型のベクターと混同している。問 7
sがstd::string s{"casa azul"};のとき、s.find("x")は何を返すか。ほかの言語の慣習である。std::size_tには符号がなく、それに変換された-1はちょうどnposになるが、結果を-1と比較してうまくいくのはこの変換のおかげにすぎず、コンパイラの警告も出る。「見つからない」を位置 0 と混同している。位置 0 は文字列の先頭に出現したときの位置である。誠実な答えである。レッスン 7.1 でfindとnposを紹介する。問 8UTF-8 で保存されたプログラム(このコースのプログラムはそうである)で、
std::println("{}", std::string{"año"}.size());は何を出力するか。文字を数えている。sizeが数えるのは、それらを書くのに使われたバイトである。どの文字も二バイトを占めると思っている。ASCII の文字は一バイトである。誠実な答えである。レッスン 7.2 でバイト単位で説明する。sizeが数えるのは文字ではなくバイトである。スペイン語の単語「año」(年)では、「a」と「o」はそれぞれ一バイトを占め、ASCII にない「ñ」は UTF-8 で二バイトを占める。レッスン テキストは ASCII ではない はその理由を説明し、文字の数え方を教える。復習: テキストは ASCII ではない
問 9プログラムが
int age{}; std::string name{}; std::cin >> age; std::getline(std::cin, name);を実行し、ユーザーが36と入力して Enter キーを押し、花子と入力して Enter キーを押す。nameには何が残るか。getlineが新しい行から始まると思っている。始まるのは>>が止まったところである。getlineが>>のすでに消費したものをもう一度読むと思っている。誠実な答えである。レッスン 7.3 はこの欠陥から始まる。問 10プログラムが
std::ifstream in{"data.txt"}; int x{-1}; in >> x;を実行するが、実行しているフォルダにdata.txtが存在しない。何が起こるか。存在しないファイルを開くことは C++ が報告するエラーだと思っている。プログラムは何の知らせもなく続く。読むために開くことと、書くために開くことを混同している。後者は実際にファイルを作る。誠実な答えである。レッスン 7.4 はこの確認から始まる。
コンテンツを読み込んでいます…